From ce4276455d2f15cc77ca2b3914f1a0c772a523cc Mon Sep 17 00:00:00 2001 From: Erik Wennberg <227982975+skatasweden@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:26:23 +0100 Subject: [PATCH 1/2] Port Derive maker-flow client to the v3 API Derive has moved its public API to v3, and the v2 gateway at api.lyra.finance now answers public/get_all_instruments and public/get_trade_history with HTTP 503, so the Derive maker-flow rows in the GEX view can no longer load data. Point the client at https://api.derive.xyz/v3 and follow the v3 rename of the trade settlement field from `tx_status` ("settled") to `batch_status` ("Settled"). Changing the host alone is not enough: v3 trade rows no longer carry `tx_status`, so the required `TradeDto` field makes every trade-history page fail to deserialize, and v3 ignores the old `tx_status` request filter. Instrument payloads, maker trade_id uniqueness and the persisted DeriveMakerTrade model are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WXidSXAuYV4R2ebJ1TzFxp --- docs/derive-maker-flow.md | 2 +- exchange/src/options/derive.rs | 12 ++++++------ exchange/tests/fixtures/derive_option_history.json | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) 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..763640ab 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); @@ -154,7 +154,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 +166,7 @@ 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 != "Settled" || item.liquidity_role != "maker" { continue; } let Some(trade) = item.into_model(instrument) else { @@ -306,7 +306,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 +329,7 @@ struct TradeDto { trade_amount: String, mark_price: String, index_price: String, - tx_status: String, + batch_status: String, } impl TradeDto { @@ -426,7 +426,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}}} } From 3f6f41679deec41f6a265559cab5339665646724 Mon Sep 17 00:00:00 2001 From: Niketion Date: Tue, 3 Nov 2026 17:41:34 +0100 Subject: [PATCH 2/2] fix(derive): skip trades with missing or null batch status --- exchange/src/options/derive.rs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/exchange/src/options/derive.rs b/exchange/src/options/derive.rs index 763640ab..835d291b 100644 --- a/exchange/src/options/derive.rs +++ b/exchange/src/options/derive.rs @@ -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, @@ -166,7 +169,8 @@ impl DeriveOptionsClient { let Some(instrument) = by_name.get(item.instrument_name.as_str()) else { continue; }; - if item.batch_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 { @@ -329,7 +333,7 @@ struct TradeDto { trade_amount: String, mark_price: String, index_price: String, - batch_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(),