use crate::data::{BasisPoint, FundingRatePoint, FundingSettlementPoint, MarkPricePoint, IndexPricePoint};
use crate::series::DataPoint;
use crate::series::SeriesKey;
use crate::subscription::{Event, Stream};
pub(crate) trait DerivedStream: Send + 'static {
type Output: DataPoint;
fn deps() -> &'static [Stream];
fn new_for_key(key: &SeriesKey) -> Self;
fn on_upstream_event(&mut self, ev: &Event, dep_idx: usize) -> Option<Self::Output>;
}
pub(crate) struct BasisDerived {
last_mark: Option<(i64, f64)>,
last_index: Option<(i64, f64)>,
max_skew_ms: i64,
}
impl DerivedStream for BasisDerived {
type Output = BasisPoint;
fn deps() -> &'static [Stream] {
&[Stream::MarkPrice, Stream::IndexPrice]
}
fn new_for_key(_key: &SeriesKey) -> Self {
Self {
last_mark: None,
last_index: None,
max_skew_ms: 2_000,
}
}
fn on_upstream_event(&mut self, ev: &Event, dep_idx: usize) -> Option<BasisPoint> {
match dep_idx {
0 => {
if let Event::MarkPrice { point, .. } = ev {
self.last_mark = Some((point.ts_ms, point.mark));
}
}
1 => {
if let Event::IndexPrice { point, .. } = ev {
self.last_index = Some((point.ts_ms, point.price));
}
}
_ => return None,
}
let (mark_ts, mark) = self.last_mark?;
let (idx_ts, idx) = self.last_index?;
if (mark_ts - idx_ts).abs() > self.max_skew_ms {
return None;
}
let now_ms = mark_ts.max(idx_ts);
Some(BasisPoint {
ts_ms: now_ms,
value: mark - idx,
mark,
index: idx,
})
}
}
pub(crate) struct FundingSettlementDerived {
last_next_funding_time: i64,
last_rate: f64,
}
impl DerivedStream for FundingSettlementDerived {
type Output = FundingSettlementPoint;
fn deps() -> &'static [Stream] {
&[Stream::FundingRate]
}
fn new_for_key(_key: &SeriesKey) -> Self {
Self {
last_next_funding_time: 0,
last_rate: 0.0,
}
}
fn on_upstream_event(&mut self, ev: &Event, _dep_idx: usize) -> Option<FundingSettlementPoint> {
let Event::FundingRate { point, .. } = ev else { return None };
let new_nft = point.next_funding_time_ms;
let new_rate = point.rate;
let now_ms = point.ts_ms;
if new_nft == 0 {
self.last_rate = new_rate;
return None;
}
if self.last_next_funding_time == 0 {
self.last_next_funding_time = new_nft;
self.last_rate = new_rate;
return None;
}
let output = if now_ms >= self.last_next_funding_time
&& new_nft != self.last_next_funding_time
{
Some(FundingSettlementPoint {
ts_ms: now_ms,
settled_rate: self.last_rate,
settlement_time: self.last_next_funding_time,
})
} else {
None
};
self.last_next_funding_time = new_nft;
self.last_rate = new_rate;
output
}
}
const _: fn() = || {
let _ = std::mem::size_of::<MarkPricePoint>();
let _ = std::mem::size_of::<IndexPricePoint>();
let _ = std::mem::size_of::<FundingRatePoint>();
};
#[cfg(test)]
mod tests {
use super::*;
use digdigdig3::core::types::ExchangeId;
use digdigdig3::core::types::AccountType;
fn mark_price_event(ts_ms: i64, mark: f64) -> Event {
Event::MarkPrice {
exchange: ExchangeId::Binance,
symbol: "BTCUSDT".to_string(),
point: MarkPricePoint { ts_ms, mark, index: f64::NAN },
}
}
fn index_price_event(ts_ms: i64, price: f64) -> Event {
Event::IndexPrice {
exchange: ExchangeId::Binance,
symbol: "BTCUSDT".to_string(),
point: IndexPricePoint { ts_ms, price },
}
}
fn funding_rate_event(ts_ms: i64, rate: f64, next_funding_time_ms: i64) -> Event {
Event::FundingRate {
exchange: ExchangeId::Binance,
symbol: "BTCUSDT".to_string(),
point: FundingRatePoint { ts_ms, rate, next_funding_time_ms },
}
}
fn test_key() -> SeriesKey {
SeriesKey::new(ExchangeId::Binance, AccountType::FuturesCross, "BTCUSDT", crate::series::Kind::Basis)
}
#[test]
fn basis_no_emit_until_both_sides_seen() {
let mut d = BasisDerived::new_for_key(&test_key());
let r = d.on_upstream_event(&mark_price_event(1000, 50_000.0), 0);
assert!(r.is_none(), "MarkPrice alone must not emit");
let mut d2 = BasisDerived::new_for_key(&test_key());
let r2 = d2.on_upstream_event(&index_price_event(1000, 49_990.0), 1);
assert!(r2.is_none(), "IndexPrice alone must not emit");
}
#[test]
fn basis_emits_on_paired_events() {
let mut d = BasisDerived::new_for_key(&test_key());
let r1 = d.on_upstream_event(&mark_price_event(1000, 50_000.0), 0);
assert!(r1.is_none());
let r2 = d.on_upstream_event(&index_price_event(1200, 49_990.0), 1);
let p = r2.expect("should emit after both sides seen");
assert!((p.value - 10.0).abs() < 1e-9, "value = mark - index = 10.0");
assert_eq!(p.mark, 50_000.0);
assert_eq!(p.index, 49_990.0);
assert_eq!(p.ts_ms, 1200); }
#[test]
fn basis_skew_rejection() {
let mut d = BasisDerived::new_for_key(&test_key());
d.on_upstream_event(&mark_price_event(0, 50_000.0), 0);
let r = d.on_upstream_event(&index_price_event(3000, 49_990.0), 1);
assert!(r.is_none(), "stale pair must be rejected (skew > 2000 ms)");
}
#[test]
fn basis_emits_on_each_update_once_seeded() {
let mut d = BasisDerived::new_for_key(&test_key());
d.on_upstream_event(&mark_price_event(1000, 50_000.0), 0);
d.on_upstream_event(&index_price_event(1001, 49_990.0), 1);
let r = d.on_upstream_event(&mark_price_event(1002, 50_010.0), 0);
let p = r.expect("should emit after update when both seeded");
assert!((p.value - 20.0).abs() < 1e-9, "updated mark=50010, index=49990 → 20.0");
}
#[test]
fn basis_value_correct() {
let mut d = BasisDerived::new_for_key(&test_key());
d.on_upstream_event(&mark_price_event(100, 50_000.0), 0);
let p = d.on_upstream_event(&index_price_event(100, 49_990.0), 1).unwrap();
assert!((p.value - 10.0).abs() < 1e-9);
assert_eq!(p.mark, 50_000.0);
assert_eq!(p.index, 49_990.0);
}
fn fs_key() -> SeriesKey {
SeriesKey::new(ExchangeId::Binance, AccountType::FuturesCross, "BTCUSDT", crate::series::Kind::FundingSettlement)
}
#[test]
fn settlement_no_emit_on_first_event() {
let mut d = FundingSettlementDerived::new_for_key(&fs_key());
let r = d.on_upstream_event(&funding_rate_event(500, 0.0001, 1000), 0);
assert!(r.is_none(), "first event must only initialize state");
}
#[test]
fn settlement_no_emit_if_nft_unchanged() {
let mut d = FundingSettlementDerived::new_for_key(&fs_key());
d.on_upstream_event(&funding_rate_event(500, 0.0001, 1000), 0);
let r = d.on_upstream_event(&funding_rate_event(800, 0.0001, 1000), 0);
assert!(r.is_none(), "no crossing: nft unchanged and ts < nft");
}
#[test]
fn settlement_emit_on_crossing() {
let mut d = FundingSettlementDerived::new_for_key(&fs_key());
d.on_upstream_event(&funding_rate_event(500, 0.0001, 1000), 0);
let r = d.on_upstream_event(&funding_rate_event(1001, 0.0002, 2000), 0);
let p = r.expect("must emit on crossing");
assert_eq!(p.ts_ms, 1001);
assert!((p.settled_rate - 0.0001).abs() < 1e-12, "settled_rate must be from PREVIOUS event");
assert_eq!(p.settlement_time, 1000);
}
#[test]
fn settlement_no_emit_when_nft_zero() {
let mut d = FundingSettlementDerived::new_for_key(&fs_key());
let r = d.on_upstream_event(&funding_rate_event(1000, 0.0001, 0), 0);
assert!(r.is_none(), "nft=0 must be silently absorbed");
}
#[test]
fn settlement_rate_is_from_previous_event() {
let mut d = FundingSettlementDerived::new_for_key(&fs_key());
d.on_upstream_event(&funding_rate_event(500, 0.05, 1000), 0);
let p = d.on_upstream_event(&funding_rate_event(1001, 0.03, 2000), 0).unwrap();
assert!((p.settled_rate - 0.05).abs() < 1e-12, "settled_rate must be 0.05 (from prior event), not 0.03");
}
}