Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/derive-maker-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
39 changes: 32 additions & 7 deletions exchange/src/options/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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, DeriveError> {
Self::with_base_url(PRODUCTION_BASE_URL, proxy)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -329,7 +333,7 @@ struct TradeDto {
trade_amount: String,
mark_price: String,
index_price: String,
tx_status: String,
batch_status: Option<String>,
}

impl TradeDto {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions exchange/tests/fixtures/derive_option_history.json
Original file line number Diff line number Diff line change
@@ -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}}}
}