-
Notifications
You must be signed in to change notification settings - Fork 301
feat(pyth-lazer-sdk): add funding rate #2423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
07181fd
7ef7b5d
06d1158
d29a68b
b55cf8e
1707f8c
bac763b
a0f999b
13f6207
bc9f03a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>), | ||
} | ||
|
||
#[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; | ||
|
@@ -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(), | ||
}) | ||
|
@@ -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)?; | ||
|
@@ -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)?; | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -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"); | ||
}; | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()); | ||
|
@@ -142,6 +148,8 @@ pub enum PriceFeedProperty { | |
PublisherCount, | ||
Exponent, | ||
Confidence, | ||
FundingRate, | ||
FundingTimestamp, | ||
// More fields may be added later. | ||
} | ||
|
||
|
@@ -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 { | ||
|
@@ -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. | ||
} | ||
|
||
|
@@ -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 { | ||
|
@@ -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 | ||
|
@@ -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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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 toPrice
instead of using rawi64
. We can also useTimestampUs
for the timestamp here.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.