1use std::collections::HashMap;
32
33use nautilus_core::serialization::{deserialize_decimal, deserialize_optional_decimal};
34use rust_decimal::Decimal;
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37use ustr::Ustr;
38
39use crate::common::{
40 enums::{
41 DeriveAssetType, DeriveInstrumentType, DeriveLiquidityRole, DeriveMarginType,
42 DeriveOptionKind, DeriveOrderCancelReason, DeriveOrderSide, DeriveOrderStatus,
43 DeriveOrderType, DeriveTimeInForce, DeriveTriggerPriceType, DeriveTriggerType,
44 DeriveTxStatus,
45 },
46 parse::{deserialize_derive_decimal, deserialize_optional_derive_decimal},
47};
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct JsonRpcRequest<P> {
54 pub jsonrpc: &'static str,
56 pub id: u64,
58 pub method: &'static str,
60 pub params: P,
62}
63
64impl<P> JsonRpcRequest<P> {
65 #[must_use]
67 pub fn new(id: u64, method: &'static str, params: P) -> Self {
68 Self {
69 jsonrpc: "2.0",
70 id,
71 method,
72 params,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Deserialize)]
80pub struct JsonRpcResponse<R> {
81 #[serde(default, deserialize_with = "deserialize_optional_jsonrpc_id")]
84 pub id: Option<u64>,
85 #[serde(default = "Option::default")]
87 pub result: Option<R>,
88 #[serde(default)]
90 pub error: Option<JsonRpcError>,
91}
92
93fn deserialize_optional_jsonrpc_id<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
94where
95 D: serde::Deserializer<'de>,
96{
97 let value = Option::<Value>::deserialize(deserializer)?;
98 match value {
99 None | Some(Value::Null) => Ok(None),
100 Some(Value::Number(number)) => number
101 .as_u64()
102 .map(Some)
103 .ok_or_else(|| serde::de::Error::custom("JSON-RPC id must be an unsigned integer")),
104 Some(Value::String(value)) => Ok(value.parse::<u64>().ok()),
105 Some(other) => Err(serde::de::Error::custom(format!(
106 "JSON-RPC id must be an unsigned integer or string, was {other}"
107 ))),
108 }
109}
110
111#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
113pub struct JsonRpcError {
114 pub code: i64,
116 pub message: String,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub data: Option<Value>,
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct DeriveOptionPublicDetails {
127 pub expiry: i64,
129 pub index: Ustr,
131 pub option_type: DeriveOptionKind,
133 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
135 pub settlement_price: Option<Decimal>,
136 #[serde(deserialize_with = "deserialize_decimal")]
138 pub strike: Decimal,
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct DerivePerpPublicDetails {
145 #[serde(deserialize_with = "deserialize_decimal")]
147 pub aggregate_funding: Decimal,
148 #[serde(deserialize_with = "deserialize_decimal")]
150 pub funding_rate: Decimal,
151 pub index: Ustr,
153 #[serde(deserialize_with = "deserialize_decimal")]
155 pub max_rate_per_hour: Decimal,
156 #[serde(deserialize_with = "deserialize_decimal")]
158 pub min_rate_per_hour: Decimal,
159 #[serde(deserialize_with = "deserialize_decimal")]
161 pub static_interest_rate: Decimal,
162}
163
164#[derive(Clone, Debug, Serialize, Deserialize)]
166pub struct DeriveInstrument {
167 #[serde(deserialize_with = "deserialize_decimal")]
169 pub amount_step: Decimal,
170 pub base_asset_address: Ustr,
172 pub base_asset_sub_id: Ustr,
174 pub base_currency: Ustr,
176 #[serde(deserialize_with = "deserialize_decimal")]
178 pub base_fee: Decimal,
179 pub instrument_name: Ustr,
181 pub instrument_type: DeriveInstrumentType,
183 pub is_active: bool,
185 #[serde(deserialize_with = "deserialize_decimal")]
187 pub maker_fee_rate: Decimal,
188 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
190 pub mark_price_fee_rate_cap: Option<Decimal>,
191 #[serde(deserialize_with = "deserialize_decimal")]
193 pub maximum_amount: Decimal,
194 #[serde(deserialize_with = "deserialize_decimal")]
196 pub minimum_amount: Decimal,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub option_details: Option<DeriveOptionPublicDetails>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub perp_details: Option<DerivePerpPublicDetails>,
203 pub quote_currency: Ustr,
205 pub scheduled_activation: i64,
207 pub scheduled_deactivation: i64,
209 #[serde(deserialize_with = "deserialize_decimal")]
211 pub taker_fee_rate: Decimal,
212 #[serde(deserialize_with = "deserialize_decimal")]
214 pub tick_size: Decimal,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize)]
219pub struct DeriveAggregateTradingStats {
220 #[serde(alias = "c", deserialize_with = "deserialize_decimal")]
222 pub contract_volume: Decimal,
223 #[serde(alias = "h", deserialize_with = "deserialize_decimal")]
225 pub high: Decimal,
226 #[serde(alias = "l", deserialize_with = "deserialize_decimal")]
228 pub low: Decimal,
229 #[serde(alias = "n", deserialize_with = "deserialize_decimal")]
231 pub num_trades: Decimal,
232 #[serde(alias = "oi", deserialize_with = "deserialize_decimal")]
234 pub open_interest: Decimal,
235 #[serde(alias = "p", deserialize_with = "deserialize_decimal")]
237 pub percent_change: Decimal,
238 #[serde(alias = "pr", deserialize_with = "deserialize_decimal")]
240 pub usd_change: Decimal,
241}
242
243#[derive(Clone, Debug, Serialize, Deserialize)]
245pub struct DeriveOptionPricing {
246 #[serde(alias = "ai", deserialize_with = "deserialize_decimal")]
248 pub ask_iv: Decimal,
249 #[serde(alias = "bi", deserialize_with = "deserialize_decimal")]
251 pub bid_iv: Decimal,
252 #[serde(alias = "d", deserialize_with = "deserialize_decimal")]
254 pub delta: Decimal,
255 #[serde(alias = "f", deserialize_with = "deserialize_decimal")]
257 pub forward_price: Decimal,
258 #[serde(alias = "g", deserialize_with = "deserialize_decimal")]
260 pub gamma: Decimal,
261 #[serde(alias = "i", deserialize_with = "deserialize_decimal")]
263 pub iv: Decimal,
264 #[serde(alias = "m", deserialize_with = "deserialize_decimal")]
266 pub mark_price: Decimal,
267 #[serde(alias = "r", deserialize_with = "deserialize_decimal")]
269 pub rho: Decimal,
270 #[serde(alias = "t", deserialize_with = "deserialize_decimal")]
272 pub theta: Decimal,
273 #[serde(alias = "v", deserialize_with = "deserialize_decimal")]
275 pub vega: Decimal,
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct DeriveTickerSnapshot {
281 #[serde(default)]
283 pub instrument_name: Ustr,
284 #[serde(
286 rename = "A",
287 alias = "best_ask_amount",
288 deserialize_with = "deserialize_decimal"
289 )]
290 pub best_ask_amount: Decimal,
291 #[serde(
293 rename = "a",
294 alias = "best_ask_price",
295 deserialize_with = "deserialize_decimal"
296 )]
297 pub best_ask_price: Decimal,
298 #[serde(
300 rename = "B",
301 alias = "best_bid_amount",
302 deserialize_with = "deserialize_decimal"
303 )]
304 pub best_bid_amount: Decimal,
305 #[serde(
307 rename = "b",
308 alias = "best_bid_price",
309 deserialize_with = "deserialize_decimal"
310 )]
311 pub best_bid_price: Decimal,
312 #[serde(
314 rename = "f",
315 alias = "funding_rate",
316 default,
317 deserialize_with = "deserialize_optional_decimal"
318 )]
319 pub funding_rate: Option<Decimal>,
320 #[serde(
322 rename = "I",
323 alias = "index_price",
324 deserialize_with = "deserialize_decimal"
325 )]
326 pub index_price: Decimal,
327 #[serde(
329 rename = "M",
330 alias = "mark_price",
331 deserialize_with = "deserialize_decimal"
332 )]
333 pub mark_price: Decimal,
334 #[serde(
336 rename = "maxp",
337 alias = "max_price",
338 deserialize_with = "deserialize_decimal"
339 )]
340 pub max_price: Decimal,
341 #[serde(
343 rename = "minp",
344 alias = "min_price",
345 deserialize_with = "deserialize_decimal"
346 )]
347 pub min_price: Decimal,
348 #[serde(default)]
350 pub option_pricing: Option<DeriveOptionPricing>,
351 #[serde(default)]
353 pub stats: Option<DeriveAggregateTradingStats>,
354 #[serde(rename = "t", alias = "timestamp")]
356 pub timestamp: i64,
357}
358
359#[derive(Clone, Debug, Serialize, Deserialize)]
361pub struct DeriveTickersResult {
362 pub tickers: HashMap<String, DeriveTickerSnapshot>,
364}
365
366#[derive(Clone, Debug, Serialize, Deserialize)]
369pub struct DeriveTicker {
370 #[serde(deserialize_with = "deserialize_decimal")]
372 pub amount_step: Decimal,
373 pub base_asset_address: Ustr,
375 pub base_asset_sub_id: Ustr,
377 pub base_currency: Ustr,
379 #[serde(deserialize_with = "deserialize_decimal")]
381 pub base_fee: Decimal,
382 #[serde(deserialize_with = "deserialize_decimal")]
384 pub best_ask_amount: Decimal,
385 #[serde(deserialize_with = "deserialize_decimal")]
387 pub best_ask_price: Decimal,
388 #[serde(deserialize_with = "deserialize_decimal")]
390 pub best_bid_amount: Decimal,
391 #[serde(deserialize_with = "deserialize_decimal")]
393 pub best_bid_price: Decimal,
394 #[serde(deserialize_with = "deserialize_decimal")]
396 pub index_price: Decimal,
397 pub instrument_name: Ustr,
399 pub instrument_type: DeriveInstrumentType,
401 pub is_active: bool,
403 #[serde(deserialize_with = "deserialize_decimal")]
405 pub maker_fee_rate: Decimal,
406 #[serde(deserialize_with = "deserialize_decimal")]
408 pub mark_price: Decimal,
409 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
411 pub mark_price_fee_rate_cap: Option<Decimal>,
412 #[serde(deserialize_with = "deserialize_decimal")]
414 pub max_price: Decimal,
415 #[serde(deserialize_with = "deserialize_decimal")]
417 pub maximum_amount: Decimal,
418 #[serde(deserialize_with = "deserialize_decimal")]
420 pub min_price: Decimal,
421 #[serde(deserialize_with = "deserialize_decimal")]
423 pub minimum_amount: Decimal,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub option_details: Option<DeriveOptionPublicDetails>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub option_pricing: Option<DeriveOptionPricing>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
432 pub perp_details: Option<DerivePerpPublicDetails>,
433 pub quote_currency: Ustr,
435 pub scheduled_activation: i64,
437 pub scheduled_deactivation: i64,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub stats: Option<DeriveAggregateTradingStats>,
442 #[serde(deserialize_with = "deserialize_decimal")]
444 pub taker_fee_rate: Decimal,
445 #[serde(deserialize_with = "deserialize_decimal")]
447 pub tick_size: Decimal,
448 pub timestamp: i64,
450}
451
452#[derive(Clone, Debug, Serialize, Deserialize)]
455pub struct DeriveOrder {
456 #[serde(deserialize_with = "deserialize_decimal")]
458 pub amount: Decimal,
459 #[serde(deserialize_with = "deserialize_decimal")]
461 pub average_price: Decimal,
462 pub cancel_reason: DeriveOrderCancelReason,
464 pub creation_timestamp: i64,
466 pub direction: DeriveOrderSide,
468 #[serde(deserialize_with = "deserialize_decimal")]
470 pub filled_amount: Decimal,
471 pub instrument_name: Ustr,
473 pub is_transfer: bool,
475 pub label: Ustr,
477 pub last_update_timestamp: i64,
479 #[serde(deserialize_with = "deserialize_decimal")]
481 pub limit_price: Decimal,
482 #[serde(deserialize_with = "deserialize_decimal")]
484 pub max_fee: Decimal,
485 pub mmp: bool,
487 pub nonce: i64,
489 #[serde(deserialize_with = "deserialize_decimal")]
491 pub order_fee: Decimal,
492 pub order_id: String,
494 pub order_status: DeriveOrderStatus,
496 pub order_type: DeriveOrderType,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub quote_id: Option<String>,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
503 pub replaced_order_id: Option<String>,
504 pub signature: String,
506 pub signature_expiry_sec: i64,
508 pub signer: Ustr,
510 pub subaccount_id: i64,
512 pub time_in_force: DeriveTimeInForce,
514 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
516 pub trigger_price: Option<Decimal>,
517 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub trigger_price_type: Option<DeriveTriggerPriceType>,
520 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub trigger_reject_message: Option<String>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub trigger_type: Option<DeriveTriggerType>,
526}
527
528#[derive(Clone, Debug, Serialize, Deserialize)]
530pub struct DeriveOrderResult {
531 pub order: DeriveOrder,
533 #[serde(default)]
535 pub trades: Vec<DeriveTrade>,
536}
537
538#[derive(Clone, Debug, Serialize, Deserialize)]
540pub struct DeriveReplaceResult {
541 pub order: DeriveOrder,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub cancelled_order: Option<DeriveOrder>,
546}
547
548#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
550pub struct DeriveEmptyResult {}
551
552impl<'de> Deserialize<'de> for DeriveEmptyResult {
553 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
554 where
555 D: serde::Deserializer<'de>,
556 {
557 match Value::deserialize(deserializer)? {
558 Value::Null | Value::Object(_) => Ok(Self {}),
559 Value::String(value) if value == "ok" => Ok(Self {}),
560 other => Err(serde::de::Error::custom(format!(
561 "empty Derive result must be an object, null, or \"ok\", was {other}"
562 ))),
563 }
564 }
565}
566
567#[derive(Clone, Debug, Serialize, Deserialize)]
570pub struct DerivePosition {
571 #[serde(deserialize_with = "deserialize_derive_decimal")]
573 pub amount: Decimal,
574 #[serde(deserialize_with = "deserialize_derive_decimal")]
576 pub average_price: Decimal,
577 pub creation_timestamp: i64,
579 #[serde(deserialize_with = "deserialize_derive_decimal")]
581 pub cumulative_funding: Decimal,
582 #[serde(deserialize_with = "deserialize_derive_decimal")]
584 pub delta: Decimal,
585 #[serde(deserialize_with = "deserialize_derive_decimal")]
587 pub gamma: Decimal,
588 #[serde(deserialize_with = "deserialize_derive_decimal")]
590 pub index_price: Decimal,
591 #[serde(deserialize_with = "deserialize_derive_decimal")]
593 pub initial_margin: Decimal,
594 pub instrument_name: Ustr,
596 pub instrument_type: DeriveInstrumentType,
598 #[serde(default, deserialize_with = "deserialize_optional_derive_decimal")]
600 pub leverage: Option<Decimal>,
601 #[serde(default, deserialize_with = "deserialize_optional_derive_decimal")]
603 pub liquidation_price: Option<Decimal>,
604 #[serde(deserialize_with = "deserialize_derive_decimal")]
606 pub maintenance_margin: Decimal,
607 #[serde(deserialize_with = "deserialize_derive_decimal")]
609 pub mark_price: Decimal,
610 #[serde(deserialize_with = "deserialize_derive_decimal")]
612 pub mark_value: Decimal,
613 #[serde(deserialize_with = "deserialize_derive_decimal")]
615 pub net_settlements: Decimal,
616 #[serde(deserialize_with = "deserialize_derive_decimal")]
618 pub open_orders_margin: Decimal,
619 #[serde(deserialize_with = "deserialize_derive_decimal")]
621 pub pending_funding: Decimal,
622 #[serde(deserialize_with = "deserialize_derive_decimal")]
624 pub realized_pnl: Decimal,
625 #[serde(deserialize_with = "deserialize_derive_decimal")]
627 pub theta: Decimal,
628 #[serde(deserialize_with = "deserialize_derive_decimal")]
630 pub unrealized_pnl: Decimal,
631 #[serde(deserialize_with = "deserialize_derive_decimal")]
633 pub vega: Decimal,
634}
635
636#[derive(Clone, Debug, Serialize, Deserialize)]
638pub struct DeriveCollateral {
639 #[serde(deserialize_with = "deserialize_derive_decimal")]
641 pub amount: Decimal,
642 pub asset_name: Ustr,
644 pub asset_type: DeriveAssetType,
646 #[serde(deserialize_with = "deserialize_derive_decimal")]
648 pub cumulative_interest: Decimal,
649 pub currency: Ustr,
651 #[serde(deserialize_with = "deserialize_derive_decimal")]
653 pub initial_margin: Decimal,
654 #[serde(deserialize_with = "deserialize_derive_decimal")]
656 pub maintenance_margin: Decimal,
657 #[serde(deserialize_with = "deserialize_derive_decimal")]
659 pub mark_price: Decimal,
660 #[serde(deserialize_with = "deserialize_derive_decimal")]
662 pub mark_value: Decimal,
663 #[serde(deserialize_with = "deserialize_derive_decimal")]
665 pub pending_interest: Decimal,
666}
667
668#[derive(Clone, Debug, Serialize, Deserialize)]
670pub struct DeriveSubaccount {
671 pub collaterals: Vec<DeriveCollateral>,
673 #[serde(deserialize_with = "deserialize_derive_decimal")]
675 pub collaterals_initial_margin: Decimal,
676 #[serde(deserialize_with = "deserialize_derive_decimal")]
678 pub collaterals_maintenance_margin: Decimal,
679 #[serde(deserialize_with = "deserialize_derive_decimal")]
681 pub collaterals_value: Decimal,
682 pub currency: Ustr,
684 #[serde(deserialize_with = "deserialize_derive_decimal")]
686 pub initial_margin: Decimal,
687 pub is_under_liquidation: bool,
689 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub label: Option<String>,
692 #[serde(deserialize_with = "deserialize_derive_decimal")]
694 pub maintenance_margin: Decimal,
695 pub margin_type: DeriveMarginType,
697 pub open_orders: Vec<DeriveOrder>,
699 #[serde(deserialize_with = "deserialize_derive_decimal")]
701 pub open_orders_margin: Decimal,
702 pub positions: Vec<DerivePosition>,
704 #[serde(deserialize_with = "deserialize_derive_decimal")]
706 pub positions_initial_margin: Decimal,
707 #[serde(deserialize_with = "deserialize_derive_decimal")]
709 pub positions_maintenance_margin: Decimal,
710 #[serde(deserialize_with = "deserialize_derive_decimal")]
712 pub positions_value: Decimal,
713 pub subaccount_id: i64,
715 #[serde(deserialize_with = "deserialize_derive_decimal")]
717 pub subaccount_value: Decimal,
718}
719
720#[derive(Clone, Debug, Serialize, Deserialize)]
723pub struct DeriveTrade {
724 pub direction: DeriveOrderSide,
726 #[serde(deserialize_with = "deserialize_decimal")]
728 pub index_price: Decimal,
729 pub instrument_name: Ustr,
731 pub is_transfer: bool,
733 pub label: Ustr,
735 pub liquidity_role: DeriveLiquidityRole,
737 #[serde(deserialize_with = "deserialize_decimal")]
739 pub mark_price: Decimal,
740 pub order_id: String,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
744 pub quote_id: Option<String>,
745 #[serde(deserialize_with = "deserialize_decimal")]
747 pub realized_pnl: Decimal,
748 pub subaccount_id: i64,
750 pub timestamp: i64,
752 #[serde(deserialize_with = "deserialize_decimal")]
754 pub trade_amount: Decimal,
755 #[serde(deserialize_with = "deserialize_decimal")]
757 pub trade_fee: Decimal,
758 pub trade_id: String,
760 #[serde(deserialize_with = "deserialize_decimal")]
762 pub trade_price: Decimal,
763 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub tx_hash: Option<String>,
766 pub tx_status: DeriveTxStatus,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub wallet: Option<Ustr>,
771}
772
773#[derive(Clone, Debug, Serialize, Deserialize)]
782pub struct DerivePublicTrade {
783 pub direction: DeriveOrderSide,
785 #[serde(deserialize_with = "deserialize_decimal")]
787 pub index_price: Decimal,
788 pub instrument_name: Ustr,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
793 pub liquidity_role: Option<DeriveLiquidityRole>,
794 #[serde(deserialize_with = "deserialize_decimal")]
796 pub mark_price: Decimal,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub quote_id: Option<String>,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
802 pub rfq_id: Option<String>,
803 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
805 pub realized_pnl: Option<Decimal>,
806 #[serde(default, skip_serializing_if = "Option::is_none")]
808 pub subaccount_id: Option<i64>,
809 pub timestamp: i64,
811 #[serde(deserialize_with = "deserialize_decimal")]
813 pub trade_amount: Decimal,
814 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
816 pub trade_fee: Option<Decimal>,
817 pub trade_id: String,
819 #[serde(deserialize_with = "deserialize_decimal")]
821 pub trade_price: Decimal,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
824 pub tx_hash: Option<String>,
825 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub tx_status: Option<DeriveTxStatus>,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub wallet: Option<Ustr>,
831}
832
833#[derive(Clone, Debug, Serialize, Deserialize)]
835pub struct DerivePaginationInfo {
836 pub count: i64,
838 pub num_pages: i64,
840}
841
842#[derive(Clone, Debug, Serialize, Deserialize)]
844pub struct DeriveOrdersResult {
845 pub orders: Vec<DeriveOrder>,
847 pub pagination: DerivePaginationInfo,
849 pub subaccount_id: i64,
851}
852
853#[derive(Clone, Debug, Serialize, Deserialize)]
855pub struct DeriveOpenOrdersResult {
856 pub orders: Vec<DeriveOrder>,
858 pub subaccount_id: i64,
860}
861
862#[derive(Clone, Debug, Serialize, Deserialize)]
864pub struct DeriveTradesResult {
865 pub trades: Vec<DeriveTrade>,
867 pub pagination: DerivePaginationInfo,
869 pub subaccount_id: i64,
871}
872
873#[derive(Clone, Debug, Serialize, Deserialize)]
875pub struct DerivePublicTradesResult {
876 pub trades: Vec<DerivePublicTrade>,
878 pub pagination: DerivePaginationInfo,
880}
881
882#[derive(Clone, Debug, Serialize, Deserialize)]
889pub struct DerivePublicCandle {
890 #[serde(deserialize_with = "deserialize_decimal")]
892 pub open_price: Decimal,
893 #[serde(deserialize_with = "deserialize_decimal")]
895 pub high_price: Decimal,
896 #[serde(deserialize_with = "deserialize_decimal")]
898 pub low_price: Decimal,
899 #[serde(deserialize_with = "deserialize_decimal")]
901 pub close_price: Decimal,
902 #[serde(deserialize_with = "deserialize_decimal")]
904 pub volume_usd: Decimal,
905 #[serde(deserialize_with = "deserialize_decimal")]
907 pub volume_contracts: Decimal,
908 pub timestamp: i64,
910 pub timestamp_bucket: i64,
912}
913
914#[derive(Clone, Debug, Serialize, Deserialize)]
916pub struct DerivePublicFundingRate {
917 #[serde(deserialize_with = "deserialize_decimal")]
919 pub funding_rate: Decimal,
920 pub timestamp: i64,
922}
923
924#[derive(Clone, Debug, Serialize, Deserialize)]
926pub struct DerivePublicFundingRateHistoryResult {
927 pub funding_rate_history: Vec<DerivePublicFundingRate>,
929}
930
931#[derive(Clone, Debug, Serialize, Deserialize)]
933pub struct DerivePositionsResult {
934 pub positions: Vec<DerivePosition>,
936 pub subaccount_id: i64,
938}
939
940#[cfg(test)]
941mod tests {
942 use std::path::PathBuf;
943
944 use rstest::rstest;
945 use serde_json::{Value, json};
946
947 use super::*;
948
949 fn data_path() -> PathBuf {
950 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data")
951 }
952
953 fn load_json(filename: &str) -> Value {
954 let content = std::fs::read_to_string(data_path().join(filename))
955 .unwrap_or_else(|_| panic!("failed to read {filename}"));
956 serde_json::from_str(&content).expect("invalid json")
957 }
958
959 #[rstest]
960 fn test_request_serializes_with_jsonrpc_version_tag() {
961 let req = JsonRpcRequest::new(7, "public/get_instruments", json!({"currency": "ETH"}));
962 let wire = serde_json::to_value(&req).unwrap();
963 assert_eq!(wire["jsonrpc"], "2.0");
964 assert_eq!(wire["id"], 7);
965 assert_eq!(wire["method"], "public/get_instruments");
966 assert_eq!(wire["params"]["currency"], "ETH");
967 }
968
969 #[rstest]
970 fn test_response_decodes_success_envelope() {
971 let body = json!({"id": 1, "result": {"instruments": []}});
972 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
973 assert_eq!(resp.id, Some(1));
974 assert!(resp.error.is_none());
975 assert!(resp.result.is_some());
976 }
977
978 #[rstest]
979 fn test_response_decodes_error_envelope() {
980 let body = json!({
981 "id": 9,
982 "error": {"code": -32600, "message": "Invalid Request"}
983 });
984 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
985 assert_eq!(resp.id, Some(9));
986 assert!(resp.result.is_none());
987 let err = resp.error.expect("error present");
988 assert_eq!(err.code, -32600);
989 assert_eq!(err.message, "Invalid Request");
990 assert!(err.data.is_none());
991 }
992
993 #[rstest]
994 fn test_response_decodes_error_envelope_with_data_field() {
995 let body = json!({
996 "id": 9,
997 "error": {
998 "code": -32602,
999 "message": "Invalid params",
1000 "data": {"field": "currency"},
1001 }
1002 });
1003 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1004 let err = resp.error.expect("error present");
1005 assert_eq!(err.data, Some(json!({"field": "currency"})));
1006 }
1007
1008 #[rstest]
1009 fn test_response_tolerates_missing_id() {
1010 let body = json!({"result": {"ok": true}});
1011 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1012 assert!(resp.id.is_none());
1013 assert!(resp.result.is_some());
1014 }
1015
1016 #[rstest]
1017 fn test_response_tolerates_string_id() {
1018 let body = json!({"id": "e3c970c6-94aa-420c-b6db-d0f585a7fde9", "result": {"ok": true}});
1019 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1020 assert!(resp.id.is_none());
1021 assert!(resp.result.is_some());
1022 }
1023
1024 #[rstest]
1025 fn test_response_decodes_numeric_string_id() {
1026 let body = json!({"id": "42", "result": {"ok": true}});
1027 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1028 assert_eq!(resp.id, Some(42));
1029 assert!(resp.result.is_some());
1030 }
1031
1032 #[rstest]
1033 fn test_instrument_decodes_perp_with_perp_details() {
1034 let body = load_json("perps/instrument_eth.json");
1035 let instrument: DeriveInstrument = serde_json::from_value(body).unwrap();
1036 assert_eq!(instrument.instrument_name.as_str(), "ETH-PERP");
1037 assert_eq!(instrument.instrument_type, DeriveInstrumentType::Perp);
1038 assert!(instrument.option_details.is_none());
1039 let perp = instrument.perp_details.expect("perp details present");
1040 assert_eq!(perp.index, "ETH-USD");
1041 }
1042
1043 #[rstest]
1044 fn test_instrument_decodes_option_with_option_details() {
1045 let mut body = load_json("options/instrument_eth.json");
1046 body["scheduled_activation"] = json!(0);
1047 let instrument: DeriveInstrument = serde_json::from_value(body).unwrap();
1048 let option = instrument.option_details.expect("option details present");
1049 assert_eq!(option.option_type, DeriveOptionKind::Call);
1050 assert_eq!(option.strike.to_string(), "3500");
1051 assert!(option.settlement_price.is_none());
1052 }
1053
1054 #[rstest]
1055 fn test_order_decodes_partially_filled_market_order() {
1056 let body = load_json("perps/http_order_eth_partially_filled.json");
1060 let order: DeriveOrder = serde_json::from_value(body).unwrap();
1061 assert_eq!(order.amount.to_string(), "2.0");
1062 assert_eq!(order.filled_amount.to_string(), "1.5");
1063 assert_eq!(order.average_price.to_string(), "3500.25");
1064 assert_eq!(order.order_status, DeriveOrderStatus::Filled);
1065 assert_eq!(order.cancel_reason, DeriveOrderCancelReason::Empty);
1066 assert_eq!(order.direction, DeriveOrderSide::Buy);
1067 assert_eq!(order.time_in_force, DeriveTimeInForce::Ioc);
1068 assert_eq!(order.order_type, DeriveOrderType::Market);
1069 assert_eq!(order.instrument_name.as_str(), "ETH-PERP");
1070 assert_eq!(order.label.as_str(), "alpha-strategy");
1071 assert_eq!(order.signer.as_str(), "0xsigner");
1072 assert_eq!(order.order_id, "abc-123");
1073 assert_eq!(order.subaccount_id, 42);
1074 assert_eq!(order.signature_expiry_sec, 1_700_001_000);
1075 assert!(!order.mmp);
1076 assert!(!order.is_transfer);
1077 assert!(order.quote_id.is_none());
1078 assert!(order.replaced_order_id.is_none());
1079 }
1080
1081 #[rstest]
1082 fn test_position_decodes_perp_with_optional_leverage() {
1083 let body = load_json("perps/http_position_eth.json");
1085 let position: DerivePosition = serde_json::from_value(body).unwrap();
1086 assert_eq!(position.instrument_type, DeriveInstrumentType::Perp);
1087 assert_eq!(position.instrument_name.as_str(), "ETH-PERP");
1088 assert_eq!(position.amount.to_string(), "-2");
1089 assert_eq!(position.delta.to_string(), "-2");
1090 assert_eq!(position.gamma.to_string(), "0.1");
1091 assert_eq!(position.theta.to_string(), "-0.3");
1092 assert_eq!(position.vega.to_string(), "0.5");
1093 assert_eq!(position.unrealized_pnl.to_string(), "8");
1094 assert_eq!(position.mark_value.to_string(), "-7008");
1095 assert_eq!(
1096 position.leverage.as_ref().map(ToString::to_string),
1097 Some("5.0".into()),
1098 );
1099 assert_eq!(
1100 position.liquidation_price.as_ref().map(ToString::to_string),
1101 Some("4200".into()),
1102 );
1103 }
1104
1105 #[rstest]
1106 fn test_subaccount_decodes_with_collaterals_and_open_orders() {
1107 let body = load_json("common/http_subaccount_usdc.json");
1108 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1109 assert_eq!(subaccount.subaccount_id, 42);
1110 assert_eq!(subaccount.margin_type, DeriveMarginType::Pm);
1111 assert_eq!(subaccount.collaterals.len(), 1);
1112 assert_eq!(subaccount.collaterals[0].asset_type, DeriveAssetType::Erc20);
1113 assert!(!subaccount.is_under_liquidation);
1114 }
1115
1116 #[rstest]
1117 fn test_subaccount_decodes_high_scale_decimal_values() {
1118 let body = load_json("common/http_subaccount_high_scale.json");
1119 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1120 let position = &subaccount.positions[0];
1121
1122 assert_eq!(
1123 subaccount.initial_margin.to_string(),
1124 "0.1234567890123456789012345679",
1125 );
1126 assert_eq!(
1127 subaccount.collaterals[0].amount.to_string(),
1128 "0.1234567890123456789012345679",
1129 );
1130 assert_eq!(
1131 position.pending_funding.to_string(),
1132 "0.1234567890123456789012345679",
1133 );
1134 assert_eq!(
1135 position.leverage.as_ref().map(ToString::to_string),
1136 Some("5.1234567890123456789012345679".into()),
1137 );
1138 assert_eq!(
1139 position.liquidation_price.as_ref().map(ToString::to_string),
1140 Some("4200.1234567890123456789012346".into()),
1141 );
1142 }
1143
1144 #[rstest]
1145 fn test_public_trade_round_trips() {
1146 let body = load_json("perps/http_public_trade_eth_sell.json");
1147 let trade: DerivePublicTrade = serde_json::from_value(body).unwrap();
1148 assert_eq!(trade.direction, DeriveOrderSide::Sell);
1149 assert_eq!(trade.tx_status, Some(DeriveTxStatus::Settled));
1150 let reserialized = serde_json::to_value(&trade).unwrap();
1151 assert_eq!(reserialized["instrument_name"], "ETH-PERP");
1152 assert_eq!(reserialized["liquidity_role"], "taker");
1153 }
1154
1155 #[rstest]
1156 fn test_orders_result_envelope_decodes() {
1157 let body = json!({
1158 "orders": [],
1159 "pagination": {"count": 0, "num_pages": 0},
1160 "subaccount_id": 42,
1161 });
1162 let result: DeriveOrdersResult = serde_json::from_value(body).unwrap();
1163 assert!(result.orders.is_empty());
1164 assert_eq!(result.subaccount_id, 42);
1165 assert_eq!(result.pagination.count, 0);
1166 }
1167
1168 fn perp_ticker_json() -> Value {
1169 load_json("perps/http_ticker_eth_snapshot.json")
1170 }
1171
1172 #[rstest]
1173 fn test_ticker_decodes_perp_snapshot() {
1174 let ticker: DeriveTicker = serde_json::from_value(perp_ticker_json()).unwrap();
1175 assert_eq!(ticker.instrument_name.as_str(), "ETH-PERP");
1176 assert_eq!(ticker.instrument_type, DeriveInstrumentType::Perp);
1177 assert_eq!(ticker.mark_price.to_string(), "3500.5");
1178 assert_eq!(ticker.best_bid_price.to_string(), "3499.5");
1179 assert_eq!(ticker.best_ask_price.to_string(), "3501.0");
1180 assert_eq!(ticker.timestamp, 1_700_000_000_000);
1181 assert!(ticker.option_details.is_none());
1182 assert!(ticker.option_pricing.is_none());
1183 let perp = ticker.perp_details.expect("perp details present");
1184 assert_eq!(perp.index.as_str(), "ETH-USD");
1185 assert_eq!(perp.funding_rate.to_string(), "0.0002");
1186 let stats = ticker
1187 .stats
1188 .as_ref()
1189 .expect("WS ticker fixture includes stats");
1190 assert_eq!(stats.contract_volume.to_string(), "12345.6");
1191 assert_eq!(stats.high.to_string(), "3600");
1192 assert_eq!(stats.num_trades.to_string(), "789");
1193 }
1194
1195 #[rstest]
1196 fn test_ticker_decodes_option_snapshot_with_greeks() {
1197 let body = load_json("options/http_ticker_eth_snapshot.json");
1198 let ticker: DeriveTicker = serde_json::from_value(body).unwrap();
1199 assert_eq!(ticker.instrument_type, DeriveInstrumentType::Option);
1200 assert!(ticker.perp_details.is_none());
1201 let option = ticker.option_details.expect("option details present");
1202 assert_eq!(option.option_type, DeriveOptionKind::Call);
1203 assert_eq!(option.strike.to_string(), "3500");
1204 assert!(option.settlement_price.is_none());
1205 let greeks = ticker.option_pricing.expect("option pricing present");
1206 assert_eq!(greeks.delta.to_string(), "0.55");
1207 assert_eq!(greeks.gamma.to_string(), "0.0008");
1208 assert_eq!(greeks.theta.to_string(), "-2.1");
1209 assert_eq!(greeks.vega.to_string(), "4.5");
1210 assert_eq!(greeks.iv.to_string(), "0.60");
1211 assert_eq!(greeks.forward_price.to_string(), "3505");
1212 }
1213
1214 #[rstest]
1215 fn test_private_trade_decodes_with_order_link() {
1216 let body = load_json("perps/http_private_trade_eth.json");
1220 let trade: DeriveTrade = serde_json::from_value(body).unwrap();
1221 assert_eq!(trade.direction, DeriveOrderSide::Buy);
1222 assert_eq!(trade.liquidity_role, DeriveLiquidityRole::Maker);
1223 assert_eq!(trade.tx_status, DeriveTxStatus::Settled);
1224 assert_eq!(trade.instrument_name.as_str(), "ETH-PERP");
1225 assert_eq!(trade.label.as_str(), "alpha-strategy");
1226 assert_eq!(trade.wallet.as_ref().map(Ustr::as_str), Some("0xwallet"));
1227 assert_eq!(trade.order_id, "order-abc");
1228 assert_eq!(trade.trade_id, "trade-xyz");
1229 assert_eq!(trade.subaccount_id, 42);
1230 assert_eq!(trade.realized_pnl.to_string(), "12.5");
1231 assert_eq!(trade.trade_amount.to_string(), "0.5");
1232 assert_eq!(trade.trade_price.to_string(), "3499.0");
1233 assert!(!trade.is_transfer);
1234 assert!(trade.quote_id.is_none());
1235 assert_eq!(trade.tx_hash.as_deref(), Some("0xhash"));
1236 }
1237
1238 #[rstest]
1239 fn test_order_result_decodes_pending_trade_with_null_tx_hash() {
1240 let mut body = load_json("spot/http_submit_order_response_mainnet.json");
1241 let mut trade = load_json("perps/http_private_trade_eth.json");
1242 trade["tx_hash"] = Value::Null;
1243 trade["tx_status"] = json!("requested");
1244 trade.as_object_mut().unwrap().remove("wallet");
1245 body["result"]["trades"] = json!([trade]);
1246
1247 let result: DeriveOrderResult =
1248 serde_json::from_value(body["result"].clone()).expect("result decodes");
1249
1250 assert_eq!(result.trades.len(), 1);
1251 assert!(result.trades[0].tx_hash.is_none());
1252 assert_eq!(result.trades[0].tx_status, DeriveTxStatus::Requested);
1253 assert!(result.trades[0].wallet.is_none());
1254 }
1255
1256 #[rstest]
1257 fn test_empty_result_decodes_cancel_ack_shapes() {
1258 let object: DeriveEmptyResult = serde_json::from_value(json!({})).unwrap();
1259 let ok_string: DeriveEmptyResult = serde_json::from_value(json!("ok")).unwrap();
1260 let null_value: DeriveEmptyResult = serde_json::from_value(Value::Null).unwrap();
1261
1262 assert_eq!(object, DeriveEmptyResult {});
1263 assert_eq!(ok_string, DeriveEmptyResult {});
1264 assert_eq!(null_value, DeriveEmptyResult {});
1265 }
1266
1267 #[rstest]
1268 fn test_trades_result_envelope_decodes() {
1269 let body = load_json("perps/http_trades_result_eth.json");
1270 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1271 assert_eq!(result.trades.len(), 1);
1272 assert_eq!(result.subaccount_id, 7);
1273 assert_eq!(result.pagination.count, 1);
1274 assert_eq!(result.pagination.num_pages, 1);
1275 assert_eq!(result.trades[0].trade_id, "t-1");
1276 }
1277
1278 #[rstest]
1279 fn test_public_trades_result_envelope_decodes() {
1280 let body = load_json("perps/http_public_trades_result_eth.json");
1281 let result: DerivePublicTradesResult = serde_json::from_value(body).unwrap();
1282 assert_eq!(result.trades.len(), 1);
1283 assert_eq!(result.pagination.count, 1);
1284 assert_eq!(result.trades[0].trade_id, "pub-1");
1285 }
1286
1287 #[rstest]
1288 fn test_public_funding_rate_history_result_envelope_decodes() {
1289 let body = load_json("perps/http_public_funding_rate_history_eth.json");
1290 let result: DerivePublicFundingRateHistoryResult = serde_json::from_value(body).unwrap();
1291 assert_eq!(result.funding_rate_history.len(), 3);
1292 let first = &result.funding_rate_history[0];
1293 assert_eq!(first.funding_rate.to_string(), "0.00012");
1294 assert_eq!(first.timestamp, 1_700_000_000_000);
1295 assert_eq!(
1296 result.funding_rate_history.last().unwrap().timestamp,
1297 1_700_007_200_000,
1298 );
1299 }
1300
1301 #[rstest]
1302 fn test_public_candles_decode_array() {
1303 let body = load_json("perps/http_public_candles_eth.json");
1307 let candles: Vec<DerivePublicCandle> = serde_json::from_value(body).unwrap();
1308 assert_eq!(candles.len(), 3);
1309 let first = &candles[0];
1310 assert_eq!(first.open_price.to_string(), "3500.0");
1311 assert_eq!(first.high_price.to_string(), "3501.5");
1312 assert_eq!(first.low_price.to_string(), "3499.0");
1313 assert_eq!(first.close_price.to_string(), "3501.0");
1314 assert_eq!(first.volume_usd.to_string(), "12345.6");
1315 assert_eq!(first.volume_contracts.to_string(), "3.527");
1316 assert_eq!(first.timestamp, 1_700_000_007);
1319 assert_eq!(first.timestamp_bucket, 1_700_000_000);
1320 assert_eq!(candles.last().unwrap().timestamp_bucket, 1_700_001_800);
1321 }
1322
1323 #[rstest]
1324 fn test_positions_result_envelope_decodes() {
1325 let body = load_json("perps/http_positions_result_eth.json");
1326 let result: DerivePositionsResult = serde_json::from_value(body).unwrap();
1327 assert_eq!(result.positions.len(), 1);
1328 assert_eq!(result.subaccount_id, 42);
1329 assert_eq!(result.positions[0].instrument_name.as_str(), "ETH-PERP");
1330 assert!(result.positions[0].leverage.is_none());
1331 assert!(result.positions[0].liquidation_price.is_none());
1332 }
1333}