1use chrono::{DateTime, Utc};
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11use crate::{Side, TradingModes, WalletAddress};
12
13fn decimal_or_zero<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Decimal, D::Error> {
14 Option::<Decimal>::deserialize(d).map(|o| o.unwrap_or_default())
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
24#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
25pub struct ApiResponse<T> {
26 pub success: bool,
28 pub data: Option<T>,
30 pub error: Option<String>,
32}
33
34impl<T> ApiResponse<T> {
35 pub fn success(data: T) -> Self {
37 Self {
38 success: true,
39 data: Some(data),
40 error: None,
41 }
42 }
43
44 pub fn success_empty() -> Self {
46 Self {
47 success: true,
48 data: None,
49 error: None,
50 }
51 }
52
53 pub fn error(message: impl Into<String>) -> Self {
55 Self {
56 success: false,
57 data: None,
58 error: Some(message.into()),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
70#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
71pub enum InstrumentStatus {
72 #[default]
74 Active,
75 ExpiredPendingPrice,
77 Settled,
79}
80
81impl InstrumentStatus {
82 pub fn from_db_str(s: &str) -> Option<Self> {
84 match s.to_uppercase().as_str() {
85 "ACTIVE" => Some(Self::Active),
86 "EXPIRED_PENDING_PRICE" => Some(Self::ExpiredPendingPrice),
87 "SETTLED" => Some(Self::Settled),
88 _ => None,
89 }
90 }
91
92 pub fn is_active(&self) -> bool {
94 matches!(self, Self::Active)
95 }
96
97 pub fn as_db_str(&self) -> &'static str {
99 match self {
100 Self::Active => "ACTIVE",
101 Self::ExpiredPendingPrice => "EXPIRED_PENDING_PRICE",
102 Self::Settled => "SETTLED",
103 }
104 }
105}
106
107impl std::fmt::Display for InstrumentStatus {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.write_str(self.as_db_str())
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
117#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
118#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
119pub struct Position {
120 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
122 pub wallet_address: WalletAddress,
123 pub symbol: String,
125 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
127 pub amount: Decimal,
128 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
130 pub entry_price: Decimal,
131 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
133 pub margin_posted: Decimal,
134 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
136 pub realized_pnl: Decimal,
137 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
139 pub unrealized_pnl: Decimal,
140 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
142 pub updated_at: DateTime<Utc>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
148pub struct PositionWithMetrics {
149 #[serde(flatten)]
151 pub position: Position,
152 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
154 pub notional_value: Decimal,
155 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
157 pub maintenance_margin: Decimal,
158 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
160 pub liquidation_price: Decimal,
161 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
163 pub margin_ratio: Decimal,
164}
165
166#[derive(Debug, Serialize, Deserialize)]
168#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
169#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
170pub struct AccountBalance {
171 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
173 pub wallet_address: WalletAddress,
174 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
176 pub balance: Decimal,
177 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
179 pub updated_at: DateTime<Utc>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
186#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
187pub struct SpanMarginSummary {
188 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
190 pub equity: Decimal,
191 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
193 pub initial_margin_required: Decimal,
194 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
196 pub maintenance_margin_required: Decimal,
197 #[serde(default)]
199 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
200 pub open_orders_initial_margin: Decimal,
201 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
203 pub option_margin_required: Decimal,
204 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
206 pub scanning_risk: Decimal,
207 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
209 pub option_floor: Decimal,
210 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
212 pub gamma_overlay: Decimal,
213 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
215 pub hypercore_margin_required: Decimal,
216}
217
218pub use crate::responses::MarginSummary;
220
221fn default_margin_mode() -> String {
224 "standard".to_string()
225}
226
227#[derive(Debug, Serialize, Deserialize)]
229#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
230pub struct Portfolio {
231 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
233 pub wallet_address: WalletAddress,
234 pub positions: Vec<PositionWithMetrics>,
236 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
238 pub total_margin_used: Decimal,
239 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
241 pub available_balance: Decimal,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub span_margin: Option<SpanMarginSummary>,
245 #[serde(default = "default_margin_mode")]
247 pub margin_mode: String,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub margin_summary: Option<MarginSummary>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
257#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
258pub struct RiskGridScenario {
259 pub id: String,
261 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
263 pub spot_shock_pct: Decimal,
264 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
266 pub vol_shock_pct: Decimal,
267 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
269 pub pnl_weight: Decimal,
270 pub is_tail: bool,
272 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
274 pub total_pnl: Decimal,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
280pub struct ScenarioDefinition {
281 pub id: String,
283 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
285 pub spot_shock_pct: Decimal,
286 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
288 pub vol_shock_pct: Decimal,
289 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
291 pub pnl_weight: Decimal,
292 pub is_tail: bool,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299pub struct InstrumentRiskRowResponse {
300 pub symbol: String,
302 pub underlying: String,
304 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
306 pub amount: Decimal,
307 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
309 pub base_amount: Decimal,
310 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
312 pub current_value: Decimal,
313 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
315 pub scenario_pnls: Vec<Decimal>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
321pub struct ExtendedRiskMatrixResponse {
322 pub scenarios: Vec<ScenarioDefinition>,
324 pub instruments: Vec<InstrumentRiskRowResponse>,
326 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
328 pub total_pnls: Vec<Decimal>,
329 pub worst_scenario_index: usize,
331 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
333 pub worst_scenario_pnl: Decimal,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
339pub struct RiskGridResponse {
340 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
342 pub equity: Decimal,
343 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
345 pub position_initial_margin: Decimal,
346 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
348 pub position_maintenance_margin: Decimal,
349 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
351 pub open_orders_initial_margin: Decimal,
352 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
354 pub total_initial_margin: Decimal,
355 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
357 pub scanning_risk: Decimal,
358 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
360 pub option_floor: Decimal,
361 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
363 pub gamma_overlay: Decimal,
364 pub scenarios: Vec<RiskGridScenario>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub extended_risk_matrix: Option<ExtendedRiskMatrixResponse>,
369}
370
371#[derive(Debug, Serialize, Deserialize)]
375#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
376pub struct TradeApiResponse {
377 pub trade_id: i64,
379 pub symbol: String,
381 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
383 pub price: Decimal,
384 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
386 pub size: Decimal,
387 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
389 pub maker_address: WalletAddress,
390 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
392 pub taker_address: WalletAddress,
393 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
395 pub maker_fee: Decimal,
396 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
398 pub taker_fee: Decimal,
399 pub timestamp: i64,
401 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
403 pub created_at: DateTime<Utc>,
404}
405
406#[derive(Debug, Serialize, Deserialize)]
408#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
409pub struct TradesResponse {
410 pub success: bool,
412 pub data: Vec<TradeApiResponse>,
414 pub pagination: crate::Pagination,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
421pub struct FillApiResponse {
422 pub fill_id: i64,
424 pub trade_id: i64,
426 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
428 pub wallet_address: WalletAddress,
429 pub symbol: String,
431 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
433 pub price: Decimal,
434 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
436 pub size: Decimal,
437 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
439 pub fee: Decimal,
440 pub is_taker: bool,
442 pub timestamp: i64,
444 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
446 pub created_at: DateTime<Utc>,
447 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
449 pub builder_code_address: Option<WalletAddress>,
450 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
452 pub builder_code_fee: Option<Decimal>,
453 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
455 pub realized_pnl: Option<Decimal>,
456 pub explorer_url: Option<String>,
458}
459
460#[derive(Debug, Serialize, Deserialize)]
462#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
463pub struct FillsResponse {
464 pub success: bool,
466 pub data: Vec<FillApiResponse>,
468 pub pagination: crate::Pagination,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
476#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
477#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
478pub struct Order {
479 pub order_id: i64,
481 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
483 pub wallet_address: WalletAddress,
484 pub symbol: String,
486 pub side: String,
488 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
490 pub price: Decimal,
491 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
493 pub size: Decimal,
494 pub tif: String,
496 pub status: Option<String>,
498 pub created_at: i64,
500 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
502 pub updated_at: Option<DateTime<Utc>>,
503 #[serde(skip_serializing_if = "Option::is_none")]
505 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
506 pub filled_size: Option<Decimal>,
507 #[serde(default)]
509 pub mmp_enabled: bool,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
516#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
517pub struct Instrument {
518 #[serde(default)]
520 pub instrument_id: i32,
521 pub id: String,
523 pub underlying: String,
525 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
527 pub strike: Decimal,
528 pub expiry: u64,
530 pub option_type: String,
532 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
534 pub option_token_address: Option<WalletAddress>,
535 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
537 pub mark_iv: Option<Decimal>,
538 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
540 pub volume_24h: Decimal,
541 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
543 pub open_interest: Decimal,
544 #[serde(default = "chrono::Utc::now")]
546 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
547 pub updated_at: DateTime<Utc>,
548 #[serde(default)]
550 pub status: InstrumentStatus,
551 #[serde(default)]
553 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
554 pub trading_mode: TradingModes,
555}
556
557#[derive(Debug, Serialize, Deserialize)]
561#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
562pub struct MarketInfo {
563 pub underlying: String,
565 pub expiry: u64,
567 #[serde(default, deserialize_with = "decimal_or_zero")]
570 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
571 pub index_price: Decimal,
572 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
574 pub atm_vol: Option<Decimal>,
575 pub instruments: Vec<Instrument>,
577 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
579 pub total_volume_24h: Decimal,
580 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
582 pub total_open_interest: Decimal,
583 #[serde(skip_serializing_if = "Option::is_none")]
585 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
586 pub prev_day_price: Option<Decimal>,
587}
588
589#[derive(Debug, Serialize, Deserialize)]
591#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
592pub struct MarketsResponse {
593 pub success: bool,
595 pub data: Vec<MarketInfo>,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
603#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
604#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
605pub struct OptionsChainGreeksAbs {
606 pub delta: f64,
608 pub gamma: f64,
610 pub theta: f64,
612 pub vega: f64,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
618#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
619#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
620pub struct OptionsChainGreeksCash {
621 pub delta_1pct_usd: f64,
623 pub gamma_1pct_usd: f64,
625 pub theta_1d_usd: f64,
627 pub vega_1vol_usd: f64,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
633#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
634#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
635pub struct OptionsChainLeg {
636 pub symbol: String,
638 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
640 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
641 pub option_token_address: Option<WalletAddress>,
642 pub bid_price_usd: Option<f64>,
644 pub bid_iv: Option<f64>,
646 pub bid_size_contracts: Option<f64>,
648 pub bid_size_usd_notional: Option<f64>,
650 pub ask_price_usd: Option<f64>,
652 pub ask_iv: Option<f64>,
654 pub ask_size_contracts: Option<f64>,
656 pub ask_size_usd_notional: Option<f64>,
658 pub greeks_abs: Option<OptionsChainGreeksAbs>,
660 pub greeks_cash: Option<OptionsChainGreeksCash>,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
666#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
667#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
668pub struct OptionsChainStrikeRow {
669 pub strike: f64,
671 #[serde(skip_serializing_if = "Option::is_none")]
673 pub call: Option<OptionsChainLeg>,
674 #[serde(skip_serializing_if = "Option::is_none")]
676 pub put: Option<OptionsChainLeg>,
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize)]
681#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
682#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
683pub struct OptionsChainSnapshotResponse {
684 pub currency: String,
686 pub expiry: u64,
688 pub rows: Vec<OptionsChainStrikeRow>,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
696#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
697pub struct SimulatedGreeksOrder {
698 pub symbol: String,
700 pub side: Side,
702 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
704 pub size: Decimal,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize)]
709#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
710pub struct PositionGreeksLeg {
711 pub symbol: String,
713 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
715 pub quantity: Decimal,
716 pub delta: f64,
718 pub gamma: f64,
720 pub theta: f64,
722 pub vega: f64,
724 pub iv: f64,
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
730#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
731pub struct PortfolioGreeksAggregate {
732 pub delta: f64,
734 pub gamma: f64,
736 pub theta: f64,
738 pub vega: f64,
740 pub iv: Option<f64>,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
746#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
747pub struct PortfolioGreeksResponse {
748 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
750 pub wallet_address: WalletAddress,
751 #[serde(default)]
753 pub per_leg: Vec<PositionGreeksLeg>,
754 #[serde(skip_serializing_if = "Option::is_none")]
756 pub aggregate: Option<PortfolioGreeksAggregate>,
757}
758
759#[derive(Debug, Serialize, Deserialize)]
763#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
764pub struct HealthResponse {
765 pub status: String,
767}
768
769#[derive(Debug, Serialize, Deserialize)]
771#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
772pub struct VersionResponse {
773 pub version: String,
775 pub commit: String,
777 #[serde(rename = "ref")]
779 pub git_ref: String,
780 pub build_time: String,
782 #[serde(skip_serializing_if = "Option::is_none")]
785 pub signing_chain_id: Option<u64>,
786}
787
788#[derive(Debug, Serialize, Deserialize, Clone)]
790#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
791pub struct ReadinessComponentReport {
792 pub name: String,
794 pub ready: bool,
796 #[serde(skip_serializing_if = "Option::is_none")]
798 pub detail: Option<String>,
799}
800
801#[derive(Debug, Serialize, Deserialize)]
803#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
804pub struct ReadyResponse {
805 pub status: String,
807 #[serde(skip_serializing_if = "Option::is_none")]
809 pub message: Option<String>,
810 pub components: Vec<ReadinessComponentReport>,
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize)]
818#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
819pub struct MmpConfigData {
820 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
822 pub wallet_address: WalletAddress,
823 pub currency: String,
825 pub interval_ms: i64,
827 pub frozen_time_ms: i64,
829 #[serde(skip_serializing_if = "Option::is_none")]
831 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
832 pub qty_limit: Option<Decimal>,
833 #[serde(skip_serializing_if = "Option::is_none")]
835 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
836 pub delta_limit: Option<Decimal>,
837 #[serde(skip_serializing_if = "Option::is_none")]
839 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
840 pub vega_limit: Option<Decimal>,
841 pub enabled: bool,
843}
844
845#[derive(Debug, Serialize, Deserialize)]
847#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
848pub struct SetMmpConfigRequest {
849 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
851 pub wallet: WalletAddress,
852 pub currency: String,
854 pub interval_ms: i64,
856 pub frozen_time_ms: i64,
858 #[serde(skip_serializing_if = "Option::is_none")]
860 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
861 pub qty_limit: Option<Decimal>,
862 #[serde(skip_serializing_if = "Option::is_none")]
864 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
865 pub delta_limit: Option<Decimal>,
866 #[serde(skip_serializing_if = "Option::is_none")]
868 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
869 pub vega_limit: Option<Decimal>,
870 pub nonce: u64,
872 pub signature: String,
874}
875
876#[derive(Debug, Serialize, Deserialize)]
878#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
879pub struct DeleteMmpConfigRequest {
880 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
882 pub wallet: WalletAddress,
883 pub currency: String,
885 pub nonce: u64,
887 pub signature: String,
889}
890
891#[derive(Debug, Serialize, Deserialize)]
893#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
894pub struct ResetMmpRequest {
895 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
897 pub wallet: WalletAddress,
898 pub currency: String,
900 pub nonce: u64,
902 pub signature: String,
904}
905
906#[derive(Debug, Serialize, Deserialize)]
908#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
909pub struct MmpConfigResponse {
910 pub success: bool,
912 pub data: Vec<MmpConfigData>,
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
920#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
921pub struct UserTierData {
922 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
924 pub wallet_address: WalletAddress,
925 pub tier: String,
927}
928
929#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
931#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
932pub struct TradingLimits {
933 pub max_open_orders: i32,
935 pub max_open_positions: i32,
937 pub orders_per_minute: i32,
939 pub cancels_per_minute: i32,
941 pub api_requests_per_minute: i32,
943}
944
945impl Default for TradingLimits {
946 fn default() -> Self {
947 Self {
948 max_open_orders: 100,
949 max_open_positions: 50,
950 orders_per_minute: 60,
951 cancels_per_minute: 120,
952 api_requests_per_minute: 600,
953 }
954 }
955}
956
957#[derive(Debug, Serialize, Deserialize)]
959#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
960pub struct SetUserTierRequest {
961 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
963 pub wallet: WalletAddress,
964 pub tier: String,
966 pub nonce: u64,
968 pub signature: String,
970}
971
972#[derive(Debug, Serialize, Deserialize)]
974#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
975pub struct DeleteUserTierRequest {
976 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
978 pub wallet: WalletAddress,
979 pub nonce: u64,
981 pub signature: String,
983}
984
985#[derive(Debug, Serialize, Deserialize)]
987#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
988pub struct UserTierResponse {
989 pub success: bool,
991 pub data: UserTierData,
993}
994
995#[derive(Debug, Serialize, Deserialize)]
999#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1000pub struct MarginModeResponse {
1001 pub wallet: String,
1003 pub margin_mode: String,
1005 pub previous_mode: String,
1007}
1008
1009#[derive(Debug, Serialize, Deserialize)]
1011#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1012pub struct MarginModeApiResponse {
1013 pub success: bool,
1015 pub data: Option<MarginModeResponse>,
1017 pub error: Option<String>,
1019}
1020
1021#[derive(Debug, Serialize, Deserialize)]
1025#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1026pub struct MonitoringAccountSummary {
1027 pub wallet: String,
1029 pub margin_mode: String,
1031 #[serde(skip_serializing_if = "Option::is_none")]
1033 pub equity: Option<String>,
1034 #[serde(skip_serializing_if = "Option::is_none")]
1036 pub margin_used: Option<String>,
1037 pub position_count: usize,
1039 pub total_notional: String,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub margin_error: Option<String>,
1044}
1045
1046#[derive(Debug, Serialize, Deserialize)]
1048#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1049pub struct MonitoringAccountsResponse {
1050 pub account_count: usize,
1052 pub accounts: Vec<MonitoringAccountSummary>,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1058#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1059pub struct MonitoringPositionHolder {
1060 pub wallet: String,
1062 pub amount: String,
1064 pub entry_price: String,
1066 pub unrealized_pnl: String,
1068}
1069
1070#[derive(Debug, Serialize, Deserialize)]
1072#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1073pub struct MonitoringSymbolPosition {
1074 pub symbol: String,
1076 pub total_long: String,
1078 pub total_short: String,
1080 pub net_position: String,
1082 pub is_balanced: bool,
1084 pub holder_count: usize,
1086 pub holders: Vec<MonitoringPositionHolder>,
1088}
1089
1090#[derive(Debug, Serialize, Deserialize)]
1092#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1093pub struct MonitoringPositionsResponse {
1094 pub symbol_count: usize,
1096 pub symbols: Vec<MonitoringSymbolPosition>,
1098}
1099#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1103#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1104#[serde(rename_all = "lowercase")]
1105pub enum CompetitionStateValue {
1106 Pre,
1108 Active,
1110 Post,
1112}
1113
1114#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1116#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1117#[serde(rename_all = "lowercase")]
1118pub enum CompetitionSortByValue {
1119 Pnl,
1121 Volume,
1123 Efficiency,
1125}
1126
1127#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1129#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1130#[serde(rename_all = "lowercase")]
1131pub enum CompetitionSortOrderValue {
1132 Asc,
1134 Desc,
1136}
1137
1138#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1140#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1141#[serde(rename_all = "lowercase")]
1142pub enum CompetitionWinConditionValue {
1143 Pnl,
1145 Volume,
1147 Efficiency,
1149}
1150
1151#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1154pub struct CompetitionData {
1155 pub id: i64,
1157 pub name: String,
1159 pub description: Option<String>,
1161 pub rules_url: Option<String>,
1163 pub rules_content: Option<String>,
1165 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<CompetitionWinConditionValue>))]
1167 pub win_conditions: Vec<String>,
1168 #[cfg_attr(feature = "utoipa", schema(value_type = CompetitionWinConditionValue))]
1170 pub primary_win_condition: String,
1171 pub start_ts_ms: i64,
1173 pub end_ts_ms: i64,
1175 #[cfg_attr(feature = "utoipa", schema(value_type = CompetitionStateValue))]
1177 pub state: String,
1178 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1180 pub created_at: DateTime<Utc>,
1181 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1183 pub updated_at: DateTime<Utc>,
1184}
1185
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1188#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1189pub struct CompetitionsResponse {
1190 pub success: bool,
1192 pub data: Vec<CompetitionData>,
1194 pub pagination: crate::Pagination,
1196}
1197
1198#[derive(Debug, Clone, Serialize, Deserialize)]
1200#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1201pub struct CompetitionResponse {
1202 pub success: bool,
1204 pub data: CompetitionData,
1206}
1207
1208#[derive(Debug, Clone, Serialize, Deserialize)]
1210#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1211pub struct LeaderboardRow {
1212 pub rank: usize,
1214 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1216 pub wallet: WalletAddress,
1217 pub username: String,
1219 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1221 pub pnl: Decimal,
1222 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1224 pub volume: Decimal,
1225 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1227 pub efficiency: Decimal,
1228 pub medal: Option<u8>,
1230}
1231
1232#[derive(Debug, Clone, Serialize, Deserialize)]
1234#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1235pub struct ConnectedUserRank {
1236 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1238 pub wallet: WalletAddress,
1239 pub username: String,
1241 pub rank: Option<usize>,
1243 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1245 pub pnl: Option<Decimal>,
1246 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1248 pub volume: Option<Decimal>,
1249 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1251 pub efficiency: Option<Decimal>,
1252 pub medal: Option<u8>,
1254}
1255
1256#[derive(Debug, Clone, Serialize, Deserialize)]
1258#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1259pub struct CompetitionLeaderboardResponse {
1260 pub success: bool,
1262 pub competition_id: i64,
1264 #[cfg_attr(feature = "utoipa", schema(value_type = CompetitionSortByValue))]
1266 pub sort_by: String,
1267 #[cfg_attr(feature = "utoipa", schema(value_type = CompetitionSortOrderValue))]
1269 pub sort_order: String,
1270 pub data: Vec<LeaderboardRow>,
1272 pub connected_user: Option<ConnectedUserRank>,
1274 pub pagination: crate::Pagination,
1276}
1277
1278#[derive(Debug, Clone, Serialize, Deserialize)]
1280#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1281pub struct RealizedPnlRow {
1282 pub symbol: String,
1284 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1286 pub realized_pnl: Decimal,
1287 pub event_count: i64,
1289}
1290
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1293#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1294pub struct RealizedPnlResponse {
1295 pub success: bool,
1297 pub data: Vec<RealizedPnlRow>,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize)]
1303#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1304pub struct ProfileMarginStats {
1305 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1307 pub in_use: Decimal,
1308 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1310 pub available: Decimal,
1311 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1313 pub total: Decimal,
1314 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1316 pub deposits: Decimal,
1317 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1319 pub withdraws: Decimal,
1320}
1321
1322#[derive(Debug, Clone, Serialize, Deserialize)]
1324#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1325pub struct ProfilePnlStats {
1326 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1328 pub unrealized: Decimal,
1329 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1331 pub pnl_24h: Decimal,
1332 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1334 pub lifetime_realized: Decimal,
1335}
1336
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1339#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1340pub struct ProfileCompetitionRankSummary {
1341 pub competition_id: i64,
1343 pub competition_name: String,
1345 #[cfg_attr(feature = "utoipa", schema(value_type = CompetitionStateValue))]
1347 pub competition_state: String,
1348 pub rank: usize,
1350 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1352 pub pnl: Decimal,
1353 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1355 pub volume: Decimal,
1356 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1358 pub efficiency: Decimal,
1359 pub medal: Option<u8>,
1361}
1362
1363#[derive(Debug, Clone, Serialize, Deserialize)]
1365#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1366pub struct ProfileMetricMedals {
1367 pub pnl: Option<u8>,
1369 pub volume: Option<u8>,
1371 pub efficiency: Option<u8>,
1373}
1374
1375#[derive(Debug, Clone, Serialize, Deserialize)]
1377#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1378pub struct ProfileData {
1379 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1381 pub wallet: WalletAddress,
1382 pub username: String,
1384 pub profile_image_url: Option<String>,
1386 pub account_first_seen_ts_ms: Option<i64>,
1388 pub account_age_days: Option<i64>,
1390 pub margin: ProfileMarginStats,
1392 pub pnl: ProfilePnlStats,
1394 pub medal: Option<u8>,
1396 pub platform_medals: ProfileMetricMedals,
1398 #[serde(skip_serializing_if = "Option::is_none")]
1400 pub active_competition_rank: Option<ProfileCompetitionRankSummary>,
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize)]
1405#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1406pub struct ProfileResponse {
1407 pub success: bool,
1409 pub data: ProfileData,
1411}
1412
1413#[derive(Debug, Clone, Serialize, Deserialize)]
1415#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1416pub struct ProfileTradesResponse {
1417 pub success: bool,
1419 pub data: Vec<FillApiResponse>,
1421 pub pagination: crate::Pagination,
1423}
1424
1425#[derive(Debug, Clone, Serialize, Deserialize)]
1427#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1428pub struct CompetitionUpsertRequest {
1429 pub name: String,
1431 pub description: Option<String>,
1433 pub rules_url: Option<String>,
1435 pub rules_content: Option<String>,
1437 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<CompetitionWinConditionValue>, min_items = 1))]
1439 pub win_conditions: Vec<String>,
1440 #[cfg_attr(feature = "utoipa", schema(value_type = CompetitionWinConditionValue))]
1442 pub primary_win_condition: String,
1443 pub start_ts_ms: i64,
1445 pub end_ts_ms: i64,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize)]
1454#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1455pub struct CompetitionUpdateRequest {
1456 pub name: Option<String>,
1458 pub description: Option<Option<String>>,
1460 pub rules_url: Option<Option<String>>,
1462 pub rules_content: Option<Option<String>>,
1464 #[cfg_attr(feature = "utoipa", schema(value_type = Option<Vec<CompetitionWinConditionValue>>, min_items = 1))]
1466 pub win_conditions: Option<Vec<String>>,
1467 #[cfg_attr(feature = "utoipa", schema(value_type = Option<CompetitionWinConditionValue>))]
1469 pub primary_win_condition: Option<String>,
1470 pub start_ts_ms: Option<i64>,
1472 pub end_ts_ms: Option<i64>,
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478 use super::*;
1479 use rust_decimal_macros::dec;
1480
1481 #[test]
1482 fn api_response_preserves_null_fields() {
1483 let resp: ApiResponse<String> = ApiResponse::error("something broke".to_string());
1484 let json = serde_json::to_value(&resp).unwrap();
1485 assert_eq!(json["success"], false);
1486 assert!(
1487 json.get("data").is_some(),
1488 "data field must be present (null, not omitted)"
1489 );
1490 assert!(json["data"].is_null());
1491 assert_eq!(json["error"], "something broke");
1492 }
1493
1494 #[test]
1495 fn api_response_success_includes_data() {
1496 let resp = ApiResponse::success("hello".to_string());
1497 let json = serde_json::to_value(&resp).unwrap();
1498 assert_eq!(json["success"], true);
1499 assert_eq!(json["data"], "hello");
1500 assert!(json["error"].is_null());
1501 }
1502
1503 #[test]
1504 fn api_response_roundtrip() {
1505 let resp = ApiResponse::success(42u32);
1506 let json_str = serde_json::to_string(&resp).unwrap();
1507 let parsed: ApiResponse<u32> = serde_json::from_str(&json_str).unwrap();
1508 assert!(parsed.success);
1509 assert_eq!(parsed.data, Some(42));
1510 }
1511
1512 #[test]
1513 fn profile_image_url_serde_contract() {
1514 for profile_image_url in [
1515 serde_json::json!("https://example.com/profile.png"),
1516 serde_json::Value::Null,
1517 ] {
1518 let input = serde_json::json!({
1519 "wallet": "0x0000000000000000000000000000000000000000",
1520 "username": "alice",
1521 "profile_image_url": profile_image_url,
1522 "account_first_seen_ts_ms": null,
1523 "account_age_days": null,
1524 "margin": {
1525 "in_use": "0",
1526 "available": "0",
1527 "total": "0",
1528 "deposits": "0",
1529 "withdraws": "0"
1530 },
1531 "pnl": {
1532 "unrealized": "0",
1533 "pnl_24h": "0",
1534 "lifetime_realized": "0"
1535 },
1536 "medal": null,
1537 "platform_medals": {
1538 "pnl": null,
1539 "volume": null,
1540 "efficiency": null
1541 },
1542 "active_competition_rank": null
1543 });
1544
1545 let profile: ProfileData = serde_json::from_value(input.clone()).unwrap();
1546 let output = serde_json::to_value(&profile).unwrap();
1547 assert_eq!(output["profile_image_url"], input["profile_image_url"]);
1548 }
1549 }
1550
1551 #[test]
1552 fn decimal_fields_serialize_as_strings() {
1553 let position = Position {
1554 wallet_address: WalletAddress::default(),
1555 symbol: "BTC-20260101-100000-C".to_string(),
1556 amount: dec!(1.5),
1557 entry_price: dec!(2500),
1558 margin_posted: dec!(100),
1559 realized_pnl: dec!(-50),
1560 unrealized_pnl: dec!(200),
1561 updated_at: chrono::Utc::now(),
1562 };
1563 let json = serde_json::to_value(&position).unwrap();
1564 assert_eq!(json["amount"], "1.5");
1565 assert_eq!(json["entry_price"], "2500");
1566 assert_eq!(json["realized_pnl"], "-50");
1567 }
1568
1569 #[test]
1570 fn decimal_fields_deserialize_from_strings() {
1571 let json = serde_json::json!({
1572 "wallet_address": "0x0000000000000000000000000000000000000000",
1573 "symbol": "BTC-20260101-100000-C",
1574 "amount": "1.5",
1575 "entry_price": "2500",
1576 "margin_posted": "100",
1577 "realized_pnl": "-50",
1578 "unrealized_pnl": "200",
1579 "updated_at": "2026-01-01T00:00:00Z"
1580 });
1581 let position: Position = serde_json::from_value(json).unwrap();
1582 assert_eq!(position.amount, dec!(1.5));
1583 assert_eq!(position.entry_price, dec!(2500));
1584 }
1585
1586 #[test]
1587 fn decimal_fields_deserialize_from_serde_json_value() {
1588 let json = serde_json::json!({
1589 "wallet_address": "0x0000000000000000000000000000000000000000",
1590 "symbol": "BTC-20260101-100000-C",
1591 "amount": "1.5",
1592 "entry_price": "2500",
1593 "margin_posted": "100",
1594 "realized_pnl": "-50",
1595 "unrealized_pnl": "200",
1596 "updated_at": "2026-01-01T00:00:00Z"
1597 });
1598 let pos: Position = serde_json::from_value(json).unwrap();
1599 assert_eq!(pos.amount, dec!(1.5));
1600 assert_eq!(pos.entry_price, dec!(2500));
1601 }
1602
1603 #[test]
1604 fn position_with_metrics_flattens_position_fields() {
1605 let pwm = PositionWithMetrics {
1606 position: Position {
1607 wallet_address: WalletAddress::default(),
1608 symbol: "ETH-20260301-5000-P".to_string(),
1609 amount: dec!(-3),
1610 entry_price: dec!(150),
1611 margin_posted: dec!(50),
1612 realized_pnl: dec!(0),
1613 unrealized_pnl: dec!(-10),
1614 updated_at: chrono::Utc::now(),
1615 },
1616 notional_value: dec!(-450),
1617 maintenance_margin: dec!(0),
1618 liquidation_price: dec!(0),
1619 margin_ratio: dec!(0),
1620 };
1621 let json = serde_json::to_value(&pwm).unwrap();
1622 assert_eq!(json["symbol"], "ETH-20260301-5000-P");
1624 assert_eq!(json["amount"], "-3");
1625 assert_eq!(json["notional_value"], "-450");
1627 assert!(json.get("position").is_none());
1629 }
1630
1631 #[test]
1632 fn position_with_metrics_roundtrip_from_flat_json() {
1633 let json = serde_json::json!({
1634 "wallet_address": "0x0000000000000000000000000000000000000001",
1635 "symbol": "BTC-20260101-100000-C",
1636 "amount": "2",
1637 "entry_price": "3000",
1638 "margin_posted": "100",
1639 "realized_pnl": "0",
1640 "unrealized_pnl": "50",
1641 "updated_at": "2026-01-01T00:00:00Z",
1642 "notional_value": "6000",
1643 "maintenance_margin": "0",
1644 "liquidation_price": "0",
1645 "margin_ratio": "0"
1646 });
1647 let pwm: PositionWithMetrics = serde_json::from_value(json).unwrap();
1648 assert_eq!(pwm.position.symbol, "BTC-20260101-100000-C");
1649 assert_eq!(pwm.position.amount, dec!(2));
1650 assert_eq!(pwm.notional_value, dec!(6000));
1651 }
1652
1653 #[test]
1654 fn instrument_status_serde_roundtrip() {
1655 for status in [
1656 InstrumentStatus::Active,
1657 InstrumentStatus::ExpiredPendingPrice,
1658 InstrumentStatus::Settled,
1659 ] {
1660 let json = serde_json::to_string(&status).unwrap();
1661 let parsed: InstrumentStatus = serde_json::from_str(&json).unwrap();
1662 assert_eq!(parsed, status);
1663 }
1664 }
1665
1666 #[test]
1667 fn instrument_status_serializes_screaming_snake() {
1668 assert_eq!(
1669 serde_json::to_string(&InstrumentStatus::ExpiredPendingPrice).unwrap(),
1670 "\"EXPIRED_PENDING_PRICE\""
1671 );
1672 }
1673
1674 #[test]
1675 fn portfolio_serialization_omits_none_fields() {
1676 let portfolio = Portfolio {
1677 wallet_address: WalletAddress::default(),
1678 positions: vec![],
1679 total_margin_used: dec!(0),
1680 available_balance: dec!(1000),
1681 span_margin: None,
1682 margin_mode: "standard".to_string(),
1683 margin_summary: None,
1684 };
1685 let json = serde_json::to_value(&portfolio).unwrap();
1686 assert!(
1687 json.get("span_margin").is_none(),
1688 "None span_margin should be omitted"
1689 );
1690 assert!(
1691 json.get("margin_summary").is_none(),
1692 "None margin_summary should be omitted"
1693 );
1694 assert_eq!(json["margin_mode"], "standard");
1695 }
1696
1697 #[test]
1698 fn portfolio_deserializes_without_optional_fields() {
1699 let json = serde_json::json!({
1700 "wallet_address": "0x0000000000000000000000000000000000000000",
1701 "positions": [],
1702 "total_margin_used": "0",
1703 "available_balance": "1000"
1704 });
1705 let portfolio: Portfolio = serde_json::from_value(json).unwrap();
1706 assert_eq!(portfolio.margin_mode, "standard");
1707 assert!(portfolio.span_margin.is_none());
1708 }
1709
1710 #[test]
1711 fn markets_response_roundtrip() {
1712 let resp = MarketsResponse {
1713 success: true,
1714 data: vec![MarketInfo {
1715 underlying: "BTC".to_string(),
1716 expiry: 1735689600,
1717 index_price: dec!(95000),
1718 atm_vol: Some(dec!(0.65)),
1719 instruments: vec![Instrument {
1720 instrument_id: 1,
1721 id: "BTC-20260101-100000-C".to_string(),
1722 underlying: "BTC".to_string(),
1723 strike: dec!(100000),
1724 expiry: 1735689600,
1725 option_type: "call".to_string(),
1726 option_token_address: None,
1727 mark_iv: Some(dec!(0.70)),
1728 volume_24h: dec!(1500),
1729 open_interest: dec!(25000),
1730 updated_at: chrono::Utc::now(),
1731 status: InstrumentStatus::Active,
1732 trading_mode: TradingModes::default(),
1733 }],
1734 total_volume_24h: dec!(1500),
1735 total_open_interest: dec!(25000),
1736 prev_day_price: None,
1737 }],
1738 };
1739 let json_str = serde_json::to_string(&resp).unwrap();
1740 let parsed: MarketsResponse = serde_json::from_str(&json_str).unwrap();
1741 assert!(parsed.success);
1742 assert_eq!(parsed.data.len(), 1);
1743 assert_eq!(parsed.data[0].instruments[0].strike, dec!(100000));
1744 assert_eq!(
1745 parsed.data[0].instruments[0].status,
1746 InstrumentStatus::Active
1747 );
1748 }
1749
1750 #[test]
1751 fn risk_grid_decimal_precision_preserved() {
1752 let scenario = RiskGridScenario {
1753 id: "S1".to_string(),
1754 spot_shock_pct: dec!(-0.15),
1755 vol_shock_pct: dec!(0.30),
1756 pnl_weight: dec!(1.00),
1757 is_tail: false,
1758 total_pnl: dec!(-1234.56789),
1759 };
1760 let json = serde_json::to_value(&scenario).unwrap();
1761 assert_eq!(json["spot_shock_pct"], "-0.15");
1762 assert_eq!(json["total_pnl"], "-1234.56789");
1763
1764 let parsed: RiskGridScenario = serde_json::from_value(json).unwrap();
1765 assert_eq!(parsed.total_pnl, dec!(-1234.56789));
1766 }
1767
1768 #[test]
1769 fn span_margin_summary_open_orders_defaults_to_zero() {
1770 let json = serde_json::json!({
1771 "equity": "10000",
1772 "initial_margin_required": "1500",
1773 "maintenance_margin_required": "1275",
1774 "option_margin_required": "1500",
1775 "scanning_risk": "1200",
1776 "option_floor": "1500",
1777 "gamma_overlay": "250",
1778 "hypercore_margin_required": "0"
1779 });
1780 let summary: SpanMarginSummary = serde_json::from_value(json).unwrap();
1781 assert_eq!(summary.open_orders_initial_margin, dec!(0));
1782 }
1783
1784 #[test]
1785 fn fill_api_response_explorer_url_omission() {
1786 let fill = FillApiResponse {
1787 fill_id: 1,
1788 trade_id: 100,
1789 wallet_address: WalletAddress::default(),
1790 symbol: "BTC-20260101-100000-C".to_string(),
1791 price: dec!(2500),
1792 size: dec!(1),
1793 fee: dec!(2.5),
1794 is_taker: true,
1795 timestamp: 1700000000000,
1796 created_at: chrono::Utc::now(),
1797 builder_code_address: None,
1798 builder_code_fee: None,
1799 realized_pnl: Some(dec!(150)),
1800 explorer_url: None,
1801 };
1802 let json = serde_json::to_value(&fill).unwrap();
1803 assert_eq!(json["price"], "2500");
1804 assert_eq!(json["realized_pnl"], "150");
1805 }
1806
1807 #[test]
1808 fn health_response_roundtrip() {
1809 let resp = HealthResponse {
1810 status: "ok".to_string(),
1811 };
1812 let json_str = serde_json::to_string(&resp).unwrap();
1813 let parsed: HealthResponse = serde_json::from_str(&json_str).unwrap();
1814 assert_eq!(parsed.status, "ok");
1815 }
1816
1817 #[test]
1818 fn competition_enums_serialize_lowercase() {
1819 assert_eq!(
1820 serde_json::to_string(&CompetitionStateValue::Active).unwrap(),
1821 "\"active\""
1822 );
1823 assert_eq!(
1824 serde_json::to_string(&CompetitionSortByValue::Pnl).unwrap(),
1825 "\"pnl\""
1826 );
1827 assert_eq!(
1828 serde_json::to_string(&CompetitionSortOrderValue::Desc).unwrap(),
1829 "\"desc\""
1830 );
1831 }
1832
1833 #[test]
1834 fn options_chain_leg_all_none_fields() {
1835 let leg = OptionsChainLeg {
1836 symbol: "BTC-20260101-100000-C".to_string(),
1837 option_token_address: None,
1838 bid_price_usd: None,
1839 bid_iv: None,
1840 bid_size_contracts: None,
1841 bid_size_usd_notional: None,
1842 ask_price_usd: None,
1843 ask_iv: None,
1844 ask_size_contracts: None,
1845 ask_size_usd_notional: None,
1846 greeks_abs: None,
1847 greeks_cash: None,
1848 };
1849 let json = serde_json::to_value(&leg).unwrap();
1850 assert_eq!(json["symbol"], "BTC-20260101-100000-C");
1851 assert!(json["bid_price_usd"].is_null());
1852 }
1853
1854 #[test]
1855 fn mmp_config_data_roundtrip() {
1856 let config = MmpConfigData {
1857 wallet_address: WalletAddress::default(),
1858 currency: "BTC".to_string(),
1859 interval_ms: 5000,
1860 frozen_time_ms: 10000,
1861 qty_limit: Some(dec!(100)),
1862 delta_limit: None,
1863 vega_limit: Some(dec!(50000)),
1864 enabled: true,
1865 };
1866 let json_str = serde_json::to_string(&config).unwrap();
1867 let parsed: MmpConfigData = serde_json::from_str(&json_str).unwrap();
1868 assert_eq!(parsed.qty_limit, Some(dec!(100)));
1869 assert_eq!(parsed.delta_limit, None);
1870 assert!(parsed.enabled);
1871 }
1872}
1873
1874#[derive(Debug, Serialize, Deserialize)]
1876#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1877pub struct ExchangeInfoResponse {
1878 pub exchange_address: String,
1880 pub chain_id: u64,
1882 pub signing_domain: SigningDomainInfo,
1884}
1885
1886#[derive(Debug, Serialize, Deserialize)]
1888#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1889pub struct SigningDomainInfo {
1890 pub name: String,
1892 pub version: String,
1894}
1895
1896#[derive(Debug, Serialize, Deserialize)]
1898#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1899pub struct WithdrawalHistoryResponse {
1900 pub withdrawals: Vec<DirectiveStatusResponse>,
1902}
1903
1904#[derive(Debug, Serialize, Deserialize)]
1906#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1907pub struct DirectiveStatusResponse {
1908 pub directive_id: String,
1910 pub action_key: String,
1912 pub domain_status: String,
1914 pub delivery_status: String,
1916 pub tx_hash: Option<String>,
1918 pub created_at: Option<String>,
1920}