1use std::collections::HashMap;
32
33#[cfg(test)]
34use nautilus_core::string::secret::REDACTED;
35use nautilus_core::{
36 serialization::{deserialize_decimal, deserialize_optional_decimal},
37 string::secret::SecretString,
38};
39use rust_decimal::Decimal;
40use serde::{Deserialize, Serialize};
41use serde_json::Value;
42use ustr::Ustr;
43
44use crate::common::{
45 enums::{
46 DeriveAssetType, DeriveInstrumentType, DeriveLiquidityRole, DeriveMarginType,
47 DeriveOptionKind, DeriveOrderCancelReason, DeriveOrderSide, DeriveOrderStatus,
48 DeriveOrderType, DeriveTimeInForce, DeriveTriggerPriceType, DeriveTriggerType,
49 DeriveTxStatus,
50 },
51 parse::deserialize_salvaged_vec,
52};
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct JsonRpcRequest<P> {
59 pub jsonrpc: &'static str,
61 pub id: u64,
63 pub method: &'static str,
65 pub params: P,
67}
68
69impl<P> JsonRpcRequest<P> {
70 #[must_use]
72 pub fn new(id: u64, method: &'static str, params: P) -> Self {
73 Self {
74 jsonrpc: "2.0",
75 id,
76 method,
77 params,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Deserialize)]
85#[serde(bound(deserialize = "R: Deserialize<'de>"))]
86pub struct JsonRpcResponse<R> {
87 #[serde(default, deserialize_with = "deserialize_optional_jsonrpc_id")]
90 pub id: Option<u64>,
91 #[serde(default, deserialize_with = "deserialize_present_jsonrpc_result")]
93 pub result: Option<R>,
94 #[serde(default)]
96 pub error: Option<JsonRpcError>,
97}
98
99fn deserialize_present_jsonrpc_result<'de, D, R>(deserializer: D) -> Result<Option<R>, D::Error>
100where
101 D: serde::Deserializer<'de>,
102 R: Deserialize<'de>,
103{
104 R::deserialize(deserializer).map(Some)
105}
106
107fn deserialize_optional_jsonrpc_id<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
108where
109 D: serde::Deserializer<'de>,
110{
111 let value = Option::<Value>::deserialize(deserializer)?;
112 match value {
113 None | Some(Value::Null) => Ok(None),
114 Some(Value::Number(number)) => number
115 .as_u64()
116 .map(Some)
117 .ok_or_else(|| serde::de::Error::custom("JSON-RPC id must be an unsigned integer")),
118 Some(Value::String(value)) => Ok(value.parse::<u64>().ok()),
119 Some(other) => Err(serde::de::Error::custom(format!(
120 "JSON-RPC id must be an unsigned integer or string, was {other}"
121 ))),
122 }
123}
124
125#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
127pub struct JsonRpcError {
128 pub code: i64,
130 pub message: String,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub data: Option<Value>,
135}
136
137#[derive(Clone, Debug, Serialize, Deserialize)]
140pub struct DeriveOptionPublicDetails {
141 pub expiry: i64,
143 pub index: Ustr,
145 pub option_type: DeriveOptionKind,
147 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
149 pub settlement_price: Option<Decimal>,
150 #[serde(deserialize_with = "deserialize_decimal")]
152 pub strike: Decimal,
153}
154
155#[derive(Clone, Debug, Serialize, Deserialize)]
158pub struct DerivePerpPublicDetails {
159 #[serde(deserialize_with = "deserialize_decimal")]
161 pub aggregate_funding: Decimal,
162 #[serde(deserialize_with = "deserialize_decimal")]
164 pub funding_rate: Decimal,
165 pub index: Ustr,
167 #[serde(deserialize_with = "deserialize_decimal")]
169 pub max_rate_per_hour: Decimal,
170 #[serde(deserialize_with = "deserialize_decimal")]
172 pub min_rate_per_hour: Decimal,
173 #[serde(deserialize_with = "deserialize_decimal")]
175 pub static_interest_rate: Decimal,
176}
177
178#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct DeriveInstrument {
181 #[serde(deserialize_with = "deserialize_decimal")]
183 pub amount_step: Decimal,
184 pub base_asset_address: Ustr,
186 pub base_asset_sub_id: Ustr,
188 pub base_currency: Ustr,
190 #[serde(deserialize_with = "deserialize_decimal")]
192 pub base_fee: Decimal,
193 pub instrument_name: Ustr,
195 pub instrument_type: DeriveInstrumentType,
197 pub is_active: bool,
199 #[serde(deserialize_with = "deserialize_decimal")]
201 pub maker_fee_rate: Decimal,
202 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
204 pub mark_price_fee_rate_cap: Option<Decimal>,
205 #[serde(deserialize_with = "deserialize_decimal")]
207 pub maximum_amount: Decimal,
208 #[serde(deserialize_with = "deserialize_decimal")]
210 pub minimum_amount: Decimal,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub option_details: Option<DeriveOptionPublicDetails>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub perp_details: Option<DerivePerpPublicDetails>,
217 pub quote_currency: Ustr,
219 pub scheduled_activation: i64,
221 pub scheduled_deactivation: i64,
223 #[serde(deserialize_with = "deserialize_decimal")]
225 pub taker_fee_rate: Decimal,
226 #[serde(deserialize_with = "deserialize_decimal")]
228 pub tick_size: Decimal,
229}
230
231#[derive(Clone, Debug, Serialize, Deserialize)]
233pub struct DeriveAggregateTradingStats {
234 #[serde(alias = "c", deserialize_with = "deserialize_decimal")]
236 pub contract_volume: Decimal,
237 #[serde(alias = "h", deserialize_with = "deserialize_decimal")]
239 pub high: Decimal,
240 #[serde(alias = "l", deserialize_with = "deserialize_decimal")]
242 pub low: Decimal,
243 #[serde(alias = "n", deserialize_with = "deserialize_decimal")]
245 pub num_trades: Decimal,
246 #[serde(alias = "oi", deserialize_with = "deserialize_decimal")]
248 pub open_interest: Decimal,
249 #[serde(alias = "p", deserialize_with = "deserialize_decimal")]
251 pub percent_change: Decimal,
252 #[serde(alias = "pr", deserialize_with = "deserialize_decimal")]
254 pub usd_change: Decimal,
255}
256
257#[derive(Clone, Debug, Serialize, Deserialize)]
259pub struct DeriveOptionPricing {
260 #[serde(alias = "ai", deserialize_with = "deserialize_decimal")]
262 pub ask_iv: Decimal,
263 #[serde(alias = "bi", deserialize_with = "deserialize_decimal")]
265 pub bid_iv: Decimal,
266 #[serde(alias = "d", deserialize_with = "deserialize_decimal")]
268 pub delta: Decimal,
269 #[serde(alias = "f", deserialize_with = "deserialize_decimal")]
271 pub forward_price: Decimal,
272 #[serde(alias = "g", deserialize_with = "deserialize_decimal")]
274 pub gamma: Decimal,
275 #[serde(alias = "i", deserialize_with = "deserialize_decimal")]
277 pub iv: Decimal,
278 #[serde(alias = "m", deserialize_with = "deserialize_decimal")]
280 pub mark_price: Decimal,
281 #[serde(alias = "r", deserialize_with = "deserialize_decimal")]
283 pub rho: Decimal,
284 #[serde(alias = "t", deserialize_with = "deserialize_decimal")]
286 pub theta: Decimal,
287 #[serde(alias = "v", deserialize_with = "deserialize_decimal")]
289 pub vega: Decimal,
290}
291
292#[derive(Clone, Debug, Serialize, Deserialize)]
294pub struct DeriveTickerSnapshot {
295 #[serde(default)]
297 pub instrument_name: Ustr,
298 #[serde(
300 rename = "A",
301 alias = "best_ask_amount",
302 deserialize_with = "deserialize_decimal"
303 )]
304 pub best_ask_amount: Decimal,
305 #[serde(
307 rename = "a",
308 alias = "best_ask_price",
309 deserialize_with = "deserialize_decimal"
310 )]
311 pub best_ask_price: Decimal,
312 #[serde(
314 rename = "B",
315 alias = "best_bid_amount",
316 deserialize_with = "deserialize_decimal"
317 )]
318 pub best_bid_amount: Decimal,
319 #[serde(
321 rename = "b",
322 alias = "best_bid_price",
323 deserialize_with = "deserialize_decimal"
324 )]
325 pub best_bid_price: Decimal,
326 #[serde(
328 rename = "f",
329 alias = "funding_rate",
330 default,
331 deserialize_with = "deserialize_optional_decimal"
332 )]
333 pub funding_rate: Option<Decimal>,
334 #[serde(
336 rename = "I",
337 alias = "index_price",
338 deserialize_with = "deserialize_decimal"
339 )]
340 pub index_price: Decimal,
341 #[serde(
343 rename = "M",
344 alias = "mark_price",
345 deserialize_with = "deserialize_decimal"
346 )]
347 pub mark_price: Decimal,
348 #[serde(
350 rename = "maxp",
351 alias = "max_price",
352 deserialize_with = "deserialize_decimal"
353 )]
354 pub max_price: Decimal,
355 #[serde(
357 rename = "minp",
358 alias = "min_price",
359 deserialize_with = "deserialize_decimal"
360 )]
361 pub min_price: Decimal,
362 #[serde(default)]
364 pub option_pricing: Option<DeriveOptionPricing>,
365 #[serde(default)]
367 pub stats: Option<DeriveAggregateTradingStats>,
368 #[serde(rename = "t", alias = "timestamp")]
370 pub timestamp: i64,
371}
372
373#[derive(Clone, Debug, Serialize, Deserialize)]
375pub struct DeriveTickersResult {
376 pub tickers: HashMap<String, DeriveTickerSnapshot>,
378}
379
380#[derive(Clone, Debug, Serialize, Deserialize)]
383pub struct DeriveTicker {
384 #[serde(deserialize_with = "deserialize_decimal")]
386 pub amount_step: Decimal,
387 pub base_asset_address: Ustr,
389 pub base_asset_sub_id: Ustr,
391 pub base_currency: Ustr,
393 #[serde(deserialize_with = "deserialize_decimal")]
395 pub base_fee: Decimal,
396 #[serde(deserialize_with = "deserialize_decimal")]
398 pub best_ask_amount: Decimal,
399 #[serde(deserialize_with = "deserialize_decimal")]
401 pub best_ask_price: Decimal,
402 #[serde(deserialize_with = "deserialize_decimal")]
404 pub best_bid_amount: Decimal,
405 #[serde(deserialize_with = "deserialize_decimal")]
407 pub best_bid_price: Decimal,
408 #[serde(deserialize_with = "deserialize_decimal")]
410 pub index_price: Decimal,
411 pub instrument_name: Ustr,
413 pub instrument_type: DeriveInstrumentType,
415 pub is_active: bool,
417 #[serde(deserialize_with = "deserialize_decimal")]
419 pub maker_fee_rate: Decimal,
420 #[serde(deserialize_with = "deserialize_decimal")]
422 pub mark_price: Decimal,
423 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
425 pub mark_price_fee_rate_cap: Option<Decimal>,
426 #[serde(deserialize_with = "deserialize_decimal")]
428 pub max_price: Decimal,
429 #[serde(deserialize_with = "deserialize_decimal")]
431 pub maximum_amount: Decimal,
432 #[serde(deserialize_with = "deserialize_decimal")]
434 pub min_price: Decimal,
435 #[serde(deserialize_with = "deserialize_decimal")]
437 pub minimum_amount: Decimal,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub option_details: Option<DeriveOptionPublicDetails>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub option_pricing: Option<DeriveOptionPricing>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub perp_details: Option<DerivePerpPublicDetails>,
447 pub quote_currency: Ustr,
449 pub scheduled_activation: i64,
451 pub scheduled_deactivation: i64,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub stats: Option<DeriveAggregateTradingStats>,
456 #[serde(deserialize_with = "deserialize_decimal")]
458 pub taker_fee_rate: Decimal,
459 #[serde(deserialize_with = "deserialize_decimal")]
461 pub tick_size: Decimal,
462 pub timestamp: i64,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct DeriveOrder {
470 #[serde(deserialize_with = "deserialize_decimal")]
472 pub amount: Decimal,
473 #[serde(deserialize_with = "deserialize_decimal")]
475 pub average_price: Decimal,
476 pub cancel_reason: DeriveOrderCancelReason,
478 pub creation_timestamp: i64,
480 pub direction: DeriveOrderSide,
482 #[serde(deserialize_with = "deserialize_decimal")]
484 pub filled_amount: Decimal,
485 pub instrument_name: Ustr,
487 pub is_transfer: bool,
489 pub label: Ustr,
491 pub last_update_timestamp: i64,
493 #[serde(deserialize_with = "deserialize_decimal")]
495 pub limit_price: Decimal,
496 #[serde(deserialize_with = "deserialize_decimal")]
498 pub max_fee: Decimal,
499 pub mmp: bool,
501 pub nonce: i64,
503 #[serde(deserialize_with = "deserialize_decimal")]
505 pub order_fee: Decimal,
506 pub order_id: String,
508 pub order_status: DeriveOrderStatus,
510 pub order_type: DeriveOrderType,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub quote_id: Option<String>,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub replaced_order_id: Option<String>,
518 pub signature: SecretString,
520 pub signature_expiry_sec: i64,
522 pub signer: Ustr,
524 pub subaccount_id: i64,
526 pub time_in_force: DeriveTimeInForce,
528 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
530 pub trigger_price: Option<Decimal>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub trigger_price_type: Option<DeriveTriggerPriceType>,
534 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub trigger_reject_message: Option<String>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub trigger_type: Option<DeriveTriggerType>,
540}
541
542#[derive(Clone, Debug, Serialize, Deserialize)]
544pub struct DeriveOrderResult {
545 pub order: DeriveOrder,
547 #[serde(default, deserialize_with = "deserialize_salvaged_vec")]
549 pub trades: Vec<DeriveTrade>,
550}
551
552#[derive(Clone, Debug, Serialize, Deserialize)]
554pub struct DeriveReplaceResult {
555 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub order: Option<DeriveOrder>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub cancelled_order: Option<DeriveOrder>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub create_order_error: Option<JsonRpcError>,
564}
565
566#[derive(Clone, Debug)]
568pub enum DeriveReplaceOutcome {
569 Replaced(DeriveOrder),
571 Canceled {
573 cancelled_order: DeriveOrder,
575 create_order_error: JsonRpcError,
577 },
578}
579
580impl DeriveReplaceResult {
581 pub(crate) fn into_outcome(
588 self,
589 expected_cancel_order_id: &str,
590 expected_replacement_label: &str,
591 ) -> Result<DeriveReplaceOutcome, String> {
592 let validate_cancelled_order = |order: &DeriveOrder| {
593 if order.order_id != expected_cancel_order_id {
594 return Err(format!(
595 "private/replace cancelled order {} did not match requested order {expected_cancel_order_id}",
596 order.order_id,
597 ));
598 }
599
600 if order.order_status != DeriveOrderStatus::Cancelled {
601 return Err(format!(
602 "private/replace cancellation record for {expected_cancel_order_id} had status {}",
603 order.order_status,
604 ));
605 }
606 Ok(())
607 };
608
609 match (self.order, self.cancelled_order, self.create_order_error) {
610 (Some(order), cancelled_order, None) => {
611 if order.order_id == expected_cancel_order_id {
612 return Err(format!(
613 "private/replace returned the cancelled order {expected_cancel_order_id} as its replacement",
614 ));
615 }
616
617 if !matches!(
618 order.order_status,
619 DeriveOrderStatus::Open | DeriveOrderStatus::Filled
620 ) {
621 return Err(format!(
622 "private/replace replacement {} had status {}",
623 order.order_id, order.order_status,
624 ));
625 }
626
627 if order.label != expected_replacement_label {
628 return Err(format!(
629 "private/replace replacement {} had label {}, expected {expected_replacement_label}",
630 order.order_id, order.label,
631 ));
632 }
633
634 if let Some(cancelled_order) = cancelled_order.as_ref() {
635 validate_cancelled_order(cancelled_order)?;
636 }
637 Ok(DeriveReplaceOutcome::Replaced(order))
638 }
639 (None, Some(cancelled_order), Some(create_order_error)) => {
640 validate_cancelled_order(&cancelled_order)?;
641 Ok(DeriveReplaceOutcome::Canceled {
642 cancelled_order,
643 create_order_error,
644 })
645 }
646 _ => Err("private/replace returned an inconsistent result".to_string()),
647 }
648 }
649}
650
651#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
653pub struct DeriveCancelByLabelResult {
654 pub cancelled_orders: i64,
656}
657
658pub type DeriveCancelByInstrumentResult = DeriveCancelByLabelResult;
660
661#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
663pub struct DeriveEmptyResult {}
664
665impl<'de> Deserialize<'de> for DeriveEmptyResult {
666 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
667 where
668 D: serde::Deserializer<'de>,
669 {
670 match Value::deserialize(deserializer)? {
671 Value::Null | Value::Object(_) => Ok(Self {}),
672 Value::String(value) if value == "ok" => Ok(Self {}),
673 other => Err(serde::de::Error::custom(format!(
674 "empty Derive result must be an object, null, or \"ok\", was {other}"
675 ))),
676 }
677 }
678}
679
680#[derive(Clone, Debug, Serialize, Deserialize)]
683pub struct DerivePosition {
684 #[serde(deserialize_with = "deserialize_decimal")]
686 pub amount: Decimal,
687 #[serde(deserialize_with = "deserialize_decimal")]
689 pub average_price: Decimal,
690 pub creation_timestamp: i64,
692 #[serde(deserialize_with = "deserialize_decimal")]
694 pub cumulative_funding: Decimal,
695 #[serde(deserialize_with = "deserialize_decimal")]
697 pub delta: Decimal,
698 #[serde(deserialize_with = "deserialize_decimal")]
700 pub gamma: Decimal,
701 #[serde(deserialize_with = "deserialize_decimal")]
703 pub index_price: Decimal,
704 #[serde(deserialize_with = "deserialize_decimal")]
706 pub initial_margin: Decimal,
707 pub instrument_name: Ustr,
709 pub instrument_type: DeriveInstrumentType,
711 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
713 pub leverage: Option<Decimal>,
714 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
716 pub liquidation_price: Option<Decimal>,
717 #[serde(deserialize_with = "deserialize_decimal")]
719 pub maintenance_margin: Decimal,
720 #[serde(deserialize_with = "deserialize_decimal")]
722 pub mark_price: Decimal,
723 #[serde(deserialize_with = "deserialize_decimal")]
725 pub mark_value: Decimal,
726 #[serde(deserialize_with = "deserialize_decimal")]
728 pub net_settlements: Decimal,
729 #[serde(deserialize_with = "deserialize_decimal")]
731 pub open_orders_margin: Decimal,
732 #[serde(deserialize_with = "deserialize_decimal")]
734 pub pending_funding: Decimal,
735 #[serde(deserialize_with = "deserialize_decimal")]
737 pub realized_pnl: Decimal,
738 #[serde(deserialize_with = "deserialize_decimal")]
740 pub theta: Decimal,
741 #[serde(deserialize_with = "deserialize_decimal")]
743 pub unrealized_pnl: Decimal,
744 #[serde(deserialize_with = "deserialize_decimal")]
746 pub vega: Decimal,
747}
748
749#[derive(Clone, Debug, Serialize, Deserialize)]
751pub struct DeriveCollateral {
752 #[serde(deserialize_with = "deserialize_decimal")]
754 pub amount: Decimal,
755 pub asset_name: Ustr,
757 pub asset_type: DeriveAssetType,
759 #[serde(deserialize_with = "deserialize_decimal")]
761 pub cumulative_interest: Decimal,
762 pub currency: Ustr,
764 #[serde(deserialize_with = "deserialize_decimal")]
766 pub initial_margin: Decimal,
767 #[serde(deserialize_with = "deserialize_decimal")]
769 pub maintenance_margin: Decimal,
770 #[serde(deserialize_with = "deserialize_decimal")]
772 pub mark_price: Decimal,
773 #[serde(deserialize_with = "deserialize_decimal")]
775 pub mark_value: Decimal,
776 #[serde(deserialize_with = "deserialize_decimal")]
778 pub pending_interest: Decimal,
779}
780
781#[derive(Clone, Debug, Serialize, Deserialize)]
783pub struct DeriveSubaccount {
784 pub collaterals: Vec<DeriveCollateral>,
786 #[serde(deserialize_with = "deserialize_decimal")]
788 pub collaterals_initial_margin: Decimal,
789 #[serde(deserialize_with = "deserialize_decimal")]
791 pub collaterals_maintenance_margin: Decimal,
792 #[serde(deserialize_with = "deserialize_decimal")]
794 pub collaterals_value: Decimal,
795 pub currency: Ustr,
797 #[serde(deserialize_with = "deserialize_decimal")]
799 pub initial_margin: Decimal,
800 pub is_under_liquidation: bool,
802 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub label: Option<String>,
805 #[serde(deserialize_with = "deserialize_decimal")]
807 pub maintenance_margin: Decimal,
808 pub margin_type: DeriveMarginType,
810 #[serde(deserialize_with = "deserialize_salvaged_vec")]
812 pub open_orders: Vec<DeriveOrder>,
813 #[serde(deserialize_with = "deserialize_decimal")]
815 pub open_orders_margin: Decimal,
816 #[serde(deserialize_with = "deserialize_salvaged_vec")]
818 pub positions: Vec<DerivePosition>,
819 #[serde(deserialize_with = "deserialize_decimal")]
821 pub positions_initial_margin: Decimal,
822 #[serde(deserialize_with = "deserialize_decimal")]
824 pub positions_maintenance_margin: Decimal,
825 #[serde(deserialize_with = "deserialize_decimal")]
827 pub positions_value: Decimal,
828 pub subaccount_id: i64,
830 #[serde(deserialize_with = "deserialize_decimal")]
832 pub subaccount_value: Decimal,
833}
834
835#[derive(Clone, Debug, Serialize, Deserialize)]
838pub struct DeriveTrade {
839 pub direction: DeriveOrderSide,
841 #[serde(deserialize_with = "deserialize_decimal")]
843 pub index_price: Decimal,
844 pub instrument_name: Ustr,
846 pub is_transfer: bool,
848 pub label: Ustr,
850 pub liquidity_role: DeriveLiquidityRole,
852 #[serde(deserialize_with = "deserialize_decimal")]
854 pub mark_price: Decimal,
855 pub order_id: String,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub quote_id: Option<String>,
860 #[serde(deserialize_with = "deserialize_decimal")]
862 pub realized_pnl: Decimal,
863 pub subaccount_id: i64,
865 pub timestamp: i64,
867 #[serde(deserialize_with = "deserialize_decimal")]
869 pub trade_amount: Decimal,
870 #[serde(deserialize_with = "deserialize_decimal")]
872 pub trade_fee: Decimal,
873 pub trade_id: String,
875 #[serde(deserialize_with = "deserialize_decimal")]
877 pub trade_price: Decimal,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub tx_hash: Option<String>,
881 pub tx_status: DeriveTxStatus,
883 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub wallet: Option<Ustr>,
886}
887
888#[derive(Clone, Debug, Serialize, Deserialize)]
897pub struct DerivePublicTrade {
898 pub direction: DeriveOrderSide,
900 #[serde(deserialize_with = "deserialize_decimal")]
902 pub index_price: Decimal,
903 pub instrument_name: Ustr,
905 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub liquidity_role: Option<DeriveLiquidityRole>,
910 #[serde(deserialize_with = "deserialize_decimal")]
912 pub mark_price: Decimal,
913 #[serde(default, skip_serializing_if = "Option::is_none")]
915 pub quote_id: Option<String>,
916 #[serde(default, skip_serializing_if = "Option::is_none")]
918 pub rfq_id: Option<String>,
919 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
921 pub realized_pnl: Option<Decimal>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
924 pub subaccount_id: Option<i64>,
925 pub timestamp: i64,
927 #[serde(deserialize_with = "deserialize_decimal")]
929 pub trade_amount: Decimal,
930 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
932 pub trade_fee: Option<Decimal>,
933 pub trade_id: String,
935 #[serde(deserialize_with = "deserialize_decimal")]
937 pub trade_price: Decimal,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub tx_hash: Option<String>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub tx_status: Option<DeriveTxStatus>,
944 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub wallet: Option<Ustr>,
947}
948
949#[derive(Clone, Debug, Serialize, Deserialize)]
951pub struct DerivePaginationInfo {
952 pub count: i64,
954 pub num_pages: i64,
956}
957
958#[derive(Clone, Debug, Serialize, Deserialize)]
960pub struct DeriveOrdersResult {
961 pub orders: Vec<DeriveOrder>,
964 pub pagination: DerivePaginationInfo,
966 pub subaccount_id: i64,
968}
969
970#[derive(Clone, Debug, Serialize, Deserialize)]
972pub struct DeriveOpenOrdersResult {
973 pub orders: Vec<DeriveOrder>,
976 pub subaccount_id: i64,
978}
979
980#[derive(Clone, Debug, Serialize, Deserialize)]
982pub struct DeriveTradesResult {
983 #[serde(deserialize_with = "deserialize_salvaged_vec")]
985 pub trades: Vec<DeriveTrade>,
986 pub pagination: DerivePaginationInfo,
988 pub subaccount_id: i64,
990}
991
992#[derive(Clone, Debug, Serialize, Deserialize)]
994pub struct DerivePublicTradesResult {
995 pub trades: Vec<DerivePublicTrade>,
997 pub pagination: DerivePaginationInfo,
999}
1000
1001#[derive(Clone, Debug, Serialize, Deserialize)]
1008pub struct DerivePublicCandle {
1009 #[serde(deserialize_with = "deserialize_decimal")]
1011 pub open_price: Decimal,
1012 #[serde(deserialize_with = "deserialize_decimal")]
1014 pub high_price: Decimal,
1015 #[serde(deserialize_with = "deserialize_decimal")]
1017 pub low_price: Decimal,
1018 #[serde(deserialize_with = "deserialize_decimal")]
1020 pub close_price: Decimal,
1021 #[serde(deserialize_with = "deserialize_decimal")]
1023 pub volume_usd: Decimal,
1024 #[serde(deserialize_with = "deserialize_decimal")]
1026 pub volume_contracts: Decimal,
1027 pub timestamp: i64,
1029 pub timestamp_bucket: i64,
1031}
1032
1033#[derive(Clone, Debug, Serialize, Deserialize)]
1035pub struct DerivePublicFundingRate {
1036 #[serde(deserialize_with = "deserialize_decimal")]
1038 pub funding_rate: Decimal,
1039 pub timestamp: i64,
1041}
1042
1043#[derive(Clone, Debug, Serialize, Deserialize)]
1045pub struct DerivePublicFundingRateHistoryResult {
1046 pub funding_rate_history: Vec<DerivePublicFundingRate>,
1048}
1049
1050#[derive(Clone, Debug, Serialize, Deserialize)]
1052pub struct DerivePositionsResult {
1053 pub positions: Vec<DerivePosition>,
1058 pub subaccount_id: i64,
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use std::path::PathBuf;
1065
1066 use rstest::rstest;
1067 use serde_json::{Value, json};
1068
1069 use super::*;
1070
1071 fn data_path() -> PathBuf {
1072 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data")
1073 }
1074
1075 fn load_json(filename: &str) -> Value {
1076 let content = std::fs::read_to_string(data_path().join(filename))
1077 .unwrap_or_else(|_| panic!("failed to read {filename}"));
1078 serde_json::from_str(&content).expect("invalid json")
1079 }
1080
1081 #[rstest]
1082 fn test_request_serializes_with_jsonrpc_version_tag() {
1083 let req = JsonRpcRequest::new(7, "public/get_instruments", json!({"currency": "ETH"}));
1084 let wire = serde_json::to_value(&req).unwrap();
1085 assert_eq!(wire["jsonrpc"], "2.0");
1086 assert_eq!(wire["id"], 7);
1087 assert_eq!(wire["method"], "public/get_instruments");
1088 assert_eq!(wire["params"]["currency"], "ETH");
1089 }
1090
1091 #[rstest]
1092 fn test_response_decodes_success_envelope() {
1093 let body = json!({"id": 1, "result": {"instruments": []}});
1094 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1095 assert_eq!(resp.id, Some(1));
1096 assert!(resp.error.is_none());
1097 assert!(resp.result.is_some());
1098 }
1099
1100 #[rstest]
1101 fn test_response_decodes_error_envelope() {
1102 let body = json!({
1103 "id": 9,
1104 "error": {"code": -32600, "message": "Invalid Request"}
1105 });
1106 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1107 assert_eq!(resp.id, Some(9));
1108 assert!(resp.result.is_none());
1109 let err = resp.error.expect("error present");
1110 assert_eq!(err.code, -32600);
1111 assert_eq!(err.message, "Invalid Request");
1112 assert!(err.data.is_none());
1113 }
1114
1115 #[rstest]
1116 fn test_response_decodes_error_envelope_with_data_field() {
1117 let body = json!({
1118 "id": 9,
1119 "error": {
1120 "code": -32602,
1121 "message": "Invalid params",
1122 "data": {"field": "currency"},
1123 }
1124 });
1125 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1126 let err = resp.error.expect("error present");
1127 assert_eq!(err.data, Some(json!({"field": "currency"})));
1128 }
1129
1130 #[rstest]
1131 fn test_response_tolerates_missing_id() {
1132 let body = json!({"result": {"ok": true}});
1133 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1134 assert!(resp.id.is_none());
1135 assert!(resp.result.is_some());
1136 }
1137
1138 #[rstest]
1139 fn test_response_tolerates_string_id() {
1140 let body = json!({"id": "e3c970c6-94aa-420c-b6db-d0f585a7fde9", "result": {"ok": true}});
1141 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1142 assert!(resp.id.is_none());
1143 assert!(resp.result.is_some());
1144 }
1145
1146 #[rstest]
1147 fn test_response_decodes_numeric_string_id() {
1148 let body = json!({"id": "42", "result": {"ok": true}});
1149 let resp: JsonRpcResponse<Value> = serde_json::from_value(body).unwrap();
1150 assert_eq!(resp.id, Some(42));
1151 assert!(resp.result.is_some());
1152 }
1153
1154 #[rstest]
1155 fn test_instrument_decodes_perp_with_perp_details() {
1156 let body = load_json("perps/instrument_eth.json");
1157 let instrument: DeriveInstrument = serde_json::from_value(body).unwrap();
1158 assert_eq!(instrument.instrument_name, "ETH-PERP");
1159 assert_eq!(instrument.instrument_type, DeriveInstrumentType::Perp);
1160 assert!(instrument.option_details.is_none());
1161 let perp = instrument.perp_details.expect("perp details present");
1162 assert_eq!(perp.index, "ETH-USD");
1163 }
1164
1165 #[rstest]
1166 fn test_instrument_decodes_option_with_option_details() {
1167 let mut body = load_json("options/instrument_eth.json");
1168 body["scheduled_activation"] = json!(0);
1169 let instrument: DeriveInstrument = serde_json::from_value(body).unwrap();
1170 let option = instrument.option_details.expect("option details present");
1171 assert_eq!(option.option_type, DeriveOptionKind::Call);
1172 assert_eq!(option.strike.to_string(), "3500");
1173 assert!(option.settlement_price.is_none());
1174 }
1175
1176 #[rstest]
1177 fn test_order_decodes_partially_filled_market_order() {
1178 let body = load_json("perps/http_order_eth_partially_filled.json");
1182 let order: DeriveOrder = serde_json::from_value(body).unwrap();
1183 let debug = format!("{order:?}");
1184 assert_eq!(order.amount.to_string(), "2.0");
1185 assert_eq!(order.filled_amount.to_string(), "1.5");
1186 assert_eq!(order.average_price.to_string(), "3500.25");
1187 assert_eq!(order.order_status, DeriveOrderStatus::Filled);
1188 assert_eq!(order.cancel_reason, DeriveOrderCancelReason::Empty);
1189 assert_eq!(order.direction, DeriveOrderSide::Buy);
1190 assert_eq!(order.time_in_force, DeriveTimeInForce::Ioc);
1191 assert_eq!(order.order_type, DeriveOrderType::Market);
1192 assert_eq!(order.instrument_name, "ETH-PERP");
1193 assert_eq!(order.label, "alpha-strategy");
1194 assert_eq!(order.signer, "0xsigner");
1195 assert_eq!(order.order_id, "abc-123");
1196 assert_eq!(order.subaccount_id, 42);
1197 assert_eq!(order.signature_expiry_sec, 1_700_001_000);
1198 assert!(!order.mmp);
1199 assert!(!order.is_transfer);
1200 assert!(order.quote_id.is_none());
1201 assert!(order.replaced_order_id.is_none());
1202 assert!(debug.contains(REDACTED));
1203 assert!(!debug.contains("0xdead"));
1204 }
1205
1206 #[rstest]
1207 fn test_replace_result_decodes_canceled_without_replacement() {
1208 let mut cancelled_order = load_json("perps/http_order_eth_partially_filled.json");
1209 cancelled_order["order_id"] = json!("old-order");
1210 cancelled_order["order_status"] = json!("cancelled");
1211 let result: DeriveReplaceResult = serde_json::from_value(json!({
1212 "order": null,
1213 "cancelled_order": cancelled_order,
1214 "create_order_error": {
1215 "code": 10001,
1216 "message": "insufficient margin",
1217 },
1218 }))
1219 .unwrap();
1220
1221 let outcome = result
1222 .into_outcome("old-order", "replacement-label")
1223 .unwrap();
1224 let DeriveReplaceOutcome::Canceled {
1225 cancelled_order,
1226 create_order_error,
1227 } = outcome
1228 else {
1229 panic!("expected canceled outcome");
1230 };
1231 assert_eq!(cancelled_order.order_id, "old-order");
1232 assert_eq!(cancelled_order.order_status, DeriveOrderStatus::Cancelled);
1233 assert_eq!(create_order_error.code, 10001);
1234 assert_eq!(create_order_error.message, "insufficient margin");
1235 assert!(create_order_error.data.is_none());
1236 }
1237
1238 #[rstest]
1239 fn test_replace_result_rejects_non_cancelled_partial_record() {
1240 let mut cancelled_order = load_json("perps/http_order_eth_partially_filled.json");
1241 cancelled_order["order_id"] = json!("old-order");
1242 cancelled_order["order_status"] = json!("open");
1243 let result: DeriveReplaceResult = serde_json::from_value(json!({
1244 "order": null,
1245 "cancelled_order": cancelled_order,
1246 "create_order_error": {
1247 "code": 10001,
1248 "message": "insufficient margin",
1249 },
1250 }))
1251 .unwrap();
1252
1253 let error = result
1254 .into_outcome("old-order", "replacement-label")
1255 .unwrap_err();
1256 assert!(error.contains("had status open"));
1257 }
1258
1259 #[rstest]
1260 fn test_replace_result_rejects_mismatched_replacement_label() {
1261 let mut replacement_order = load_json("perps/http_order_eth_partially_filled.json");
1262 replacement_order["order_id"] = json!("new-order");
1263 replacement_order["order_status"] = json!("open");
1264 replacement_order["label"] = json!("wrong-label");
1265 let result: DeriveReplaceResult = serde_json::from_value(json!({
1266 "order": replacement_order,
1267 "cancelled_order": null,
1268 "create_order_error": null,
1269 }))
1270 .unwrap();
1271
1272 let error = result
1273 .into_outcome("old-order", "expected-label")
1274 .unwrap_err();
1275 assert!(error.contains("had label wrong-label, expected expected-label"));
1276 }
1277
1278 #[rstest]
1279 fn test_position_decodes_perp_with_optional_leverage() {
1280 let body = load_json("perps/http_position_eth.json");
1282 let position: DerivePosition = serde_json::from_value(body).unwrap();
1283 assert_eq!(position.instrument_type, DeriveInstrumentType::Perp);
1284 assert_eq!(position.instrument_name, "ETH-PERP");
1285 assert_eq!(position.amount.to_string(), "-2");
1286 assert_eq!(position.delta.to_string(), "-2");
1287 assert_eq!(position.gamma.to_string(), "0.1");
1288 assert_eq!(position.theta.to_string(), "-0.3");
1289 assert_eq!(position.vega.to_string(), "0.5");
1290 assert_eq!(position.unrealized_pnl.to_string(), "8");
1291 assert_eq!(position.mark_value.to_string(), "-7008");
1292 assert_eq!(
1293 position.leverage.as_ref().map(ToString::to_string),
1294 Some("5.0".into()),
1295 );
1296 assert_eq!(
1297 position.liquidation_price.as_ref().map(ToString::to_string),
1298 Some("4200".into()),
1299 );
1300 }
1301
1302 #[rstest]
1303 fn test_subaccount_decodes_with_collaterals_and_open_orders() {
1304 let body = load_json("common/http_subaccount_usdc.json");
1305 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1306 assert_eq!(subaccount.subaccount_id, 42);
1307 assert_eq!(subaccount.margin_type, DeriveMarginType::Pm);
1308 assert_eq!(subaccount.collaterals.len(), 1);
1309 assert_eq!(subaccount.collaterals[0].asset_type, DeriveAssetType::Erc20);
1310 assert!(!subaccount.is_under_liquidation);
1311 }
1312
1313 #[rstest]
1314 fn test_subaccount_decodes_high_scale_decimal_values() {
1315 let body = load_json("common/http_subaccount_high_scale.json");
1316 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1317 let position = &subaccount.positions[0];
1318
1319 assert_eq!(
1320 subaccount.initial_margin.to_string(),
1321 "0.1234567890123456789012345679",
1322 );
1323 assert_eq!(
1324 subaccount.collaterals[0].amount.to_string(),
1325 "0.1234567890123456789012345679",
1326 );
1327 assert_eq!(
1328 position.pending_funding.to_string(),
1329 "0.1234567890123456789012345679",
1330 );
1331 assert_eq!(
1332 position.leverage.as_ref().map(ToString::to_string),
1333 Some("5.1234567890123456789012345679".into()),
1334 );
1335 assert_eq!(
1336 position.liquidation_price.as_ref().map(ToString::to_string),
1337 Some("4200.1234567890123456789012346".into()),
1338 );
1339
1340 let open_order = &subaccount.open_orders[0];
1342 assert_eq!(
1343 open_order.filled_amount.to_string(),
1344 "0.1234567890123456789012345679",
1345 );
1346 assert_eq!(
1347 open_order.max_fee.to_string(),
1348 "0.1234567890123456789012345679",
1349 );
1350 }
1351
1352 #[rstest]
1353 fn test_subaccount_salvages_unknown_variant_rows() {
1354 let body = load_json("common/http_subaccount_unknown_variants.json");
1355 let subaccount: DeriveSubaccount = serde_json::from_value(body).unwrap();
1356
1357 assert_eq!(subaccount.margin_type, DeriveMarginType::Unknown);
1359 assert_eq!(
1360 subaccount.collaterals[0].asset_type,
1361 DeriveAssetType::Unknown
1362 );
1363 assert_eq!(subaccount.open_orders.len(), 1);
1364 assert_eq!(subaccount.open_orders[0].label, "alpha-strategy");
1365 assert_eq!(subaccount.positions.len(), 1);
1366 assert_eq!(
1367 subaccount.positions[0].instrument_type,
1368 DeriveInstrumentType::Unknown,
1369 );
1370 }
1371
1372 #[rstest]
1373 fn test_public_trade_round_trips() {
1374 let body = load_json("perps/http_public_trade_eth_sell.json");
1375 let trade: DerivePublicTrade = serde_json::from_value(body).unwrap();
1376 assert_eq!(trade.direction, DeriveOrderSide::Sell);
1377 assert_eq!(trade.tx_status, Some(DeriveTxStatus::Settled));
1378 let reserialized = serde_json::to_value(&trade).unwrap();
1379 assert_eq!(reserialized["instrument_name"], "ETH-PERP");
1380 assert_eq!(reserialized["liquidity_role"], "taker");
1381 }
1382
1383 #[rstest]
1384 fn test_orders_result_envelope_decodes() {
1385 let body = json!({
1386 "orders": [],
1387 "pagination": {"count": 0, "num_pages": 0},
1388 "subaccount_id": 42,
1389 });
1390 let result: DeriveOrdersResult = serde_json::from_value(body).unwrap();
1391 assert!(result.orders.is_empty());
1392 assert_eq!(result.subaccount_id, 42);
1393 assert_eq!(result.pagination.count, 0);
1394 }
1395
1396 #[rstest]
1397 fn test_orders_result_decodes_unknown_variant_fields() {
1398 let body = load_json("perps/http_orders_result_eth_unknown_variants.json");
1399 let result: DeriveOrdersResult = serde_json::from_value(body).unwrap();
1400
1401 assert_eq!(result.orders.len(), 2);
1402 assert_eq!(result.orders[0].order_status, DeriveOrderStatus::Open);
1403 let unknowns = &result.orders[1];
1404 assert_eq!(unknowns.order_status, DeriveOrderStatus::Cancelled);
1405 assert_eq!(unknowns.cancel_reason, DeriveOrderCancelReason::Unknown);
1406 assert_eq!(unknowns.order_type, DeriveOrderType::Unknown);
1407 assert_eq!(unknowns.time_in_force, DeriveTimeInForce::Unknown);
1408 assert_eq!(unknowns.trigger_type, Some(DeriveTriggerType::Unknown));
1409 assert_eq!(
1410 unknowns.trigger_price_type,
1411 Some(DeriveTriggerPriceType::Unknown),
1412 );
1413 }
1414
1415 #[rstest]
1416 fn test_orders_result_fails_on_unknown_order_status() {
1417 let mut body = load_json("perps/http_orders_result_eth_unknown_variants.json");
1419 body["orders"][0]["order_status"] = json!("queued");
1420
1421 assert!(serde_json::from_value::<DeriveOrdersResult>(body).is_err());
1422 }
1423
1424 #[rstest]
1425 fn test_positions_result_fails_on_undecodable_row() {
1426 let mut body = load_json("perps/http_positions_result_eth.json");
1428 body["positions"][0]["amount"] = json!({});
1429
1430 assert!(serde_json::from_value::<DerivePositionsResult>(body).is_err());
1431 }
1432
1433 fn perp_ticker_json() -> Value {
1434 load_json("perps/http_ticker_eth_snapshot.json")
1435 }
1436
1437 #[rstest]
1438 fn test_ticker_decodes_perp_snapshot() {
1439 let ticker: DeriveTicker = serde_json::from_value(perp_ticker_json()).unwrap();
1440 assert_eq!(ticker.instrument_name, "ETH-PERP");
1441 assert_eq!(ticker.instrument_type, DeriveInstrumentType::Perp);
1442 assert_eq!(ticker.mark_price.to_string(), "3500.5");
1443 assert_eq!(ticker.best_bid_price.to_string(), "3499.5");
1444 assert_eq!(ticker.best_ask_price.to_string(), "3501.0");
1445 assert_eq!(ticker.timestamp, 1_700_000_000_000);
1446 assert!(ticker.option_details.is_none());
1447 assert!(ticker.option_pricing.is_none());
1448 let perp = ticker.perp_details.expect("perp details present");
1449 assert_eq!(perp.index, "ETH-USD");
1450 assert_eq!(perp.funding_rate.to_string(), "0.0002");
1451 let stats = ticker
1452 .stats
1453 .as_ref()
1454 .expect("WS ticker fixture includes stats");
1455 assert_eq!(stats.contract_volume.to_string(), "12345.6");
1456 assert_eq!(stats.high.to_string(), "3600");
1457 assert_eq!(stats.num_trades.to_string(), "789");
1458 }
1459
1460 #[rstest]
1461 fn test_ticker_decodes_option_snapshot_with_greeks() {
1462 let body = load_json("options/http_ticker_eth_snapshot.json");
1463 let ticker: DeriveTicker = serde_json::from_value(body).unwrap();
1464 assert_eq!(ticker.instrument_type, DeriveInstrumentType::Option);
1465 assert!(ticker.perp_details.is_none());
1466 let option = ticker.option_details.expect("option details present");
1467 assert_eq!(option.option_type, DeriveOptionKind::Call);
1468 assert_eq!(option.strike.to_string(), "3500");
1469 assert!(option.settlement_price.is_none());
1470 let greeks = ticker.option_pricing.expect("option pricing present");
1471 assert_eq!(greeks.delta.to_string(), "0.55");
1472 assert_eq!(greeks.gamma.to_string(), "0.0008");
1473 assert_eq!(greeks.theta.to_string(), "-2.1");
1474 assert_eq!(greeks.vega.to_string(), "4.5");
1475 assert_eq!(greeks.iv.to_string(), "0.60");
1476 assert_eq!(greeks.forward_price.to_string(), "3505");
1477 }
1478
1479 #[rstest]
1480 fn test_private_trade_decodes_with_order_link() {
1481 let body = load_json("perps/http_private_trade_eth.json");
1485 let trade: DeriveTrade = serde_json::from_value(body).unwrap();
1486 assert_eq!(trade.direction, DeriveOrderSide::Buy);
1487 assert_eq!(trade.liquidity_role, DeriveLiquidityRole::Maker);
1488 assert_eq!(trade.tx_status, DeriveTxStatus::Settled);
1489 assert_eq!(trade.instrument_name, "ETH-PERP");
1490 assert_eq!(trade.label, "alpha-strategy");
1491 assert_eq!(trade.wallet.as_ref().map(Ustr::as_str), Some("0xwallet"));
1492 assert_eq!(trade.order_id, "order-abc");
1493 assert_eq!(trade.trade_id, "trade-xyz");
1494 assert_eq!(trade.subaccount_id, 42);
1495 assert_eq!(trade.realized_pnl.to_string(), "12.5");
1496 assert_eq!(trade.trade_amount.to_string(), "0.5");
1497 assert_eq!(trade.trade_price.to_string(), "3499.0");
1498 assert!(!trade.is_transfer);
1499 assert!(trade.quote_id.is_none());
1500 assert_eq!(trade.tx_hash.as_deref(), Some("0xhash"));
1501 }
1502
1503 #[rstest]
1504 fn test_private_trade_decodes_high_scale_decimal_values() {
1505 let mut body = load_json("perps/http_private_trade_eth.json");
1506 body["trade_fee"] = json!("1.234567890123456789012345678912345e-1");
1507 body["realized_pnl"] = json!("0.1234567890123456789012345678912345");
1508
1509 let trade: DeriveTrade = serde_json::from_value(body).unwrap();
1510
1511 assert_eq!(
1512 trade.trade_fee.to_string(),
1513 "0.1234567890123456789012345679"
1514 );
1515 assert_eq!(
1516 trade.realized_pnl.to_string(),
1517 "0.1234567890123456789012345679",
1518 );
1519 }
1520
1521 #[rstest]
1522 fn test_order_result_decodes_pending_trade_with_null_tx_hash() {
1523 let mut body = load_json("spot/http_submit_order_response_mainnet.json");
1524 let mut trade = load_json("perps/http_private_trade_eth.json");
1525 trade["tx_hash"] = Value::Null;
1526 trade["tx_status"] = json!("requested");
1527 trade.as_object_mut().unwrap().remove("wallet");
1528 body["result"]["trades"] = json!([trade]);
1529
1530 let result: DeriveOrderResult =
1531 serde_json::from_value(body["result"].clone()).expect("result decodes");
1532
1533 assert_eq!(result.trades.len(), 1);
1534 assert!(result.trades[0].tx_hash.is_none());
1535 assert_eq!(result.trades[0].tx_status, DeriveTxStatus::Requested);
1536 assert!(result.trades[0].wallet.is_none());
1537 }
1538
1539 #[rstest]
1540 fn test_empty_result_decodes_cancel_ack_shapes() {
1541 let object: DeriveEmptyResult = serde_json::from_value(json!({})).unwrap();
1542 let ok_string: DeriveEmptyResult = serde_json::from_value(json!("ok")).unwrap();
1543 let null_envelope: JsonRpcResponse<DeriveEmptyResult> =
1544 serde_json::from_value(json!({"id": 1, "result": null})).unwrap();
1545
1546 assert_eq!(object, DeriveEmptyResult {});
1547 assert_eq!(ok_string, DeriveEmptyResult {});
1548 assert_eq!(null_envelope.result, Some(DeriveEmptyResult {}));
1549 }
1550
1551 #[rstest]
1552 #[case("common/ws_cancel_by_label_zero.json", 0)]
1553 #[case("common/ws_cancel_by_label_nonzero.json", 2)]
1554 fn test_cancel_order_by_label_result_decodes_count(
1555 #[case] filename: &str,
1556 #[case] expected: i64,
1557 ) {
1558 let response: JsonRpcResponse<DeriveCancelByLabelResult> =
1559 serde_json::from_value(load_json(filename)).expect("response decodes");
1560
1561 assert_eq!(response.result.unwrap().cancelled_orders, expected);
1562 }
1563
1564 #[rstest]
1565 fn test_trades_result_envelope_decodes() {
1566 let body = load_json("perps/http_trades_result_eth.json");
1567 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1568 assert_eq!(result.trades.len(), 1);
1569 assert_eq!(result.subaccount_id, 7);
1570 assert_eq!(result.pagination.count, 1);
1571 assert_eq!(result.pagination.num_pages, 1);
1572 assert_eq!(result.trades[0].trade_id, "t-1");
1573 }
1574
1575 #[rstest]
1576 fn test_trades_result_salvages_unknown_variant_rows() {
1577 let body = load_json("perps/http_trades_result_eth_unknown_variants.json");
1578 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1579
1580 assert_eq!(result.trades.len(), 2);
1582 assert_eq!(result.trades[0].trade_id, "t-1");
1583 let unknowns = &result.trades[1];
1584 assert_eq!(unknowns.trade_id, "t-2");
1585 assert_eq!(unknowns.liquidity_role, DeriveLiquidityRole::Unknown);
1586 }
1587
1588 #[rstest]
1589 fn test_trades_result_drops_unknown_tx_status_rows() {
1590 let mut body = load_json("perps/http_trades_result_eth.json");
1592 body["trades"][0]["tx_status"] = json!("bridging");
1593
1594 let result: DeriveTradesResult = serde_json::from_value(body).unwrap();
1595
1596 assert!(result.trades.is_empty());
1597 }
1598
1599 #[rstest]
1600 fn test_public_trades_result_envelope_decodes() {
1601 let body = load_json("perps/http_public_trades_result_eth.json");
1602 let result: DerivePublicTradesResult = serde_json::from_value(body).unwrap();
1603 assert_eq!(result.trades.len(), 1);
1604 assert_eq!(result.pagination.count, 1);
1605 assert_eq!(result.trades[0].trade_id, "pub-1");
1606 }
1607
1608 #[rstest]
1609 fn test_public_funding_rate_history_result_envelope_decodes() {
1610 let body = load_json("perps/http_public_funding_rate_history_eth.json");
1611 let result: DerivePublicFundingRateHistoryResult = serde_json::from_value(body).unwrap();
1612 assert_eq!(result.funding_rate_history.len(), 3);
1613 let first = &result.funding_rate_history[0];
1614 assert_eq!(first.funding_rate.to_string(), "0.00012");
1615 assert_eq!(first.timestamp, 1_700_000_000_000);
1616 assert_eq!(
1617 result.funding_rate_history.last().unwrap().timestamp,
1618 1_700_007_200_000,
1619 );
1620 }
1621
1622 #[rstest]
1623 fn test_public_candles_decode_array() {
1624 let body = load_json("perps/http_public_candles_eth.json");
1628 let candles: Vec<DerivePublicCandle> = serde_json::from_value(body).unwrap();
1629 assert_eq!(candles.len(), 3);
1630 let first = &candles[0];
1631 assert_eq!(first.open_price.to_string(), "3500.0");
1632 assert_eq!(first.high_price.to_string(), "3501.5");
1633 assert_eq!(first.low_price.to_string(), "3499.0");
1634 assert_eq!(first.close_price.to_string(), "3501.0");
1635 assert_eq!(first.volume_usd.to_string(), "12345.6");
1636 assert_eq!(first.volume_contracts.to_string(), "3.527");
1637 assert_eq!(first.timestamp, 1_700_000_007);
1640 assert_eq!(first.timestamp_bucket, 1_700_000_000);
1641 assert_eq!(candles.last().unwrap().timestamp_bucket, 1_700_001_800);
1642 }
1643
1644 #[rstest]
1645 fn test_positions_result_envelope_decodes() {
1646 let body = load_json("perps/http_positions_result_eth.json");
1647 let result: DerivePositionsResult = serde_json::from_value(body).unwrap();
1648 assert_eq!(result.positions.len(), 1);
1649 assert_eq!(result.subaccount_id, 42);
1650 assert_eq!(result.positions[0].instrument_name, "ETH-PERP");
1651 assert!(result.positions[0].leverage.is_none());
1652 assert!(result.positions[0].liquidation_price.is_none());
1653 }
1654}