Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 70 additions & 10 deletions lazer/sdk/rust/protocol/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,22 @@ pub enum PayloadPropertyValue {
Price(Option<Price>),
BestBidPrice(Option<Price>),
BestAskPrice(Option<Price>),
PublisherCount(Option<u16>),
PublisherCount(u16),
Exponent(i16),
Confidence(Option<Price>),
FundingRate(Option<i64>),
FundingTimestamp(Option<u64>),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a funding timestamp? Why is the main timestamp of the update not sufficient?

I also suggest creating a newtype called Rate which works similar to Price instead of using raw i64. We can also use TimestampUs for the timestamp here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funding timestamp is the timestamp of the funding rate which is different from the timestamp we have in the aggregated update (which is the time we created the aggregate). Also the timestamp needs to be different per feed as each funding rate has different intervals.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But do the users care about that timestamp? Why can't they use the timestamp of the aggregate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to have the timestamp. It needs to be verified too, for example if publishers don't see the latest data that just came out we will send an update with a timestamp for the next period but with the data of the previous rate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be hard to explain to consumers why there are two different timestamps and how they should use them. Consumers shouldn't be required to even be aware of funding periods, and it's our responsibility to make sure that our data does not contain stale values. To protect against stale values, it makes more sense to require publishers to provide the expiration timestamp for their update. Then we can clear expired updates on aggregation. This way consumers wouldn't need to worry about that at all so they wouldn't need to check a separate timestamp. Actually, if we're choosing to add a timestamp field, the rate expiration timestamp would be more useful to users than the start of the period.

Is it possible to fetch the funding rate for a new period ahead of time? Ideally we should be serving the new rate immediately when the new period starts. If it's not possible, I guess the best action would be to return None until we fetch the new rate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to keep the data similar to the funding rate apis of the exchanges. It will be easier to explain it to users that the data coming is exactly the same as binance api for example. The timestamp is the actual time that the funding happened, it makes sense to have it in the data. If we go with the expiration date instead, it will be more confusing to the consumers as we have to explain what this value exactly represents.

Getting the funding rate for next interval is impossible as it will change when users buy/see futures. I checked to see if there are any apis to get the instantaneous funding rate (i.e the next funding rate if nothing changes until next interval), but i couldn't find any.

}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AggregatedPriceFeedData {
pub price: Option<Price>,
pub best_bid_price: Option<Price>,
pub best_ask_price: Option<Price>,
pub publisher_count: Option<u16>,
pub publisher_count: u16,
pub confidence: Option<Price>,
pub funding_rate: Option<i64>,
pub funding_timestamp: Option<u64>,
}

pub const PAYLOAD_FORMAT_MAGIC: u32 = 2479346549;
Expand Down Expand Up @@ -82,6 +86,12 @@ impl PayloadData {
PriceFeedProperty::Confidence => {
PayloadPropertyValue::Confidence(feed.confidence)
}
PriceFeedProperty::FundingRate => {
PayloadPropertyValue::FundingRate(feed.funding_rate)
}
PriceFeedProperty::FundingTimestamp => {
PayloadPropertyValue::FundingTimestamp(feed.funding_timestamp)
}
})
.collect(),
})
Expand Down Expand Up @@ -113,7 +123,7 @@ impl PayloadData {
}
PayloadPropertyValue::PublisherCount(count) => {
writer.write_u8(PriceFeedProperty::PublisherCount as u8)?;
write_option_u16::<BO>(&mut writer, *count)?;
writer.write_u16::<BO>(*count)?;
}
PayloadPropertyValue::Exponent(exponent) => {
writer.write_u8(PriceFeedProperty::Exponent as u8)?;
Expand All @@ -123,6 +133,14 @@ impl PayloadData {
writer.write_u8(PriceFeedProperty::Confidence as u8)?;
write_option_price::<BO>(&mut writer, *confidence)?;
}
PayloadPropertyValue::FundingRate(rate) => {
writer.write_u8(PriceFeedProperty::FundingRate as u8)?;
write_option_i64::<BO>(&mut writer, *rate)?;
}
PayloadPropertyValue::FundingTimestamp(timestamp) => {
writer.write_u8(PriceFeedProperty::FundingTimestamp as u8)?;
write_option_u64::<BO>(&mut writer, *timestamp)?;
}
}
}
}
Expand Down Expand Up @@ -162,11 +180,15 @@ impl PayloadData {
} else if property == PriceFeedProperty::BestAskPrice as u8 {
PayloadPropertyValue::BestAskPrice(read_option_price::<BO>(&mut reader)?)
} else if property == PriceFeedProperty::PublisherCount as u8 {
PayloadPropertyValue::PublisherCount(read_option_u16::<BO>(&mut reader)?)
PayloadPropertyValue::PublisherCount(reader.read_u16::<BO>()?)
} else if property == PriceFeedProperty::Exponent as u8 {
PayloadPropertyValue::Exponent(reader.read_i16::<BO>()?)
} else if property == PriceFeedProperty::Confidence as u8 {
PayloadPropertyValue::Confidence(read_option_price::<BO>(&mut reader)?)
} else if property == PriceFeedProperty::FundingRate as u8 {
PayloadPropertyValue::FundingRate(read_option_i64::<BO>(&mut reader)?)
} else if property == PriceFeedProperty::FundingTimestamp as u8 {
PayloadPropertyValue::FundingTimestamp(read_option_u64::<BO>(&mut reader)?)
} else {
bail!("unknown property");
};
Expand Down Expand Up @@ -194,16 +216,54 @@ fn read_option_price<BO: ByteOrder>(mut reader: impl Read) -> std::io::Result<Op
Ok(value.map(Price))
}

fn write_option_u16<BO: ByteOrder>(
fn write_option_i64<BO: ByteOrder>(
mut writer: impl Write,
value: Option<i64>,
) -> std::io::Result<()> {
match value {
Some(value) => {
writer.write_u8(1)?;
writer.write_i64::<BO>(value)
}
None => {
writer.write_u8(0)?;
Ok(())
}
}
}

fn read_option_i64<BO: ByteOrder>(mut reader: impl Read) -> std::io::Result<Option<i64>> {
let present = reader.read_u8()? != 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what you were discussing about the onchain payload right? We need to implement this function on contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the contract needs to update to support this.

if present {
Ok(Some(reader.read_i64::<BO>()?))
} else {
Ok(None)
}
}

fn write_option_u64<BO: ByteOrder>(
mut writer: impl Write,
value: Option<u16>,
value: Option<u64>,
) -> std::io::Result<()> {
writer.write_u16::<BO>(value.unwrap_or(0))
match value {
Some(value) => {
writer.write_u8(1)?;
writer.write_u64::<BO>(value)
}
None => {
writer.write_u8(0)?;
Ok(())
}
}
}

fn read_option_u16<BO: ByteOrder>(mut reader: impl Read) -> std::io::Result<Option<u16>> {
let value = reader.read_u16::<BO>()?;
Ok(Some(value))
fn read_option_u64<BO: ByteOrder>(mut reader: impl Read) -> std::io::Result<Option<u64>> {
let present = reader.read_u8()? != 0;
if present {
Ok(Some(reader.read_u64::<BO>()?))
} else {
Ok(None)
}
}

pub const BINARY_UPDATE_FORMAT_MAGIC: u32 = 1937213467;
Expand Down
47 changes: 47 additions & 0 deletions lazer/sdk/rust/protocol/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,49 @@ pub struct PriceFeedData {
/// `None` if no value is currently available.
#[serde(with = "crate::serde_price_as_i64")]
pub best_ask_price: Option<Price>,
/// Last known value of the funding rate of this feed.
/// `None` if no value is currently available.
pub funding_rate: Option<i64>,
}

/// Old Represents a binary (bincode-serialized) stream update sent
/// from the publisher to the router.
/// Superseded by `PriceFeedData`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceFeedDataV0 {
pub price_feed_id: PriceFeedId,
/// Timestamp of the last update provided by the source of the prices
/// (like an exchange). If unavailable, this value is set to `publisher_timestamp_us`.
pub source_timestamp_us: TimestampUs,
/// Timestamp of the last update provided by the publisher.
pub publisher_timestamp_us: TimestampUs,
/// Last known value of the best executable price of this price feed.
/// `None` if no value is currently available.
#[serde(with = "crate::serde_price_as_i64")]
pub price: Option<Price>,
/// Last known value of the best bid price of this price feed.
/// `None` if no value is currently available.
#[serde(with = "crate::serde_price_as_i64")]
pub best_bid_price: Option<Price>,
/// Last known value of the best ask price of this price feed.
/// `None` if no value is currently available.
#[serde(with = "crate::serde_price_as_i64")]
pub best_ask_price: Option<Price>,
}

impl From<PriceFeedDataV0> for PriceFeedData {
fn from(v0: PriceFeedDataV0) -> Self {
Self {
price_feed_id: v0.price_feed_id,
source_timestamp_us: v0.source_timestamp_us,
publisher_timestamp_us: v0.publisher_timestamp_us,
price: v0.price,
best_bid_price: v0.best_bid_price,
best_ask_price: v0.best_ask_price,
funding_rate: None,
}
}
}

/// A response sent from the server to the publisher client.
Expand All @@ -57,6 +100,7 @@ fn price_feed_data_serde() {
4, 0, 0, 0, 0, 0, 0, 0, // price
5, 0, 0, 0, 0, 0, 0, 0, // best_bid_price
6, 2, 0, 0, 0, 0, 0, 0, // best_ask_price
1, 7, 3, 0, 0, 0, 0, 0, 0, // funding_rate
];

let expected = PriceFeedData {
Expand All @@ -66,6 +110,7 @@ fn price_feed_data_serde() {
price: Some(Price(4.try_into().unwrap())),
best_bid_price: Some(Price(5.try_into().unwrap())),
best_ask_price: Some(Price((2 * 256 + 6).try_into().unwrap())),
funding_rate: Some(3 * 256 + 7),
};
assert_eq!(
bincode::deserialize::<PriceFeedData>(&data).unwrap(),
Expand All @@ -80,6 +125,7 @@ fn price_feed_data_serde() {
4, 0, 0, 0, 0, 0, 0, 0, // price
0, 0, 0, 0, 0, 0, 0, 0, // best_bid_price
0, 0, 0, 0, 0, 0, 0, 0, // best_ask_price
0, // funding_rate
];
let expected2 = PriceFeedData {
price_feed_id: PriceFeedId(1),
Expand All @@ -88,6 +134,7 @@ fn price_feed_data_serde() {
price: Some(Price(4.try_into().unwrap())),
best_bid_price: None,
best_ask_price: None,
funding_rate: None,
};
assert_eq!(
bincode::deserialize::<PriceFeedData>(&data2).unwrap(),
Expand Down
35 changes: 26 additions & 9 deletions lazer/sdk/rust/protocol/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ impl Price {
Ok(self.0.get() as f64 / 10i64.checked_pow(exponent).context("overflow")? as f64)
}

pub fn from_f64(value: f64, exponent: u32) -> anyhow::Result<Self> {
let value = (value * 10f64.powi(exponent as i32)) as i64;
let value = NonZeroI64::new(value).context("zero price is unsupported")?;
Ok(Self(value))
}

pub fn mul(self, rhs: Price, rhs_exponent: u32) -> anyhow::Result<Price> {
let left_value = i128::from(self.0.get());
let right_value = i128::from(rhs.0.get());
Expand Down Expand Up @@ -142,6 +148,8 @@ pub enum PriceFeedProperty {
PublisherCount,
Exponent,
Confidence,
FundingRate,
FundingTimestamp,
// More fields may be added later.
}

Expand Down Expand Up @@ -381,13 +389,6 @@ pub struct ParsedPayload {
pub price_feeds: Vec<ParsedFeedPayload>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NatsPayload {
pub payload: ParsedPayload,
pub channel: Channel,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParsedFeedPayload {
Expand All @@ -413,6 +414,12 @@ pub struct ParsedFeedPayload {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub confidence: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub funding_rate: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub funding_timestamp: Option<u64>,
// More fields may be added later.
}

Expand All @@ -431,6 +438,8 @@ impl ParsedFeedPayload {
publisher_count: None,
exponent: None,
confidence: None,
funding_rate: None,
funding_timestamp: None,
};
for &property in properties {
match property {
Expand All @@ -444,14 +453,20 @@ impl ParsedFeedPayload {
output.best_ask_price = data.best_ask_price;
}
PriceFeedProperty::PublisherCount => {
output.publisher_count = data.publisher_count;
output.publisher_count = Some(data.publisher_count);
}
PriceFeedProperty::Exponent => {
output.exponent = exponent;
}
PriceFeedProperty::Confidence => {
output.confidence = data.confidence;
}
PriceFeedProperty::FundingRate => {
output.funding_rate = data.funding_rate;
}
PriceFeedProperty::FundingTimestamp => {
output.funding_timestamp = data.funding_timestamp;
}
}
}
output
Expand All @@ -467,9 +482,11 @@ impl ParsedFeedPayload {
price: data.price,
best_bid_price: data.best_bid_price,
best_ask_price: data.best_ask_price,
publisher_count: data.publisher_count,
publisher_count: Some(data.publisher_count),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need a Some() now if we changed this to no be an Option? I think the parsed payload type needs to also be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the properties in parsedFeedPayload need to be option because it depends on the properties a consumer request.

exponent,
confidence: data.confidence,
funding_rate: data.funding_rate,
funding_timestamp: data.funding_timestamp,
}
}
}
Loading