Skip to content

Commit

Permalink
feat(pyth-lazer-sdk): add funding rate (#2423)
Browse files Browse the repository at this point in the history
* add funding rate and refactor publisher count

* handle old price feed data

* feat: add funding timestamp

* feat: add from_f64 method for Price conversion

* feat: update PriceFeedData structure and version to 0.6.0

* feat: refactor PriceFeedData serialization tests and update funding rate handling

* feat: update funding rate and timestamp types to use Rate and TimestampUs

* feat: remove precision check for price value in Rate conversion

* feat: update pyth-lazer-protocol version to 0.6.1
  • Loading branch information
keyvankhademi authored Feb 27, 2025
1 parent 27938e7 commit 6d6a06f
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 30 deletions.
4 changes: 2 additions & 2 deletions lazer/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lazer/sdk/rust/protocol/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-lazer-protocol"
version = "0.6.0"
version = "0.6.1"
edition = "2021"
description = "Pyth Lazer SDK - protocol types."
license = "Apache-2.0"
Expand Down
86 changes: 75 additions & 11 deletions lazer/sdk/rust/protocol/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use {
super::router::{PriceFeedId, PriceFeedProperty, TimestampUs},
crate::router::{ChannelId, Price},
crate::router::{ChannelId, Price, Rate},
anyhow::bail,
byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt, BE, LE},
serde::{Deserialize, Serialize},
Expand Down 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<Rate>),
FundingTimestamp(Option<TimestampUs>),
}

#[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<Rate>,
pub funding_timestamp: Option<TimestampUs>,
}

/// First bytes of a payload's encoding
Expand Down Expand Up @@ -84,6 +88,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 @@ -115,7 +125,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 @@ -125,6 +135,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_rate::<BO>(&mut writer, *rate)?;
}
PayloadPropertyValue::FundingTimestamp(timestamp) => {
writer.write_u8(PriceFeedProperty::FundingTimestamp as u8)?;
write_option_timestamp::<BO>(&mut writer, *timestamp)?;
}
}
}
}
Expand Down Expand Up @@ -164,11 +182,17 @@ 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_rate::<BO>(&mut reader)?)
} else if property == PriceFeedProperty::FundingTimestamp as u8 {
PayloadPropertyValue::FundingTimestamp(read_option_timestamp::<BO>(
&mut reader,
)?)
} else {
bail!("unknown property");
};
Expand Down Expand Up @@ -196,14 +220,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_rate<BO: ByteOrder>(
mut writer: impl Write,
value: Option<Rate>,
) -> std::io::Result<()> {
match value {
Some(value) => {
writer.write_u8(1)?;
writer.write_i64::<BO>(value.0)
}
None => {
writer.write_u8(0)?;
Ok(())
}
}
}

fn read_option_rate<BO: ByteOrder>(mut reader: impl Read) -> std::io::Result<Option<Rate>> {
let present = reader.read_u8()? != 0;
if present {
Ok(Some(Rate(reader.read_i64::<BO>()?)))
} else {
Ok(None)
}
}

fn write_option_timestamp<BO: ByteOrder>(
mut writer: impl Write,
value: Option<u16>,
value: Option<TimestampUs>,
) -> std::io::Result<()> {
writer.write_u16::<BO>(value.unwrap_or(0))
match value {
Some(value) => {
writer.write_u8(1)?;
writer.write_u64::<BO>(value.0)
}
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_timestamp<BO: ByteOrder>(
mut reader: impl Read,
) -> std::io::Result<Option<TimestampUs>> {
let present = reader.read_u8()? != 0;
if present {
Ok(Some(TimestampUs(reader.read_u64::<BO>()?)))
} else {
Ok(None)
}
}
106 changes: 99 additions & 7 deletions lazer/sdk/rust/protocol/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! eliminating WebSocket overhead.
use {
super::router::{Price, PriceFeedId, TimestampUs},
super::router::{Price, PriceFeedId, Rate, TimestampUs},
derive_more::derive::From,
serde::{Deserialize, Serialize},
};
Expand All @@ -12,7 +12,33 @@ use {
/// from the publisher to the router.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceFeedData {
pub struct PriceFeedDataV2 {
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.
pub price: Option<Price>,
/// Last known value of the best bid price of this price feed.
/// `None` if no value is currently available.
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.
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<Rate>,
}

/// 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 PriceFeedDataV1 {
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`.
Expand All @@ -33,6 +59,20 @@ pub struct PriceFeedData {
pub best_ask_price: Option<Price>,
}

impl From<PriceFeedDataV1> for PriceFeedDataV2 {
fn from(v0: PriceFeedDataV1) -> 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.
/// Currently only serde errors are reported back to the client.
#[derive(Debug, Clone, Serialize, Deserialize, From)]
Expand All @@ -49,7 +89,7 @@ pub struct UpdateDeserializationErrorResponse {
}

#[test]
fn price_feed_data_serde() {
fn price_feed_data_v1_serde() {
let data = [
1, 0, 0, 0, // price_feed_id
2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us
Expand All @@ -59,7 +99,7 @@ fn price_feed_data_serde() {
6, 2, 0, 0, 0, 0, 0, 0, // best_ask_price
];

let expected = PriceFeedData {
let expected = PriceFeedDataV1 {
price_feed_id: PriceFeedId(1),
source_timestamp_us: TimestampUs(2),
publisher_timestamp_us: TimestampUs(3),
Expand All @@ -68,7 +108,7 @@ fn price_feed_data_serde() {
best_ask_price: Some(Price((2 * 256 + 6).try_into().unwrap())),
};
assert_eq!(
bincode::deserialize::<PriceFeedData>(&data).unwrap(),
bincode::deserialize::<PriceFeedDataV1>(&data).unwrap(),
expected
);
assert_eq!(bincode::serialize(&expected).unwrap(), data);
Expand All @@ -81,16 +121,68 @@ fn price_feed_data_serde() {
0, 0, 0, 0, 0, 0, 0, 0, // best_bid_price
0, 0, 0, 0, 0, 0, 0, 0, // best_ask_price
];
let expected2 = PriceFeedData {
let expected2 = PriceFeedDataV1 {
price_feed_id: PriceFeedId(1),
source_timestamp_us: TimestampUs(2),
publisher_timestamp_us: TimestampUs(3),
price: Some(Price(4.try_into().unwrap())),
best_bid_price: None,
best_ask_price: None,
};
assert_eq!(
bincode::deserialize::<PriceFeedDataV1>(&data2).unwrap(),
expected2
);
assert_eq!(bincode::serialize(&expected2).unwrap(), data2);
}

#[test]
fn price_feed_data_v2_serde() {
let data = [
1, 0, 0, 0, // price_feed_id
2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us
3, 0, 0, 0, 0, 0, 0, 0, // publisher_timestamp_us
1, 4, 0, 0, 0, 0, 0, 0, 0, // price
1, 5, 0, 0, 0, 0, 0, 0, 0, // best_bid_price
1, 6, 2, 0, 0, 0, 0, 0, 0, // best_ask_price
0, // funding_rate
];

let expected = PriceFeedDataV2 {
price_feed_id: PriceFeedId(1),
source_timestamp_us: TimestampUs(2),
publisher_timestamp_us: TimestampUs(3),
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: None,
};
assert_eq!(
bincode::deserialize::<PriceFeedDataV2>(&data).unwrap(),
expected
);
assert_eq!(bincode::serialize(&expected).unwrap(), data);

let data2 = [
1, 0, 0, 0, // price_feed_id
2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us
3, 0, 0, 0, 0, 0, 0, 0, // publisher_timestamp_us
1, 4, 0, 0, 0, 0, 0, 0, 0, // price
0, // best_bid_price
0, // best_ask_price
1, 7, 3, 0, 0, 0, 0, 0, 0, // funding_rate
];
let expected2 = PriceFeedDataV2 {
price_feed_id: PriceFeedId(1),
source_timestamp_us: TimestampUs(2),
publisher_timestamp_us: TimestampUs(3),
price: Some(Price(4.try_into().unwrap())),
best_bid_price: None,
best_ask_price: None,
funding_rate: Some(Rate(3 * 256 + 7)),
};
assert_eq!(
bincode::deserialize::<PriceFeedData>(&data2).unwrap(),
bincode::deserialize::<PriceFeedDataV2>(&data2).unwrap(),
expected2
);
assert_eq!(bincode::serialize(&expected2).unwrap(), data2);
Expand Down
Loading

0 comments on commit 6d6a06f

Please sign in to comment.