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_salvaged_vec,
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)]
80#[serde(bound(deserialize = "R: Deserialize<'de>"))]
81pub struct JsonRpcResponse<R> {
82 #[serde(default, deserialize_with = "deserialize_optional_jsonrpc_id")]
85 pub id: Option<u64>,
86 #[serde(default, deserialize_with = "deserialize_present_jsonrpc_result")]
88 pub result: Option<R>,
89 #[serde(default)]
91 pub error: Option<JsonRpcError>,
92}
93
94fn deserialize_present_jsonrpc_result<'de, D, R>(deserializer: D) -> Result<Option<R>, D::Error>
95where
96 D: serde::Deserializer<'de>,
97 R: Deserialize<'de>,
98{
99 R::deserialize(deserializer).map(Some)
100}
101
102fn deserialize_optional_jsonrpc_id<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
103where
104 D: serde::Deserializer<'de>,
105{
106 let value = Option::<Value>::deserialize(deserializer)?;
107 match value {
108 None | Some(Value::Null) => Ok(None),
109 Some(Value::Number(number)) => number
110 .as_u64()
111 .map(Some)
112 .ok_or_else(|| serde::de::Error::custom("JSON-RPC id must be an unsigned integer")),
113 Some(Value::String(value)) => Ok(value.parse::<u64>().ok()),
114 Some(other) => Err(serde::de::Error::custom(format!(
115 "JSON-RPC id must be an unsigned integer or string, was {other}"
116 ))),
117 }
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
122pub struct JsonRpcError {
123 pub code: i64,
125 pub message: String,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub data: Option<Value>,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct DeriveOptionPublicDetails {
136 pub expiry: i64,
138 pub index: Ustr,
140 pub option_type: DeriveOptionKind,
142 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
144 pub settlement_price: Option<Decimal>,
145 #[serde(deserialize_with = "deserialize_decimal")]
147 pub strike: Decimal,
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize)]
153pub struct DerivePerpPublicDetails {
154 #[serde(deserialize_with = "deserialize_decimal")]
156 pub aggregate_funding: Decimal,
157 #[serde(deserialize_with = "deserialize_decimal")]
159 pub funding_rate: Decimal,
160 pub index: Ustr,
162 #[serde(deserialize_with = "deserialize_decimal")]
164 pub max_rate_per_hour: Decimal,
165 #[serde(deserialize_with = "deserialize_decimal")]
167 pub min_rate_per_hour: Decimal,
168 #[serde(deserialize_with = "deserialize_decimal")]
170 pub static_interest_rate: Decimal,
171}
172
173#[derive(Clone, Debug, Serialize, Deserialize)]
175pub struct DeriveInstrument {
176 #[serde(deserialize_with = "deserialize_decimal")]
178 pub amount_step: Decimal,
179 pub base_asset_address: Ustr,
181 pub base_asset_sub_id: Ustr,
183 pub base_currency: Ustr,
185 #[serde(deserialize_with = "deserialize_decimal")]
187 pub base_fee: Decimal,
188 pub instrument_name: Ustr,
190 pub instrument_type: DeriveInstrumentType,
192 pub is_active: bool,
194 #[serde(deserialize_with = "deserialize_decimal")]
196 pub maker_fee_rate: Decimal,
197 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
199 pub mark_price_fee_rate_cap: Option<Decimal>,
200 #[serde(deserialize_with = "deserialize_decimal")]
202 pub maximum_amount: Decimal,
203 #[serde(deserialize_with = "deserialize_decimal")]
205 pub minimum_amount: Decimal,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub option_details: Option<DeriveOptionPublicDetails>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub perp_details: Option<DerivePerpPublicDetails>,
212 pub quote_currency: Ustr,
214 pub scheduled_activation: i64,
216 pub scheduled_deactivation: i64,
218 #[serde(deserialize_with = "deserialize_decimal")]
220 pub taker_fee_rate: Decimal,
221 #[serde(deserialize_with = "deserialize_decimal")]
223 pub tick_size: Decimal,
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct DeriveAggregateTradingStats {
229 #[serde(alias = "c", deserialize_with = "deserialize_decimal")]
231 pub contract_volume: Decimal,
232 #[serde(alias = "h", deserialize_with = "deserialize_decimal")]
234 pub high: Decimal,
235 #[serde(alias = "l", deserialize_with = "deserialize_decimal")]
237 pub low: Decimal,
238 #[serde(alias = "n", deserialize_with = "deserialize_decimal")]
240 pub num_trades: Decimal,
241 #[serde(alias = "oi", deserialize_with = "deserialize_decimal")]
243 pub open_interest: Decimal,
244 #[serde(alias = "p", deserialize_with = "deserialize_decimal")]
246 pub percent_change: Decimal,
247 #[serde(alias = "pr", deserialize_with = "deserialize_decimal")]
249 pub usd_change: Decimal,
250}
251
252#[derive(Clone, Debug, Serialize, Deserialize)]
254pub struct DeriveOptionPricing {
255 #[serde(alias = "ai", deserialize_with = "deserialize_decimal")]
257 pub ask_iv: Decimal,
258 #[serde(alias = "bi", deserialize_with = "deserialize_decimal")]
260 pub bid_iv: Decimal,
261 #[serde(alias = "d", deserialize_with = "deserialize_decimal")]
263 pub delta: Decimal,
264 #[serde(alias = "f", deserialize_with = "deserialize_decimal")]
266 pub forward_price: Decimal,
267 #[serde(alias = "g", deserialize_with = "deserialize_decimal")]
269 pub gamma: Decimal,
270 #[serde(alias = "i", deserialize_with = "deserialize_decimal")]
272 pub iv: Decimal,
273 #[serde(alias = "m", deserialize_with = "deserialize_decimal")]
275 pub mark_price: Decimal,
276 #[serde(alias = "r", deserialize_with = "deserialize_decimal")]
278 pub rho: Decimal,
279 #[serde(alias = "t", deserialize_with = "deserialize_decimal")]
281 pub theta: Decimal,
282 #[serde(alias = "v", deserialize_with = "deserialize_decimal")]
284 pub vega: Decimal,
285}
286
287#[derive(Clone, Debug, Serialize, Deserialize)]
289pub struct DeriveTickerSnapshot {
290 #[serde(default)]
292 pub instrument_name: Ustr,
293 #[serde(
295 rename = "A",
296 alias = "best_ask_amount",
297 deserialize_with = "deserialize_decimal"
298 )]
299 pub best_ask_amount: Decimal,
300 #[serde(
302 rename = "a",
303 alias = "best_ask_price",
304 deserialize_with = "deserialize_decimal"
305 )]
306 pub best_ask_price: Decimal,
307 #[serde(
309 rename = "B",
310 alias = "best_bid_amount",
311 deserialize_with = "deserialize_decimal"
312 )]
313 pub best_bid_amount: Decimal,
314 #[serde(
316 rename = "b",
317 alias = "best_bid_price",
318 deserialize_with = "deserialize_decimal"
319 )]
320 pub best_bid_price: Decimal,
321 #[serde(
323 rename = "f",
324 alias = "funding_rate",
325 default,
326 deserialize_with = "deserialize_optional_decimal"
327 )]
328 pub funding_rate: Option<Decimal>,
329 #[serde(
331 rename = "I",
332 alias = "index_price",
333 deserialize_with = "deserialize_decimal"
334 )]
335 pub index_price: Decimal,
336 #[serde(
338 rename = "M",
339 alias = "mark_price",
340 deserialize_with = "deserialize_decimal"
341 )]
342 pub mark_price: Decimal,
343 #[serde(
345 rename = "maxp",
346 alias = "max_price",
347 deserialize_with = "deserialize_decimal"
348 )]
349 pub max_price: Decimal,
350 #[serde(
352 rename = "minp",
353 alias = "min_price",
354 deserialize_with = "deserialize_decimal"
355 )]
356 pub min_price: Decimal,
357 #[serde(default)]
359 pub option_pricing: Option<DeriveOptionPricing>,
360 #[serde(default)]
362 pub stats: Option<DeriveAggregateTradingStats>,
363 #[serde(rename = "t", alias = "timestamp")]
365 pub timestamp: i64,
366}
367
368#[derive(Clone, Debug, Serialize, Deserialize)]
370pub struct DeriveTickersResult {
371 pub tickers: HashMap<String, DeriveTickerSnapshot>,
373}
374
375#[derive(Clone, Debug, Serialize, Deserialize)]
378pub struct DeriveTicker {
379 #[serde(deserialize_with = "deserialize_decimal")]
381 pub amount_step: Decimal,
382 pub base_asset_address: Ustr,
384 pub base_asset_sub_id: Ustr,
386 pub base_currency: Ustr,
388 #[serde(deserialize_with = "deserialize_decimal")]
390 pub base_fee: Decimal,
391 #[serde(deserialize_with = "deserialize_decimal")]
393 pub best_ask_amount: Decimal,
394 #[serde(deserialize_with = "deserialize_decimal")]
396 pub best_ask_price: Decimal,
397 #[serde(deserialize_with = "deserialize_decimal")]
399 pub best_bid_amount: Decimal,
400 #[serde(deserialize_with = "deserialize_decimal")]
402 pub best_bid_price: Decimal,
403 #[serde(deserialize_with = "deserialize_decimal")]
405 pub index_price: Decimal,
406 pub instrument_name: Ustr,
408 pub instrument_type: DeriveInstrumentType,
410 pub is_active: bool,
412 #[serde(deserialize_with = "deserialize_decimal")]
414 pub maker_fee_rate: Decimal,
415 #[serde(deserialize_with = "deserialize_decimal")]
417 pub mark_price: Decimal,
418 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
420 pub mark_price_fee_rate_cap: Option<Decimal>,
421 #[serde(deserialize_with = "deserialize_decimal")]
423 pub max_price: Decimal,
424 #[serde(deserialize_with = "deserialize_decimal")]
426 pub maximum_amount: Decimal,
427 #[serde(deserialize_with = "deserialize_decimal")]
429 pub min_price: Decimal,
430 #[serde(deserialize_with = "deserialize_decimal")]
432 pub minimum_amount: Decimal,
433 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub option_details: Option<DeriveOptionPublicDetails>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub option_pricing: Option<DeriveOptionPricing>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub perp_details: Option<DerivePerpPublicDetails>,
442 pub quote_currency: Ustr,
444 pub scheduled_activation: i64,
446 pub scheduled_deactivation: i64,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub stats: Option<DeriveAggregateTradingStats>,
451 #[serde(deserialize_with = "deserialize_decimal")]
453 pub taker_fee_rate: Decimal,
454 #[serde(deserialize_with = "deserialize_decimal")]
456 pub tick_size: Decimal,
457 pub timestamp: i64,
459}
460
461#[derive(Clone, Debug, Serialize, Deserialize)]
464pub struct DeriveOrder {
465 #[serde(deserialize_with = "deserialize_decimal")]
467 pub amount: Decimal,
468 #[serde(deserialize_with = "deserialize_decimal")]
470 pub average_price: Decimal,
471 pub cancel_reason: DeriveOrderCancelReason,
473 pub creation_timestamp: i64,
475 pub direction: DeriveOrderSide,
477 #[serde(deserialize_with = "deserialize_decimal")]
479 pub filled_amount: Decimal,
480 pub instrument_name: Ustr,
482 pub is_transfer: bool,
484 pub label: Ustr,
486 pub last_update_timestamp: i64,
488 #[serde(deserialize_with = "deserialize_decimal")]
490 pub limit_price: Decimal,
491 #[serde(deserialize_with = "deserialize_decimal")]
493 pub max_fee: Decimal,
494 pub mmp: bool,
496 pub nonce: i64,
498 #[serde(deserialize_with = "deserialize_decimal")]
500 pub order_fee: Decimal,
501 pub order_id: String,
503 pub order_status: DeriveOrderStatus,
505 pub order_type: DeriveOrderType,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
509 pub quote_id: Option<String>,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub replaced_order_id: Option<String>,
513 pub signature: String,
515 pub signature_expiry_sec: i64,
517 pub signer: Ustr,
519 pub subaccount_id: i64,
521 pub time_in_force: DeriveTimeInForce,
523 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
525 pub trigger_price: Option<Decimal>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub trigger_price_type: Option<DeriveTriggerPriceType>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub trigger_reject_message: Option<String>,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub trigger_type: Option<DeriveTriggerType>,
535}
536
537#[derive(Clone, Debug, Serialize, Deserialize)]
539pub struct DeriveOrderResult {
540 pub order: DeriveOrder,
542 #[serde(default, deserialize_with = "deserialize_salvaged_vec")]
544 pub trades: Vec<DeriveTrade>,
545}
546
547#[derive(Clone, Debug, Serialize, Deserialize)]
549pub struct DeriveReplaceResult {
550 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub order: Option<DeriveOrder>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub cancelled_order: Option<DeriveOrder>,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub create_order_error: Option<JsonRpcError>,
559}
560
561#[derive(Clone, Debug)]
563pub enum DeriveReplaceOutcome {
564 Replaced(DeriveOrder),
566 Canceled {
568 cancelled_order: DeriveOrder,
570 create_order_error: JsonRpcError,
572 },
573}
574
575impl DeriveReplaceResult {
576 pub(crate) fn into_outcome(
583 self,
584 expected_cancel_order_id: &str,
585 expected_replacement_label: &str,
586 ) -> Result<DeriveReplaceOutcome, String> {
587 let validate_cancelled_order = |order: &DeriveOrder| {
588 if order.order_id != expected_cancel_order_id {
589 return Err(format!(
590 "private/replace cancelled order {} did not match requested order {expected_cancel_order_id}",
591 order.order_id,
592 ));
593 }
594
595 if order.order_status != DeriveOrderStatus::Cancelled {
596 return Err(format!(
597 "private/replace cancellation record for {expected_cancel_order_id} had status {}",
598 order.order_status,
599 ));
600 }
601 Ok(())
602 };
603
604 match (self.order, self.cancelled_order, self.create_order_error) {
605 (Some(order), cancelled_order, None) => {
606 if order.order_id == expected_cancel_order_id {
607 return Err(format!(
608 "private/replace returned the cancelled order {expected_cancel_order_id} as its replacement",
609 ));
610 }
611
612 if !matches!(
613 order.order_status,
614 DeriveOrderStatus::Open | DeriveOrderStatus::Filled
615 ) {
616 return Err(format!(
617 "private/replace replacement {} had status {}",
618 order.order_id, order.order_status,
619 ));
620 }
621
622 if order.label.as_str() != expected_replacement_label {
623 return Err(format!(
624 "private/replace replacement {} had label {}, expected {expected_replacement_label}",
625 order.order_id, order.label,
626 ));
627 }
628
629 if let Some(cancelled_order) = cancelled_order.as_ref() {
630 validate_cancelled_order(cancelled_order)?;
631 }
632 Ok(DeriveReplaceOutcome::Replaced(order))
633 }
634 (None, Some(cancelled_order), Some(create_order_error)) => {
635 validate_cancelled_order(&cancelled_order)?;
636 Ok(DeriveReplaceOutcome::Canceled {
637 cancelled_order,
638 create_order_error,
639 })
640 }
641 _ => Err("private/replace returned an inconsistent result".to_string()),
642 }
643 }
644}
645
646#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
648pub struct DeriveCancelByLabelResult {
649 pub cancelled_orders: i64,
651}
652
653pub type DeriveCancelByInstrumentResult = DeriveCancelByLabelResult;
655
656#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
658pub struct DeriveEmptyResult {}
659
660impl<'de> Deserialize<'de> for DeriveEmptyResult {
661 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
662 where
663 D: serde::Deserializer<'de>,
664 {
665 match Value::deserialize(deserializer)? {
666 Value::Null | Value::Object(_) => Ok(Self {}),
667 Value::String(value) if value == "ok" => Ok(Self {}),
668 other => Err(serde::de::Error::custom(format!(
669 "empty Derive result must be an object, null, or \"ok\", was {other}"
670 ))),
671 }
672 }
673}
674
675#[derive(Clone, Debug, Serialize, Deserialize)]
678pub struct DerivePosition {
679 #[serde(deserialize_with = "deserialize_decimal")]
681 pub amount: Decimal,
682 #[serde(deserialize_with = "deserialize_decimal")]
684 pub average_price: Decimal,
685 pub creation_timestamp: i64,
687 #[serde(deserialize_with = "deserialize_decimal")]
689 pub cumulative_funding: Decimal,
690 #[serde(deserialize_with = "deserialize_decimal")]
692 pub delta: Decimal,
693 #[serde(deserialize_with = "deserialize_decimal")]
695 pub gamma: Decimal,
696 #[serde(deserialize_with = "deserialize_decimal")]
698 pub index_price: Decimal,
699 #[serde(deserialize_with = "deserialize_decimal")]
701 pub initial_margin: Decimal,
702 pub instrument_name: Ustr,
704 pub instrument_type: DeriveInstrumentType,
706 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
708 pub leverage: Option<Decimal>,
709 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
711 pub liquidation_price: Option<Decimal>,
712 #[serde(deserialize_with = "deserialize_decimal")]
714 pub maintenance_margin: Decimal,
715 #[serde(deserialize_with = "deserialize_decimal")]
717 pub mark_price: Decimal,
718 #[serde(deserialize_with = "deserialize_decimal")]
720 pub mark_value: Decimal,
721 #[serde(deserialize_with = "deserialize_decimal")]
723 pub net_settlements: Decimal,
724 #[serde(deserialize_with = "deserialize_decimal")]
726 pub open_orders_margin: Decimal,
727 #[serde(deserialize_with = "deserialize_decimal")]
729 pub pending_funding: Decimal,
730 #[serde(deserialize_with = "deserialize_decimal")]
732 pub realized_pnl: Decimal,
733 #[serde(deserialize_with = "deserialize_decimal")]
735 pub theta: Decimal,
736 #[serde(deserialize_with = "deserialize_decimal")]
738 pub unrealized_pnl: Decimal,
739 #[serde(deserialize_with = "deserialize_decimal")]
741 pub vega: Decimal,
742}
743
744#[derive(Clone, Debug, Serialize, Deserialize)]
746pub struct DeriveCollateral {
747 #[serde(deserialize_with = "deserialize_decimal")]
749 pub amount: Decimal,
750 pub asset_name: Ustr,
752 pub asset_type: DeriveAssetType,
754 #[serde(deserialize_with = "deserialize_decimal")]
756 pub cumulative_interest: Decimal,
757 pub currency: Ustr,
759 #[serde(deserialize_with = "deserialize_decimal")]
761 pub initial_margin: Decimal,
762 #[serde(deserialize_with = "deserialize_decimal")]
764 pub maintenance_margin: Decimal,
765 #[serde(deserialize_with = "deserialize_decimal")]
767 pub mark_price: Decimal,
768 #[serde(deserialize_with = "deserialize_decimal")]
770 pub mark_value: Decimal,
771 #[serde(deserialize_with = "deserialize_decimal")]
773 pub pending_interest: Decimal,
774}
775
776#[derive(Clone, Debug, Serialize, Deserialize)]
778pub struct DeriveSubaccount {
779 pub collaterals: Vec<DeriveCollateral>,
781 #[serde(deserialize_with = "deserialize_decimal")]
783 pub collaterals_initial_margin: Decimal,
784 #[serde(deserialize_with = "deserialize_decimal")]
786 pub collaterals_maintenance_margin: Decimal,
787 #[serde(deserialize_with = "deserialize_decimal")]
789 pub collaterals_value: Decimal,
790 pub currency: Ustr,
792 #[serde(deserialize_with = "deserialize_decimal")]
794 pub initial_margin: Decimal,
795 pub is_under_liquidation: bool,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub label: Option<String>,
800 #[serde(deserialize_with = "deserialize_decimal")]
802 pub maintenance_margin: Decimal,
803 pub margin_type: DeriveMarginType,
805 #[serde(deserialize_with = "deserialize_salvaged_vec")]
807 pub open_orders: Vec<DeriveOrder>,
808 #[serde(deserialize_with = "deserialize_decimal")]
810 pub open_orders_margin: Decimal,
811 #[serde(deserialize_with = "deserialize_salvaged_vec")]
813 pub positions: Vec<DerivePosition>,
814 #[serde(deserialize_with = "deserialize_decimal")]
816 pub positions_initial_margin: Decimal,
817 #[serde(deserialize_with = "deserialize_decimal")]
819 pub positions_maintenance_margin: Decimal,
820 #[serde(deserialize_with = "deserialize_decimal")]
822 pub positions_value: Decimal,
823 pub subaccount_id: i64,
825 #[serde(deserialize_with = "deserialize_decimal")]
827 pub subaccount_value: Decimal,
828}
829
830#[derive(Clone, Debug, Serialize, Deserialize)]
833pub struct DeriveTrade {
834 pub direction: DeriveOrderSide,
836 #[serde(deserialize_with = "deserialize_decimal")]
838 pub index_price: Decimal,
839 pub instrument_name: Ustr,
841 pub is_transfer: bool,
843 pub label: Ustr,
845 pub liquidity_role: DeriveLiquidityRole,
847 #[serde(deserialize_with = "deserialize_decimal")]
849 pub mark_price: Decimal,
850 pub order_id: String,
852 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub quote_id: Option<String>,
855 #[serde(deserialize_with = "deserialize_decimal")]
857 pub realized_pnl: Decimal,
858 pub subaccount_id: i64,
860 pub timestamp: i64,
862 #[serde(deserialize_with = "deserialize_decimal")]
864 pub trade_amount: Decimal,
865 #[serde(deserialize_with = "deserialize_decimal")]
867 pub trade_fee: Decimal,
868 pub trade_id: String,
870 #[serde(deserialize_with = "deserialize_decimal")]
872 pub trade_price: Decimal,
873 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub tx_hash: Option<String>,
876 pub tx_status: DeriveTxStatus,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub wallet: Option<Ustr>,
881}
882
883#[derive(Clone, Debug, Serialize, Deserialize)]
892pub struct DerivePublicTrade {
893 pub direction: DeriveOrderSide,
895 #[serde(deserialize_with = "deserialize_decimal")]
897 pub index_price: Decimal,
898 pub instrument_name: Ustr,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
904 pub liquidity_role: Option<DeriveLiquidityRole>,
905 #[serde(deserialize_with = "deserialize_decimal")]
907 pub mark_price: Decimal,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub quote_id: Option<String>,
911 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub rfq_id: Option<String>,
914 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
916 pub realized_pnl: Option<Decimal>,
917 #[serde(default, skip_serializing_if = "Option::is_none")]
919 pub subaccount_id: Option<i64>,
920 pub timestamp: i64,
922 #[serde(deserialize_with = "deserialize_decimal")]
924 pub trade_amount: Decimal,
925 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
927 pub trade_fee: Option<Decimal>,
928 pub trade_id: String,
930 #[serde(deserialize_with = "deserialize_decimal")]
932 pub trade_price: Decimal,
933 #[serde(default, skip_serializing_if = "Option::is_none")]
935 pub tx_hash: Option<String>,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub tx_status: Option<DeriveTxStatus>,
939 #[serde(default, skip_serializing_if = "Option::is_none")]
941 pub wallet: Option<Ustr>,
942}
943
944#[derive(Clone, Debug, Serialize, Deserialize)]
946pub struct DerivePaginationInfo {
947 pub count: i64,
949 pub num_pages: i64,
951}
952
953#[derive(Clone, Debug, Serialize, Deserialize)]
955pub struct DeriveOrdersResult {
956 pub orders: Vec<DeriveOrder>,
959 pub pagination: DerivePaginationInfo,
961 pub subaccount_id: i64,
963}
964
965#[derive(Clone, Debug, Serialize, Deserialize)]
967pub struct DeriveOpenOrdersResult {
968 pub orders: Vec<DeriveOrder>,
971 pub subaccount_id: i64,
973}
974
975#[derive(Clone, Debug, Serialize, Deserialize)]
977pub struct DeriveTradesResult {
978 #[serde(deserialize_with = "deserialize_salvaged_vec")]
980 pub trades: Vec<DeriveTrade>,
981 pub pagination: DerivePaginationInfo,
983 pub subaccount_id: i64,
985}
986
987#[derive(Clone, Debug, Serialize, Deserialize)]
989pub struct DerivePublicTradesResult {
990 pub trades: Vec<DerivePublicTrade>,
992 pub pagination: DerivePaginationInfo,
994}
995
996#[derive(Clone, Debug, Serialize, Deserialize)]
1003pub struct DerivePublicCandle {
1004 #[serde(deserialize_with = "deserialize_decimal")]
1006 pub open_price: Decimal,
1007 #[serde(deserialize_with = "deserialize_decimal")]
1009 pub high_price: Decimal,
1010 #[serde(deserialize_with = "deserialize_decimal")]
1012 pub low_price: Decimal,
1013 #[serde(deserialize_with = "deserialize_decimal")]
1015 pub close_price: Decimal,
1016 #[serde(deserialize_with = "deserialize_decimal")]
1018 pub volume_usd: Decimal,
1019 #[serde(deserialize_with = "deserialize_decimal")]
1021 pub volume_contracts: Decimal,
1022 pub timestamp: i64,
1024 pub timestamp_bucket: i64,
1026}
1027
1028#[derive(Clone, Debug, Serialize, Deserialize)]
1030pub struct DerivePublicFundingRate {
1031 #[serde(deserialize_with = "deserialize_decimal")]
1033 pub funding_rate: Decimal,
1034 pub timestamp: i64,
1036}
1037
1038#[derive(Clone, Debug, Serialize, Deserialize)]
1040pub struct DerivePublicFundingRateHistoryResult {
1041 pub funding_rate_history: Vec<DerivePublicFundingRate>,
1043}
1044
1045#[derive(Clone, Debug, Serialize, Deserialize)]
1047pub struct DerivePositionsResult {
1048 pub positions: Vec<DerivePosition>,
1053 pub subaccount_id: i64,
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use std::path::PathBuf;
1060
1061 use rstest::rstest;
1062 use serde_json::{Value, json};
1063
1064 use super::*;
1065
1066 fn data_path() -> PathBuf {
1067 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data")
1068 }
1069
1070 fn load_json(filename: &str) -> Value {
1071 let content = std::fs::read_to_string(data_path().join(filename))
1072 .unwrap_or_else(|_| panic!("failed to read {filename}"));
1073 serde_json::from_str(&content).expect("invalid json")
1074 }
1075
1076 #[rstest]
1077 fn test_request_serializes_with_jsonrpc_version_tag() {
1078 let req = JsonRpcRequest::new(7, "public/get_instruments", json!({"currency": "ETH"}));
1079 let wire = serde_json::to_value(&req).unwrap();
1080 assert_eq!(wire["jsonrpc"], "2.0");
1081 assert_eq!(wire["id"], 7);
1082 assert_eq!(wire["method"], "public/get_instruments");
1083 assert_eq!(wire["params"]["currency"], "ETH");
1084 }
1085
1086 #[rstest]
1087 fn test_response_decodes_success_envelope() {
1088 let body = json!({"id": 1, "result": {"instruments": []}});
1089 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1090 assert_eq!(resp.id, Some(1));
1091 assert!(resp.error.is_none());
1092 assert!(resp.result.is_some());
1093 }
1094
1095 #[rstest]
1096 fn test_response_decodes_error_envelope() {
1097 let body = json!({
1098 "id": 9,
1099 "error": {"code": -32600, "message": "Invalid Request"}
1100 });
1101 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1102 assert_eq!(resp.id, Some(9));
1103 assert!(resp.result.is_none());
1104 let err = resp.error.expect("error present");
1105 assert_eq!(err.code, -32600);
1106 assert_eq!(err.message, "Invalid Request");
1107 assert!(err.data.is_none());
1108 }
1109
1110 #[rstest]
1111 fn test_response_decodes_error_envelope_with_data_field() {
1112 let body = json!({
1113 "id": 9,
1114 "error": {
1115 "code": -32602,
1116 "message": "Invalid params",
1117 "data": {"field": "currency"},
1118 }
1119 });
1120 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1121 let err = resp.error.expect("error present");
1122 assert_eq!(err.data, Some(json!({"field": "currency"})));
1123 }
1124
1125 #[rstest]
1126 fn test_response_tolerates_missing_id() {
1127 let body = json!({"result": {"ok": true}});
1128 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1129 assert!(resp.id.is_none());
1130 assert!(resp.result.is_some());
1131 }
1132
1133 #[rstest]
1134 fn test_response_tolerates_string_id() {
1135 let body = json!({"id": "e3c970c6-94aa-420c-b6db-d0f585a7fde9", "result": {"ok": true}});
1136 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1137 assert!(resp.id.is_none());
1138 assert!(resp.result.is_some());
1139 }
1140
1141 #[rstest]
1142 fn test_response_decodes_numeric_string_id() {
1143 let body = json!({"id": "42", "result": {"ok": true}});
1144 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1145 assert_eq!(resp.id, Some(42));
1146 assert!(resp.result.is_some());
1147 }
1148
1149 #[rstest]
1150 fn test_instrument_decodes_perp_with_perp_details() {
1151 let body = load_json("perps/instrument_eth.json");
1152 let instrument: DeriveInstrument = serde_json::from_value(body).unwrap();
1153 assert_eq!(instrument.instrument_name.as_str(), "ETH-PERP");
1154 assert_eq!(instrument.instrument_type, DeriveInstrumentType::Perp);
1155 assert!(instrument.option_details.is_none());
1156 let perp = instrument.perp_details.expect("perp details present");
1157 assert_eq!(perp.index, "ETH-USD");
1158 }
1159
1160 #[rstest]
1161 fn test_instrument_decodes_option_with_option_details() {
1162 let mut body = load_json("options/instrument_eth.json");
1163 body["scheduled_activation"] = json!(0);
1164 let instrument: DeriveInstrument = serde_json::from_value(body).unwrap();
1165 let option = instrument.option_details.expect("option details present");
1166 assert_eq!(option.option_type, DeriveOptionKind::Call);
1167 assert_eq!(option.strike.to_string(), "3500");
1168 assert!(option.settlement_price.is_none());
1169 }
1170
1171 #[rstest]
1172 fn test_order_decodes_partially_filled_market_order() {
1173 let body = load_json("perps/http_order_eth_partially_filled.json");
1177 let order: DeriveOrder = serde_json::from_value(body).unwrap();
1178 assert_eq!(order.amount.to_string(), "2.0");
1179 assert_eq!(order.filled_amount.to_string(), "1.5");
1180 assert_eq!(order.average_price.to_string(), "3500.25");
1181 assert_eq!(order.order_status, DeriveOrderStatus::Filled);
1182 assert_eq!(order.cancel_reason, DeriveOrderCancelReason::Empty);
1183 assert_eq!(order.direction, DeriveOrderSide::Buy);
1184 assert_eq!(order.time_in_force, DeriveTimeInForce::Ioc);
1185 assert_eq!(order.order_type, DeriveOrderType::Market);
1186 assert_eq!(order.instrument_name.as_str(), "ETH-PERP");
1187 assert_eq!(order.label.as_str(), "alpha-strategy");
1188 assert_eq!(order.signer.as_str(), "0xsigner");
1189 assert_eq!(order.order_id, "abc-123");
1190 assert_eq!(order.subaccount_id, 42);
1191 assert_eq!(order.signature_expiry_sec, 1_700_001_000);
1192 assert!(!order.mmp);
1193 assert!(!order.is_transfer);
1194 assert!(order.quote_id.is_none());
1195 assert!(order.replaced_order_id.is_none());
1196 }
1197
1198 #[rstest]
1199 fn test_replace_result_decodes_canceled_without_replacement() {
1200 let mut cancelled_order = load_json("perps/http_order_eth_partially_filled.json");
1201 cancelled_order["order_id"] = json!("old-order");
1202 cancelled_order["order_status"] = json!("cancelled");
1203 let result: DeriveReplaceResult = serde_json::from_value(json!({
1204 "order": null,
1205 "cancelled_order": cancelled_order,
1206 "create_order_error": {
1207 "code": 10001,
1208 "message": "insufficient margin",
1209 },
1210 }))
1211 .unwrap();
1212
1213 let outcome = result
1214 .into_outcome("old-order", "replacement-label")
1215 .unwrap();
1216 let DeriveReplaceOutcome::Canceled {
1217 cancelled_order,
1218 create_order_error,
1219 } = outcome
1220 else {
1221 panic!("expected canceled outcome");
1222 };
1223 assert_eq!(cancelled_order.order_id, "old-order");
1224 assert_eq!(cancelled_order.order_status, DeriveOrderStatus::Cancelled);
1225 assert_eq!(create_order_error.code, 10001);
1226 assert_eq!(create_order_error.message, "insufficient margin");
1227 assert!(create_order_error.data.is_none());
1228 }
1229
1230 #[rstest]
1231 fn test_replace_result_rejects_non_cancelled_partial_record() {
1232 let mut cancelled_order = load_json("perps/http_order_eth_partially_filled.json");
1233 cancelled_order["order_id"] = json!("old-order");
1234 cancelled_order["order_status"] = json!("open");
1235 let result: DeriveReplaceResult = serde_json::from_value(json!({
1236 "order": null,
1237 "cancelled_order": cancelled_order,
1238 "create_order_error": {
1239 "code": 10001,
1240 "message": "insufficient margin",
1241 },
1242 }))
1243 .unwrap();
1244
1245 let error = result
1246 .into_outcome("old-order", "replacement-label")
1247 .unwrap_err();
1248 assert!(error.contains("had status open"));
1249 }
1250
1251 #[rstest]
1252 fn test_replace_result_rejects_mismatched_replacement_label() {
1253 let mut replacement_order = load_json("perps/http_order_eth_partially_filled.json");
1254 replacement_order["order_id"] = json!("new-order");
1255 replacement_order["order_status"] = json!("open");
1256 replacement_order["label"] = json!("wrong-label");
1257 let result: DeriveReplaceResult = serde_json::from_value(json!({
1258 "order": replacement_order,
1259 "cancelled_order": null,
1260 "create_order_error": null,
1261 }))
1262 .unwrap();
1263
1264 let error = result
1265 .into_outcome("old-order", "expected-label")
1266 .unwrap_err();
1267 assert!(error.contains("had label wrong-label, expected expected-label"));
1268 }
1269
1270 #[rstest]
1271 fn test_position_decodes_perp_with_optional_leverage() {
1272 let body = load_json("perps/http_position_eth.json");
1274 let position: DerivePosition = serde_json::from_value(body).unwrap();
1275 assert_eq!(position.instrument_type, DeriveInstrumentType::Perp);
1276 assert_eq!(position.instrument_name.as_str(), "ETH-PERP");
1277 assert_eq!(position.amount.to_string(), "-2");
1278 assert_eq!(position.delta.to_string(), "-2");
1279 assert_eq!(position.gamma.to_string(), "0.1");
1280 assert_eq!(position.theta.to_string(), "-0.3");
1281 assert_eq!(position.vega.to_string(), "0.5");
1282 assert_eq!(position.unrealized_pnl.to_string(), "8");
1283 assert_eq!(position.mark_value.to_string(), "-7008");
1284 assert_eq!(
1285 position.leverage.as_ref().map(ToString::to_string),
1286 Some("5.0".into()),
1287 );
1288 assert_eq!(
1289 position.liquidation_price.as_ref().map(ToString::to_string),
1290 Some("4200".into()),
1291 );
1292 }
1293
1294 #[rstest]
1295 fn test_subaccount_decodes_with_collaterals_and_open_orders() {
1296 let body = load_json("common/http_subaccount_usdc.json");
1297 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1298 assert_eq!(subaccount.subaccount_id, 42);
1299 assert_eq!(subaccount.margin_type, DeriveMarginType::Pm);
1300 assert_eq!(subaccount.collaterals.len(), 1);
1301 assert_eq!(subaccount.collaterals[0].asset_type, DeriveAssetType::Erc20);
1302 assert!(!subaccount.is_under_liquidation);
1303 }
1304
1305 #[rstest]
1306 fn test_subaccount_decodes_high_scale_decimal_values() {
1307 let body = load_json("common/http_subaccount_high_scale.json");
1308 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1309 let position = &subaccount.positions[0];
1310
1311 assert_eq!(
1312 subaccount.initial_margin.to_string(),
1313 "0.1234567890123456789012345679",
1314 );
1315 assert_eq!(
1316 subaccount.collaterals[0].amount.to_string(),
1317 "0.1234567890123456789012345679",
1318 );
1319 assert_eq!(
1320 position.pending_funding.to_string(),
1321 "0.1234567890123456789012345679",
1322 );
1323 assert_eq!(
1324 position.leverage.as_ref().map(ToString::to_string),
1325 Some("5.1234567890123456789012345679".into()),
1326 );
1327 assert_eq!(
1328 position.liquidation_price.as_ref().map(ToString::to_string),
1329 Some("4200.1234567890123456789012346".into()),
1330 );
1331
1332 let open_order = &subaccount.open_orders[0];
1334 assert_eq!(
1335 open_order.filled_amount.to_string(),
1336 "0.1234567890123456789012345679",
1337 );
1338 assert_eq!(
1339 open_order.max_fee.to_string(),
1340 "0.1234567890123456789012345679",
1341 );
1342 }
1343
1344 #[rstest]
1345 fn test_subaccount_salvages_unknown_variant_rows() {
1346 let body = load_json("common/http_subaccount_unknown_variants.json");
1347 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1348
1349 assert_eq!(subaccount.margin_type, DeriveMarginType::Unknown);
1351 assert_eq!(
1352 subaccount.collaterals[0].asset_type,
1353 DeriveAssetType::Unknown
1354 );
1355 assert_eq!(subaccount.open_orders.len(), 1);
1356 assert_eq!(subaccount.open_orders[0].label.as_str(), "alpha-strategy");
1357 assert_eq!(subaccount.positions.len(), 1);
1358 assert_eq!(
1359 subaccount.positions[0].instrument_type,
1360 DeriveInstrumentType::Unknown,
1361 );
1362 }
1363
1364 #[rstest]
1365 fn test_public_trade_round_trips() {
1366 let body = load_json("perps/http_public_trade_eth_sell.json");
1367 let trade: DerivePublicTrade = serde_json::from_value(body).unwrap();
1368 assert_eq!(trade.direction, DeriveOrderSide::Sell);
1369 assert_eq!(trade.tx_status, Some(DeriveTxStatus::Settled));
1370 let reserialized = serde_json::to_value(&trade).unwrap();
1371 assert_eq!(reserialized["instrument_name"], "ETH-PERP");
1372 assert_eq!(reserialized["liquidity_role"], "taker");
1373 }
1374
1375 #[rstest]
1376 fn test_orders_result_envelope_decodes() {
1377 let body = json!({
1378 "orders": [],
1379 "pagination": {"count": 0, "num_pages": 0},
1380 "subaccount_id": 42,
1381 });
1382 let result: DeriveOrdersResult = serde_json::from_value(body).unwrap();
1383 assert!(result.orders.is_empty());
1384 assert_eq!(result.subaccount_id, 42);
1385 assert_eq!(result.pagination.count, 0);
1386 }
1387
1388 #[rstest]
1389 fn test_orders_result_decodes_unknown_variant_fields() {
1390 let body = load_json("perps/http_orders_result_eth_unknown_variants.json");
1391 let result: DeriveOrdersResult = serde_json::from_value(body).unwrap();
1392
1393 assert_eq!(result.orders.len(), 2);
1394 assert_eq!(result.orders[0].order_status, DeriveOrderStatus::Open);
1395 let unknowns = &result.orders[1];
1396 assert_eq!(unknowns.order_status, DeriveOrderStatus::Cancelled);
1397 assert_eq!(unknowns.cancel_reason, DeriveOrderCancelReason::Unknown);
1398 assert_eq!(unknowns.order_type, DeriveOrderType::Unknown);
1399 assert_eq!(unknowns.time_in_force, DeriveTimeInForce::Unknown);
1400 assert_eq!(unknowns.trigger_type, Some(DeriveTriggerType::Unknown));
1401 assert_eq!(
1402 unknowns.trigger_price_type,
1403 Some(DeriveTriggerPriceType::Unknown),
1404 );
1405 }
1406
1407 #[rstest]
1408 fn test_orders_result_fails_on_unknown_order_status() {
1409 let mut body = load_json("perps/http_orders_result_eth_unknown_variants.json");
1411 body["orders"][0]["order_status"] = json!("queued");
1412
1413 assert!(serde_json::from_value::<DeriveOrdersResult>(body).is_err());
1414 }
1415
1416 #[rstest]
1417 fn test_positions_result_fails_on_undecodable_row() {
1418 let mut body = load_json("perps/http_positions_result_eth.json");
1420 body["positions"][0]["amount"] = json!({});
1421
1422 assert!(serde_json::from_value::<DerivePositionsResult>(body).is_err());
1423 }
1424
1425 fn perp_ticker_json() -> Value {
1426 load_json("perps/http_ticker_eth_snapshot.json")
1427 }
1428
1429 #[rstest]
1430 fn test_ticker_decodes_perp_snapshot() {
1431 let ticker: DeriveTicker = serde_json::from_value(perp_ticker_json()).unwrap();
1432 assert_eq!(ticker.instrument_name.as_str(), "ETH-PERP");
1433 assert_eq!(ticker.instrument_type, DeriveInstrumentType::Perp);
1434 assert_eq!(ticker.mark_price.to_string(), "3500.5");
1435 assert_eq!(ticker.best_bid_price.to_string(), "3499.5");
1436 assert_eq!(ticker.best_ask_price.to_string(), "3501.0");
1437 assert_eq!(ticker.timestamp, 1_700_000_000_000);
1438 assert!(ticker.option_details.is_none());
1439 assert!(ticker.option_pricing.is_none());
1440 let perp = ticker.perp_details.expect("perp details present");
1441 assert_eq!(perp.index.as_str(), "ETH-USD");
1442 assert_eq!(perp.funding_rate.to_string(), "0.0002");
1443 let stats = ticker
1444 .stats
1445 .as_ref()
1446 .expect("WS ticker fixture includes stats");
1447 assert_eq!(stats.contract_volume.to_string(), "12345.6");
1448 assert_eq!(stats.high.to_string(), "3600");
1449 assert_eq!(stats.num_trades.to_string(), "789");
1450 }
1451
1452 #[rstest]
1453 fn test_ticker_decodes_option_snapshot_with_greeks() {
1454 let body = load_json("options/http_ticker_eth_snapshot.json");
1455 let ticker: DeriveTicker = serde_json::from_value(body).unwrap();
1456 assert_eq!(ticker.instrument_type, DeriveInstrumentType::Option);
1457 assert!(ticker.perp_details.is_none());
1458 let option = ticker.option_details.expect("option details present");
1459 assert_eq!(option.option_type, DeriveOptionKind::Call);
1460 assert_eq!(option.strike.to_string(), "3500");
1461 assert!(option.settlement_price.is_none());
1462 let greeks = ticker.option_pricing.expect("option pricing present");
1463 assert_eq!(greeks.delta.to_string(), "0.55");
1464 assert_eq!(greeks.gamma.to_string(), "0.0008");
1465 assert_eq!(greeks.theta.to_string(), "-2.1");
1466 assert_eq!(greeks.vega.to_string(), "4.5");
1467 assert_eq!(greeks.iv.to_string(), "0.60");
1468 assert_eq!(greeks.forward_price.to_string(), "3505");
1469 }
1470
1471 #[rstest]
1472 fn test_private_trade_decodes_with_order_link() {
1473 let body = load_json("perps/http_private_trade_eth.json");
1477 let trade: DeriveTrade = serde_json::from_value(body).unwrap();
1478 assert_eq!(trade.direction, DeriveOrderSide::Buy);
1479 assert_eq!(trade.liquidity_role, DeriveLiquidityRole::Maker);
1480 assert_eq!(trade.tx_status, DeriveTxStatus::Settled);
1481 assert_eq!(trade.instrument_name.as_str(), "ETH-PERP");
1482 assert_eq!(trade.label.as_str(), "alpha-strategy");
1483 assert_eq!(trade.wallet.as_ref().map(Ustr::as_str), Some("0xwallet"));
1484 assert_eq!(trade.order_id, "order-abc");
1485 assert_eq!(trade.trade_id, "trade-xyz");
1486 assert_eq!(trade.subaccount_id, 42);
1487 assert_eq!(trade.realized_pnl.to_string(), "12.5");
1488 assert_eq!(trade.trade_amount.to_string(), "0.5");
1489 assert_eq!(trade.trade_price.to_string(), "3499.0");
1490 assert!(!trade.is_transfer);
1491 assert!(trade.quote_id.is_none());
1492 assert_eq!(trade.tx_hash.as_deref(), Some("0xhash"));
1493 }
1494
1495 #[rstest]
1496 fn test_private_trade_decodes_high_scale_decimal_values() {
1497 let mut body = load_json("perps/http_private_trade_eth.json");
1498 body["trade_fee"] = json!("1.234567890123456789012345678912345e-1");
1499 body["realized_pnl"] = json!("0.1234567890123456789012345678912345");
1500
1501 let trade: DeriveTrade = serde_json::from_value(body).unwrap();
1502
1503 assert_eq!(
1504 trade.trade_fee.to_string(),
1505 "0.1234567890123456789012345679"
1506 );
1507 assert_eq!(
1508 trade.realized_pnl.to_string(),
1509 "0.1234567890123456789012345679",
1510 );
1511 }
1512
1513 #[rstest]
1514 fn test_order_result_decodes_pending_trade_with_null_tx_hash() {
1515 let mut body = load_json("spot/http_submit_order_response_mainnet.json");
1516 let mut trade = load_json("perps/http_private_trade_eth.json");
1517 trade["tx_hash"] = Value::Null;
1518 trade["tx_status"] = json!("requested");
1519 trade.as_object_mut().unwrap().remove("wallet");
1520 body["result"]["trades"] = json!([trade]);
1521
1522 let result: DeriveOrderResult =
1523 serde_json::from_value(body["result"].clone()).expect("result decodes");
1524
1525 assert_eq!(result.trades.len(), 1);
1526 assert!(result.trades[0].tx_hash.is_none());
1527 assert_eq!(result.trades[0].tx_status, DeriveTxStatus::Requested);
1528 assert!(result.trades[0].wallet.is_none());
1529 }
1530
1531 #[rstest]
1532 fn test_empty_result_decodes_cancel_ack_shapes() {
1533 let object: DeriveEmptyResult = serde_json::from_value(json!({})).unwrap();
1534 let ok_string: DeriveEmptyResult = serde_json::from_value(json!("ok")).unwrap();
1535 let null_envelope: JsonRpcResponse<DeriveEmptyResult> =
1536 serde_json::from_value(json!({"id": 1, "result": null})).unwrap();
1537
1538 assert_eq!(object, DeriveEmptyResult {});
1539 assert_eq!(ok_string, DeriveEmptyResult {});
1540 assert_eq!(null_envelope.result, Some(DeriveEmptyResult {}));
1541 }
1542
1543 #[rstest]
1544 #[case("common/ws_cancel_by_label_zero.json", 0)]
1545 #[case("common/ws_cancel_by_label_nonzero.json", 2)]
1546 fn test_cancel_order_by_label_result_decodes_count(
1547 #[case] filename: &str,
1548 #[case] expected: i64,
1549 ) {
1550 let response: JsonRpcResponse<DeriveCancelByLabelResult> =
1551 serde_json::from_value(load_json(filename)).expect("response decodes");
1552
1553 assert_eq!(response.result.unwrap().cancelled_orders, expected);
1554 }
1555
1556 #[rstest]
1557 fn test_trades_result_envelope_decodes() {
1558 let body = load_json("perps/http_trades_result_eth.json");
1559 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1560 assert_eq!(result.trades.len(), 1);
1561 assert_eq!(result.subaccount_id, 7);
1562 assert_eq!(result.pagination.count, 1);
1563 assert_eq!(result.pagination.num_pages, 1);
1564 assert_eq!(result.trades[0].trade_id, "t-1");
1565 }
1566
1567 #[rstest]
1568 fn test_trades_result_salvages_unknown_variant_rows() {
1569 let body = load_json("perps/http_trades_result_eth_unknown_variants.json");
1570 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1571
1572 assert_eq!(result.trades.len(), 2);
1574 assert_eq!(result.trades[0].trade_id, "t-1");
1575 let unknowns = &result.trades[1];
1576 assert_eq!(unknowns.trade_id, "t-2");
1577 assert_eq!(unknowns.liquidity_role, DeriveLiquidityRole::Unknown);
1578 }
1579
1580 #[rstest]
1581 fn test_trades_result_drops_unknown_tx_status_rows() {
1582 let mut body = load_json("perps/http_trades_result_eth.json");
1584 body["trades"][0]["tx_status"] = json!("bridging");
1585
1586 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1587
1588 assert!(result.trades.is_empty());
1589 }
1590
1591 #[rstest]
1592 fn test_public_trades_result_envelope_decodes() {
1593 let body = load_json("perps/http_public_trades_result_eth.json");
1594 let result: DerivePublicTradesResult = serde_json::from_value(body).unwrap();
1595 assert_eq!(result.trades.len(), 1);
1596 assert_eq!(result.pagination.count, 1);
1597 assert_eq!(result.trades[0].trade_id, "pub-1");
1598 }
1599
1600 #[rstest]
1601 fn test_public_funding_rate_history_result_envelope_decodes() {
1602 let body = load_json("perps/http_public_funding_rate_history_eth.json");
1603 let result: DerivePublicFundingRateHistoryResult = serde_json::from_value(body).unwrap();
1604 assert_eq!(result.funding_rate_history.len(), 3);
1605 let first = &result.funding_rate_history[0];
1606 assert_eq!(first.funding_rate.to_string(), "0.00012");
1607 assert_eq!(first.timestamp, 1_700_000_000_000);
1608 assert_eq!(
1609 result.funding_rate_history.last().unwrap().timestamp,
1610 1_700_007_200_000,
1611 );
1612 }
1613
1614 #[rstest]
1615 fn test_public_candles_decode_array() {
1616 let body = load_json("perps/http_public_candles_eth.json");
1620 let candles: Vec<DerivePublicCandle> = serde_json::from_value(body).unwrap();
1621 assert_eq!(candles.len(), 3);
1622 let first = &candles[0];
1623 assert_eq!(first.open_price.to_string(), "3500.0");
1624 assert_eq!(first.high_price.to_string(), "3501.5");
1625 assert_eq!(first.low_price.to_string(), "3499.0");
1626 assert_eq!(first.close_price.to_string(), "3501.0");
1627 assert_eq!(first.volume_usd.to_string(), "12345.6");
1628 assert_eq!(first.volume_contracts.to_string(), "3.527");
1629 assert_eq!(first.timestamp, 1_700_000_007);
1632 assert_eq!(first.timestamp_bucket, 1_700_000_000);
1633 assert_eq!(candles.last().unwrap().timestamp_bucket, 1_700_001_800);
1634 }
1635
1636 #[rstest]
1637 fn test_positions_result_envelope_decodes() {
1638 let body = load_json("perps/http_positions_result_eth.json");
1639 let result: DerivePositionsResult = serde_json::from_value(body).unwrap();
1640 assert_eq!(result.positions.len(), 1);
1641 assert_eq!(result.subaccount_id, 42);
1642 assert_eq!(result.positions[0].instrument_name.as_str(), "ETH-PERP");
1643 assert!(result.positions[0].leverage.is_none());
1644 assert!(result.positions[0].liquidation_price.is_none());
1645 }
1646}