diff --git a/docs/derive-maker-flow.md b/docs/derive-maker-flow.md index edcbbbba..6ff9da87 100644 --- a/docs/derive-maker-flow.md +++ b/docs/derive-maker-flow.md @@ -8,7 +8,7 @@ The GEX integration keeps structural exposure and observed flow separate: - **Derive is an observed-flow source.** Only settled option trades where Derive reports the participant as maker are retained. - **The datasets are never summed.** Derive flow does not alter Deribit GEX, walls, flip, zones, projection, or monitor data, and Derive is not exposed as a chartable exchange adapter. -The Derive client uses REST JSON-RPC at `https://api.lyra.finance`: +The Derive client uses REST JSON-RPC at `https://api.derive.xyz/v3`: - `public/get_all_instruments` - `public/get_trade_history` diff --git a/exchange/src/options/derive.rs b/exchange/src/options/derive.rs index d6be4b24..835d291b 100644 --- a/exchange/src/options/derive.rs +++ b/exchange/src/options/derive.rs @@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet}; use std::time::Duration; use thiserror::Error; -const PRODUCTION_BASE_URL: &str = "https://api.lyra.finance"; +const PRODUCTION_BASE_URL: &str = "https://api.derive.xyz/v3"; const PAGE_SIZE: u16 = 1_000; const HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); @@ -73,6 +73,7 @@ pub struct DeriveOptionsClient { } impl DeriveOptionsClient { + /// Creates a client for the Derive v3 production API using the optional proxy. pub fn new(proxy: Option<&adapter::Proxy>) -> Result { Self::with_base_url(PRODUCTION_BASE_URL, proxy) } @@ -130,6 +131,8 @@ impl DeriveOptionsClient { Ok(result) } + /// Fetches and deduplicates settled maker option trades within the inclusive time window. + /// Trades with missing, null, or non-settled batch status are skipped. pub async fn fetch_trade_history( &self, underlying: OptionsUnderlying, @@ -154,7 +157,7 @@ impl DeriveOptionsClient { let params = TradeHistoryParams { instrument_type: "option", currency: underlying.as_str(), - tx_status: "settled", + batch_status: "Settled", from_timestamp: from_timestamp.as_u64(), to_timestamp: to_timestamp.as_u64(), page, @@ -166,7 +169,8 @@ impl DeriveOptionsClient { let Some(instrument) = by_name.get(item.instrument_name.as_str()) else { continue; }; - if item.tx_status != "settled" || item.liquidity_role != "maker" { + if item.batch_status.as_deref() != Some("Settled") || item.liquidity_role != "maker" + { continue; } let Some(trade) = item.into_model(instrument) else { @@ -306,7 +310,7 @@ impl InstrumentDto { struct TradeHistoryParams<'a> { instrument_type: &'static str, currency: &'a str, - tx_status: &'static str, + batch_status: &'static str, from_timestamp: u64, to_timestamp: u64, page: u32, @@ -329,7 +333,7 @@ struct TradeDto { trade_amount: String, mark_price: String, index_price: String, - tx_status: String, + batch_status: Option, } impl TradeDto { @@ -394,11 +398,32 @@ mod tests { assert_eq!(model.expiration_timestamp, UnixMs::new(1_909_008_000_000)); } + /// Verifies pagination, deduplication, and rejection of non-settled or unknown-status trades. #[tokio::test] async fn paginates_and_keeps_only_unique_settled_maker_trades() { let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("listener"); let address = listener.local_addr().expect("address"); - let fixture = fixture(); + let mut fixture = fixture(); + // Unknown statuses must not prevent valid trades on the same page from loading. + for (id, status) in [ + ("trade-null-status", Some(Value::Null)), + ("trade-missing-status", None), + ] { + let mut trade = fixture["trades_page_1"]["result"]["trades"][0].clone(); + trade["trade_id"] = Value::String(id.to_owned()); + if let Some(status) = status { + trade["batch_status"] = status; + } else { + trade + .as_object_mut() + .expect("trade object") + .remove("batch_status"); + } + fixture["trades_page_1"]["result"]["trades"] + .as_array_mut() + .expect("trades") + .push(trade); + } let pages = [ fixture["instruments_page_1"].to_string(), fixture["instruments_page_2"].to_string(), @@ -426,7 +451,7 @@ mod tests { assert!(request_body.contains("\"instrument_type\":\"option\"")); assert!(request_body.contains("\"currency\":\"BTC\"")); if index >= 2 { - assert!(request_body.contains("\"tx_status\":\"settled\"")); + assert!(request_body.contains("\"batch_status\":\"Settled\"")); assert!(request_body.contains("\"from_timestamp\":1800000000000")); assert!(request_body.contains("\"to_timestamp\":1800000010000")); } else { diff --git a/exchange/tests/fixtures/derive_option_history.json b/exchange/tests/fixtures/derive_option_history.json index 9f69d103..e44d775e 100644 --- a/exchange/tests/fixtures/derive_option_history.json +++ b/exchange/tests/fixtures/derive_option_history.json @@ -1,6 +1,6 @@ { "instruments_page_1": {"id":"flowsurface","result":{"instruments":[{"instrument_name":"BTC-30JUN30-100000-C","instrument_type":"option","base_currency":"BTC","is_active":true,"option_details":{"expiry":1909008000,"strike":"100000.005","option_type":"C"}}],"pagination":{"count":2,"num_pages":2}}}, "instruments_page_2": {"id":"flowsurface","result":{"instruments":[{"instrument_name":"BTC-30JUN30-90000-P","instrument_type":"option","base_currency":"BTC","is_active":true,"option_details":{"expiry":1909008000,"strike":"90000","option_type":"P"}},{"instrument_name":"ETH-30JUN30-5000-C","instrument_type":"option","base_currency":"ETH","is_active":true,"option_details":{"expiry":1909008000,"strike":"5000","option_type":"C"}}],"pagination":{"count":3,"num_pages":2}}}, - "trades_page_1": {"id":"flowsurface","result":{"trades":[{"trade_id":"trade-maker-buy","instrument_name":"BTC-30JUN30-100000-C","timestamp":1800000001000,"direction":"buy","liquidity_role":"maker","trade_amount":"2.5","mark_price":"0.12","index_price":"101000","tx_status":"settled","wallet":"0xnot-persisted","subaccount_id":42,"realized_pnl":"1","trade_fee":"0.1","tx_hash":"0xnot-persisted"},{"trade_id":"trade-taker","instrument_name":"BTC-30JUN30-100000-C","timestamp":1800000002000,"direction":"sell","liquidity_role":"taker","trade_amount":"1","mark_price":"0.12","index_price":"101000","tx_status":"settled"}],"pagination":{"count":4,"num_pages":2}}}, - "trades_page_2": {"id":"flowsurface","result":{"trades":[{"trade_id":"trade-maker-buy","instrument_name":"BTC-30JUN30-100000-C","timestamp":1800000001000,"direction":"buy","liquidity_role":"maker","trade_amount":"2.5","mark_price":"0.12","index_price":"101000","tx_status":"settled"},{"trade_id":"trade-reverted","instrument_name":"BTC-30JUN30-90000-P","timestamp":1800000003000,"direction":"sell","liquidity_role":"maker","trade_amount":"3","mark_price":"0.08","index_price":"101000","tx_status":"reverted"},{"trade_id":"trade-maker-sell","instrument_name":"BTC-30JUN30-90000-P","timestamp":1800000004000,"direction":"sell","liquidity_role":"maker","trade_amount":"3","mark_price":"0.08","index_price":"101000","tx_status":"settled"}],"pagination":{"count":5,"num_pages":2}}} + "trades_page_1": {"id":"flowsurface","result":{"trades":[{"trade_id":"trade-maker-buy","instrument_name":"BTC-30JUN30-100000-C","timestamp":1800000001000,"direction":"buy","liquidity_role":"maker","trade_amount":"2.5","mark_price":"0.12","index_price":"101000","batch_status":"Settled","wallet":"0xnot-persisted","subaccount_id":42,"realized_pnl":"1","trade_fee":"0.1","tx_hash":"0xnot-persisted"},{"trade_id":"trade-taker","instrument_name":"BTC-30JUN30-100000-C","timestamp":1800000002000,"direction":"sell","liquidity_role":"taker","trade_amount":"1","mark_price":"0.12","index_price":"101000","batch_status":"Settled"}],"pagination":{"count":4,"num_pages":2}}}, + "trades_page_2": {"id":"flowsurface","result":{"trades":[{"trade_id":"trade-maker-buy","instrument_name":"BTC-30JUN30-100000-C","timestamp":1800000001000,"direction":"buy","liquidity_role":"maker","trade_amount":"2.5","mark_price":"0.12","index_price":"101000","batch_status":"Settled"},{"trade_id":"trade-reverted","instrument_name":"BTC-30JUN30-90000-P","timestamp":1800000003000,"direction":"sell","liquidity_role":"maker","trade_amount":"3","mark_price":"0.08","index_price":"101000","batch_status":"Batching"},{"trade_id":"trade-maker-sell","instrument_name":"BTC-30JUN30-90000-P","timestamp":1800000004000,"direction":"sell","liquidity_role":"maker","trade_amount":"3","mark_price":"0.08","index_price":"101000","batch_status":"Settled"}],"pagination":{"count":5,"num_pages":2}}} }