Skip to main content

hypercall_sdk_types/
responses.rs

1//! API response types.
2
3use crate::{
4    FillSource, MarketUpdateStatus, OptionType, OrderStatus, OrderUpdateStatus, RfqStatus, Side,
5    TimeInForce, TradeSide, WalletAddress,
6};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Deserializer, Serialize};
9
10fn default_true() -> bool {
11    true
12}
13
14/// Deserialize a string as Decimal.
15/// The API always returns Decimal values as JSON strings (e.g., "0.5", "10000").
16fn string_decimal<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
17where
18    D: Deserializer<'de>,
19{
20    use serde::de;
21
22    struct StringDecimalVisitor;
23
24    impl<'de> de::Visitor<'de> for StringDecimalVisitor {
25        type Value = Decimal;
26
27        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
28            formatter.write_str("a string representing Decimal")
29        }
30
31        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
32        where
33            E: de::Error,
34        {
35            v.parse::<Decimal>().map_err(de::Error::custom)
36        }
37    }
38
39    deserializer.deserialize_str(StringDecimalVisitor)
40}
41
42/// Deserialize an optional string as Option<Decimal>.
43/// The API always returns Decimal values as JSON strings (e.g., "0.5", "10000").
44fn option_string_decimal<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>
45where
46    D: Deserializer<'de>,
47{
48    use serde::de;
49
50    struct OptionStringDecimalVisitor;
51
52    impl<'de> de::Visitor<'de> for OptionStringDecimalVisitor {
53        type Value = Option<Decimal>;
54
55        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
56            formatter.write_str("null or a string representing Decimal")
57        }
58
59        fn visit_none<E>(self) -> Result<Self::Value, E>
60        where
61            E: de::Error,
62        {
63            Ok(None)
64        }
65
66        fn visit_unit<E>(self) -> Result<Self::Value, E>
67        where
68            E: de::Error,
69        {
70            Ok(None)
71        }
72
73        fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
74        where
75            D2: Deserializer<'de>,
76        {
77            string_decimal(deserializer).map(Some)
78        }
79    }
80
81    deserializer.deserialize_option(OptionStringDecimalVisitor)
82}
83
84/// Deserialize a string or number as f64, defaulting to 0.0 if missing.
85fn string_or_f64_default<'de, D>(deserializer: D) -> Result<f64, D::Error>
86where
87    D: Deserializer<'de>,
88{
89    use serde::de;
90
91    struct StringOrF64DefaultVisitor;
92
93    impl<'de> de::Visitor<'de> for StringOrF64DefaultVisitor {
94        type Value = f64;
95
96        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
97            formatter.write_str("a string, number, or null representing f64")
98        }
99
100        fn visit_none<E>(self) -> Result<Self::Value, E>
101        where
102            E: de::Error,
103        {
104            Ok(0.0)
105        }
106
107        fn visit_unit<E>(self) -> Result<Self::Value, E>
108        where
109            E: de::Error,
110        {
111            Ok(0.0)
112        }
113
114        fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
115        where
116            E: de::Error,
117        {
118            Ok(v)
119        }
120
121        fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
122        where
123            E: de::Error,
124        {
125            Ok(v as f64)
126        }
127
128        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
129        where
130            E: de::Error,
131        {
132            Ok(v as f64)
133        }
134
135        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
136        where
137            E: de::Error,
138        {
139            v.parse::<f64>().map_err(de::Error::custom)
140        }
141    }
142
143    deserializer.deserialize_any(StringOrF64DefaultVisitor)
144}
145
146/// Generic API response wrapper.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ApiResponse<T> {
149    pub success: bool,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub data: Option<T>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub error: Option<String>,
154}
155
156impl<T> ApiResponse<T> {
157    pub fn success(data: T) -> Self {
158        Self {
159            success: true,
160            data: Some(data),
161            error: None,
162        }
163    }
164
165    pub fn error(message: impl Into<String>) -> Self {
166        Self {
167            success: false,
168            data: None,
169            error: Some(message.into()),
170        }
171    }
172}
173
174/// Order information.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
177pub struct OrderInfo {
178    /// Instrument symbol
179    pub symbol: String,
180    /// Order price
181    #[serde(deserialize_with = "string_decimal")]
182    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
183    pub price: Decimal,
184    /// Order size in raw units
185    #[serde(deserialize_with = "string_decimal")]
186    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
187    pub size: Decimal,
188    /// Order side
189    pub side: Side,
190    /// Time in force
191    pub tif: TimeInForce,
192    /// Client-provided order ID
193    pub client_id: Option<String>,
194    /// Exchange-assigned order ID
195    pub order_id: Option<u64>,
196    /// Whether this is a perp order (required - must be explicitly set)
197    pub is_perp: bool,
198    /// Underlying asset for perp orders
199    pub underlying: Option<String>,
200    /// Reduce-only flag for perp orders
201    pub reduce_only: Option<bool>,
202    /// Nonce for signature verification
203    pub nonce: Option<u64>,
204    /// EIP-712 signature
205    pub signature: Option<String>,
206    /// Whether MMP is enabled
207    #[serde(default)]
208    pub mmp_enabled: bool,
209    /// Optional builder code address for fee rebates
210    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
211    pub builder_code_address: Option<WalletAddress>,
212}
213
214/// Order message (response from order placement).
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct OrderMessage {
217    pub order_id: Option<u64>,
218    #[serde(alias = "request")]
219    pub info: OrderInfo,
220    pub status: OrderStatus,
221    pub timestamp: u64,
222    pub reason: Option<String>,
223    #[serde(
224        default,
225        skip_serializing_if = "Option::is_none",
226        deserialize_with = "option_string_decimal"
227    )]
228    pub filled_size: Option<Decimal>,
229    #[serde(alias = "account")]
230    pub wallet_address: WalletAddress,
231}
232
233/// Order update message.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
236pub struct OrderUpdateMessage {
237    /// Timestamp in milliseconds
238    pub timestamp: u64,
239    /// Order information
240    pub info: OrderInfo,
241    /// Current order status
242    pub status: OrderUpdateStatus,
243    /// Reason for rejection or cancellation
244    pub reason: Option<String>,
245    /// Amount filled so far
246    #[serde(deserialize_with = "string_decimal")]
247    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
248    pub filled_size: Decimal,
249    /// Exchange-assigned order ID
250    pub order_id: Option<u64>,
251    /// Wallet address
252    #[serde(alias = "wallet", alias = "account")]
253    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
254    pub wallet_address: WalletAddress,
255    /// Whether MMP triggered this update
256    #[serde(default)]
257    pub mmp_triggered: bool,
258    /// Request ID for correlating this update with the original command.
259    /// Populated from the triggering OrderActionMessage's request_id.
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub request_id: Option<String>,
262}
263
264/// Market information.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
267pub struct Market {
268    /// Market symbol (e.g., "BTC-20250131-100000-C")
269    pub symbol: String,
270    /// Underlying asset (e.g., "BTC")
271    pub underlying: String,
272    /// Expiry timestamp
273    pub expiry: u64,
274    /// Strike price
275    #[serde(deserialize_with = "string_decimal")]
276    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
277    pub strike: Decimal,
278    /// Option type (Call or Put)
279    pub option_type: OptionType,
280}
281
282/// Market update message.
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
285pub struct MarketUpdateMessage {
286    /// Market information
287    pub market: Market,
288    /// Update status
289    pub status: MarketUpdateStatus,
290    /// Timestamp in milliseconds
291    pub timestamp: u64,
292    /// Reason for failure (if status is MarketCreationFailed or MarketDeletionFailed)
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub reason: Option<String>,
295}
296
297/// Simple market response (for create/delete).
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
300pub struct MarketResponse {
301    pub success: bool,
302    pub message: String,
303}
304
305/// Fill (trade execution) information.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct Fill {
308    pub trade_id: u64,
309    pub taker_order_id: u64,
310    pub maker_order_id: u64,
311    pub symbol: String,
312    #[serde(deserialize_with = "string_decimal")]
313    pub price: Decimal,
314    /// Size in raw units (multiply by CONTRACT_UNIT_MULTIPLIER for human-readable)
315    #[serde(deserialize_with = "string_decimal")]
316    pub size: Decimal,
317    pub taker_side: Side,
318    pub taker_wallet_address: WalletAddress,
319    pub maker_wallet_address: WalletAddress,
320    #[serde(deserialize_with = "string_decimal")]
321    pub fee: Decimal,
322    pub is_taker: bool,
323    pub timestamp: u64,
324    /// Optional builder code address for fee rebates
325    pub builder_code_address: Option<WalletAddress>,
326    /// Fee paid to builder code (deducted from platform revenue)
327    #[serde(default)]
328    pub builder_code_fee: Option<Decimal>,
329    /// Source of the fill (orderbook or RFQ). Defaults to Orderbook for backward compatibility.
330    #[serde(default)]
331    pub source: FillSource,
332    /// Realized PnL for the taker from this fill (set by journal side-effect calculation).
333    /// Only present for position-reducing fills; None for position-opening fills.
334    #[serde(default)]
335    pub taker_realized_pnl: Option<Decimal>,
336    /// Realized PnL for the maker from this fill.
337    #[serde(default)]
338    pub maker_realized_pnl: Option<Decimal>,
339    /// Underlying notional captured at fill time for option analytics.
340    #[serde(default)]
341    pub underlying_notional: Option<Decimal>,
342}
343
344/// Orderbook snapshot.
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct OrderbookUpdate {
347    pub symbol: String,
348    pub bids: Vec<(Decimal, Decimal)>,
349    pub asks: Vec<(Decimal, Decimal)>,
350    pub timestamp: u64,
351}
352
353/// L2 update (price level change).
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct L2Update {
356    #[serde(deserialize_with = "string_decimal")]
357    pub price: Decimal,
358    #[serde(deserialize_with = "string_decimal")]
359    pub size: Decimal, // Size remaining at this price level, 0 to remove the level
360}
361
362/// L2 message (orderbook delta).
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct L2Message {
365    pub symbol: String,
366    pub bid_updates: Vec<L2Update>,
367    pub ask_updates: Vec<L2Update>,
368    pub timestamp: u64,
369    #[serde(default)]
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub sequence: Option<i64>,
372}
373
374/// Trade message.
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct TradeMessage {
377    pub symbol: String,
378    #[serde(deserialize_with = "string_decimal")]
379    pub price: Decimal,
380    #[serde(deserialize_with = "string_decimal")]
381    pub size: Decimal,
382    pub side: TradeSide,
383    pub timestamp: u64,
384}
385
386/// Result for a single order in a bulk operation.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct BulkOrderResult {
389    pub index: usize,
390    pub success: bool,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub data: Option<OrderMessage>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub error: Option<String>,
395}
396
397/// Response from bulk order placement.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct BulkPlaceOrderResponse {
400    pub results: Vec<BulkOrderResult>,
401}
402
403/// Response from bulk cancel operation.
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct BulkCancelOrderResponse {
406    pub results: Vec<BulkOrderResult>,
407}
408
409/// Pagination info for list responses.
410#[derive(Debug, Clone, Serialize, Deserialize)]
411#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
412pub struct Pagination {
413    pub limit: usize,
414    pub offset: usize,
415    pub count: usize,
416}
417
418/// Competition leaderboard row.
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
421pub struct CompetitionLeaderboardRow {
422    pub rank: usize,
423    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
424    pub wallet: WalletAddress,
425    pub username: String,
426    #[serde(deserialize_with = "string_decimal")]
427    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
428    pub pnl: Decimal,
429    #[serde(deserialize_with = "string_decimal")]
430    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
431    pub volume: Decimal,
432    #[serde(deserialize_with = "string_decimal")]
433    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
434    pub efficiency: Decimal,
435    pub medal: Option<u8>,
436}
437
438/// Connected wallet summary on a competition leaderboard response.
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
441pub struct CompetitionConnectedUserRank {
442    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
443    pub wallet: WalletAddress,
444    pub username: String,
445    pub rank: Option<usize>,
446    #[serde(default, deserialize_with = "option_string_decimal")]
447    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
448    pub pnl: Option<Decimal>,
449    #[serde(default, deserialize_with = "option_string_decimal")]
450    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
451    pub volume: Option<Decimal>,
452    #[serde(default, deserialize_with = "option_string_decimal")]
453    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
454    pub efficiency: Option<Decimal>,
455    pub medal: Option<u8>,
456}
457
458/// Competition leaderboard response.
459#[derive(Debug, Clone, Serialize, Deserialize)]
460#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
461pub struct CompetitionLeaderboardResponse {
462    pub success: bool,
463    pub competition_id: i64,
464    pub sort_by: String,
465    pub sort_order: String,
466    #[serde(default)]
467    pub data: Vec<CompetitionLeaderboardRow>,
468    #[serde(default)]
469    pub connected_user: Option<CompetitionConnectedUserRank>,
470    pub pagination: Pagination,
471}
472
473/// Competition PnL standing for a single wallet.
474#[derive(Debug, Clone, Serialize, Deserialize)]
475#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
476pub struct CompetitionPnlStanding {
477    pub competition_id: i64,
478    pub competition_name: String,
479    pub competition_state: String,
480    pub rank: Option<usize>,
481    #[serde(deserialize_with = "string_decimal")]
482    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
483    pub pnl: Decimal,
484    #[serde(deserialize_with = "string_decimal")]
485    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
486    pub volume: Decimal,
487    #[serde(deserialize_with = "string_decimal")]
488    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
489    pub efficiency: Decimal,
490    pub medal: Option<u8>,
491}
492
493/// Competition PnL summary for a wallet.
494#[derive(Debug, Clone, Serialize, Deserialize)]
495#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
496pub struct CompetitionPnlSummary {
497    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
498    pub wallet: WalletAddress,
499    #[serde(deserialize_with = "string_decimal")]
500    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
501    pub lifetime_realized_pnl: Decimal,
502    #[serde(default)]
503    pub active_competition: Option<CompetitionPnlStanding>,
504}
505
506/// Competition summary response.
507#[derive(Debug, Clone, Serialize, Deserialize)]
508#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
509pub struct CompetitionPnlSummaryResponse {
510    pub success: bool,
511    pub data: CompetitionPnlSummary,
512}
513
514/// Competition account PnL response payload.
515#[derive(Debug, Clone, Serialize, Deserialize)]
516#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
517pub struct CompetitionAccountPnl {
518    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
519    pub wallet: WalletAddress,
520    pub username: String,
521    #[serde(deserialize_with = "string_decimal")]
522    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
523    pub lifetime_realized_pnl: Decimal,
524    pub competition: CompetitionPnlStanding,
525}
526
527/// Competition account response.
528#[derive(Debug, Clone, Serialize, Deserialize)]
529#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
530pub struct CompetitionAccountResponse {
531    pub success: bool,
532    pub data: CompetitionAccountPnl,
533}
534
535/// Orders list response.
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct OrdersResponse {
538    pub success: bool,
539    pub data: Vec<OrderInfo>,
540    pub pagination: Pagination,
541}
542
543/// Option summary (for fetching underlying price).
544#[derive(Debug, Clone, Default, Serialize, Deserialize)]
545pub struct OptionSummary {
546    #[serde(default, deserialize_with = "string_or_f64_default")]
547    pub underlying_price: f64,
548    pub option_token_address: Option<WalletAddress>,
549    #[serde(default)]
550    pub greeks: Option<OptionGreeks>,
551}
552
553#[derive(Debug, Clone, Default, Serialize, Deserialize)]
554pub struct OptionGreeks {
555    #[serde(default, deserialize_with = "string_or_f64_default")]
556    pub delta: f64,
557    #[serde(default, deserialize_with = "string_or_f64_default")]
558    pub gamma: f64,
559    #[serde(default, deserialize_with = "string_or_f64_default")]
560    pub theta: f64,
561    #[serde(default, deserialize_with = "string_or_f64_default")]
562    pub vega: f64,
563    #[serde(default, deserialize_with = "string_or_f64_default")]
564    pub rho: f64,
565}
566
567/// JSON-RPC error information.
568#[derive(Debug, Clone, Serialize, Deserialize)]
569#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
570pub struct JsonRpcError {
571    /// Error code
572    pub code: i32,
573    /// Error message
574    pub message: String,
575    /// Additional error data
576    pub data: Option<serde_json::Value>,
577}
578
579/// JSON-RPC style response.
580#[derive(Debug, Clone, Serialize, Deserialize)]
581#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
582pub struct JsonRpcResponse<T> {
583    /// JSON-RPC version
584    pub jsonrpc: String,
585    /// Result data
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub result: Option<T>,
588    /// Error information
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub error: Option<JsonRpcError>,
591    /// Whether this is testnet
592    pub testnet: bool,
593    /// Processing time in microseconds
594    #[serde(rename = "usDiff")]
595    pub us_diff: i64,
596    /// Request received timestamp in microseconds
597    #[serde(rename = "usIn")]
598    pub us_in: i64,
599    /// Response sent timestamp in microseconds
600    #[serde(rename = "usOut")]
601    pub us_out: i64,
602}
603
604/// Response for approving an agent.
605#[derive(Debug, Clone, Serialize, Deserialize)]
606#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
607pub struct ApproveAgentResponse {
608    /// Whether the request succeeded
609    pub success: bool,
610    /// Error message if failed
611    pub error: Option<String>,
612}
613
614/// Response for revoking an agent.
615#[derive(Debug, Clone, Serialize, Deserialize)]
616#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
617pub struct RevokeAgentResponse {
618    /// Whether the request succeeded
619    pub success: bool,
620    /// Error message if failed
621    pub error: Option<String>,
622}
623
624/// Response listing authorized agents.
625#[derive(Debug, Clone, Serialize, Deserialize)]
626#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
627pub struct AuthorizedAgentsResponse {
628    /// List of authorized agent wallet addresses
629    #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
630    pub agents: Vec<WalletAddress>,
631}
632
633/// Tick size step for instrument pricing.
634#[derive(Debug, Clone, Serialize, Deserialize)]
635#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
636pub struct TickSizeStep {
637    /// Tick size at this level
638    pub tick_size: f64,
639    /// Price above which this tick size applies
640    pub above_price: f64,
641}
642
643/// Instrument information response.
644#[derive(Debug, Clone, Serialize, Deserialize)]
645#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
646pub struct InstrumentResponse {
647    /// Price index name
648    pub price_index: String,
649    /// RFQ enabled
650    pub rfq: bool,
651    /// Orderbook enabled
652    #[serde(default = "default_true")]
653    pub orderbook: bool,
654    /// Instrument kind
655    pub kind: String,
656    /// Instrument name/symbol
657    pub instrument_name: String,
658    /// Option token contract address
659    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
660    pub option_token_address: Option<WalletAddress>,
661    /// Maker commission rate
662    pub maker_commission: f64,
663    /// Taker commission rate
664    pub taker_commission: f64,
665    /// Instrument type
666    pub instrument_type: String,
667    /// Expiration timestamp
668    pub expiration_timestamp: i64,
669    /// Creation timestamp
670    pub creation_timestamp: i64,
671    /// Whether instrument is active
672    pub is_active: bool,
673    /// Option type (call/put)
674    pub option_type: String,
675    /// Contract size
676    pub contract_size: f64,
677    /// Tick size
678    pub tick_size: f64,
679    /// Strike price
680    pub strike: f64,
681    /// Instrument ID
682    pub instrument_id: i32,
683    /// Settlement period
684    pub settlement_period: String,
685    /// Minimum trade amount
686    pub min_trade_amount: f64,
687    /// Block trade commission
688    pub block_trade_commission: f64,
689    /// Block trade minimum amount
690    pub block_trade_min_trade_amount: f64,
691    /// Block trade tick size
692    pub block_trade_tick_size: f64,
693    /// Settlement currency
694    pub settlement_currency: String,
695    /// Base currency
696    pub base_currency: String,
697    /// Counter currency
698    pub counter_currency: String,
699    /// Quote currency
700    pub quote_currency: String,
701    /// Tick size steps
702    pub tick_size_steps: Vec<TickSizeStep>,
703}
704
705/// Canonical instrument specification for discovery, quoting, and lifecycle
706/// consumers.
707#[derive(Debug, Clone, Serialize, Deserialize)]
708#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
709pub struct InstrumentSpecResponse {
710    /// Canonical string identifier used by downstream systems.
711    pub instrument_id: String,
712    /// Database numeric identifier for legacy joins.
713    pub instrument_numeric_id: i32,
714    /// Native venue symbol used in REST and WebSocket calls.
715    pub exchange_symbol: String,
716    /// Trading pair for grouping, e.g. `"BTC-USD"`.
717    pub sym: String,
718    /// Venue identifier, e.g. `"HYPERCALL"`.
719    pub exchange: String,
720    /// Instrument class: `"OPTION"`, `"PERP"`, `"SPOT"`, or `"FUTURE"`.
721    pub instrument_kind: String,
722    /// Option side, `"C"` or `"P"` when this is an option.
723    pub option_kind: Option<String>,
724    /// Delivery mode, e.g. `"CASH"` or `"PHYSICAL"`.
725    pub delivery: Option<String>,
726    /// Settlement asset for margin and PnL accounting.
727    pub settle_asset: Option<String>,
728    /// Base asset for cross-venue mapping.
729    pub base_asset: String,
730    /// Quote asset for cross-venue mapping.
731    pub quote_asset: String,
732    /// Strike price for derivatives. Serialized as a string.
733    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
734    pub strike: Decimal,
735    /// Expiry timestamp in nanoseconds since epoch.
736    pub expiry_ns: i64,
737    /// UTC hour of day at which this instrument expires and settles.
738    pub settlement_hour_utc: Option<u8>,
739    /// UTC time of day ("HH:MM") at which this instrument expires and
740    /// settles. Authoritative alongside expiry_ns; per-underlying policy.
741    pub settlement_time_utc: Option<String>,
742    /// Contract size used for Greeks and notional scaling.
743    pub contract_size: f64,
744    /// Minimum trade size.
745    pub min_trade_size: f64,
746    /// Base tick size for price rounding.
747    pub tick_size: f64,
748    /// Decimal precision for wire price quantities.
749    pub price_decimals: Option<u32>,
750    /// Decimal precision for wire size quantities.
751    pub size_decimals: Option<u32>,
752    /// Stepped tick rules, when any.
753    pub min_price_increment_bands: Vec<TickSizeStep>,
754    /// Lifecycle state: `"OPEN"`, `"SETTLEMENT"`, or `"DELIVERED"`.
755    pub state: String,
756    /// Whether the instrument can currently accept new trading activity.
757    pub is_tradable: bool,
758    /// First-listed time in nanoseconds, if known.
759    pub listed_time_ns: Option<i64>,
760    /// Timestamp for this spec version in nanoseconds. Currently always
761    /// null: no persisted spec-change timestamp exists yet, and cache
762    /// rebuild times would read as false spec changes.
763    pub event_ts_ns: Option<i64>,
764    /// Maker fee in basis points, if configured per instrument.
765    pub maker_fee_bps: Option<f64>,
766    /// Taker fee in basis points, if configured per instrument.
767    pub taker_fee_bps: Option<f64>,
768    /// Initial margin fraction, if configured per instrument.
769    pub initial_margin_fraction: Option<f64>,
770    /// Maintenance margin fraction, if configured per instrument.
771    pub maintenance_margin_fraction: Option<f64>,
772    /// Per-instrument position limit, if configured.
773    pub position_limit: Option<f64>,
774    /// Hypercall option token contract address, if deployed.
775    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
776    pub option_token_address: Option<WalletAddress>,
777    /// Settlement oracle identifier, if modeled.
778    pub settlement_oracle: Option<String>,
779    /// External condition identifier, if modeled.
780    pub condition_id: Option<String>,
781    /// Source used to resolve the underlying at settlement, if modeled.
782    pub underlying_resolution_source: Option<String>,
783}
784
785/// Order book statistics.
786#[derive(Debug, Clone, Serialize, Deserialize)]
787#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
788pub struct OrderBookStats {
789    /// 24h high
790    pub high: Option<f64>,
791    /// 24h low
792    pub low: Option<f64>,
793    /// 24h price change
794    pub price_change: Option<f64>,
795    /// 24h volume
796    pub volume: f64,
797    /// 24h volume in USD
798    pub volume_usd: f64,
799}
800
801/// Option Greeks for order book.
802#[derive(Debug, Clone, Serialize, Deserialize)]
803#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
804pub struct OrderBookGreeks {
805    /// Delta
806    pub delta: f64,
807    /// Gamma
808    pub gamma: f64,
809    /// Vega
810    pub vega: f64,
811    /// Theta
812    pub theta: f64,
813    /// Rho
814    pub rho: f64,
815}
816
817/// Orderbook response.
818#[derive(Debug, Clone, Serialize, Deserialize)]
819#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
820pub struct OrderBookResponse {
821    /// Timestamp
822    pub timestamp: i64,
823    /// Market state
824    pub state: String,
825    /// Order book statistics
826    pub stats: OrderBookStats,
827    /// Option Greeks
828    #[serde(skip_serializing_if = "Option::is_none")]
829    pub greeks: Option<OrderBookGreeks>,
830    /// Change ID for incremental updates
831    pub change_id: i64,
832    /// Index price
833    pub index_price: f64,
834    /// Instrument name
835    pub instrument_name: String,
836    /// Option token contract address
837    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
838    pub option_token_address: Option<WalletAddress>,
839    /// Bid orders [price, size], where size is in human-readable contracts.
840    pub bids: Vec<[f64; 2]>,
841    /// Ask orders [price, size], where size is in human-readable contracts.
842    pub asks: Vec<[f64; 2]>,
843    /// Last trade price
844    pub last_price: Option<f64>,
845    /// Settlement price
846    pub settlement_price: f64,
847    /// Minimum price
848    pub min_price: f64,
849    /// Maximum price
850    pub max_price: f64,
851    /// Open interest
852    pub open_interest: f64,
853    /// Mark price
854    pub mark_price: f64,
855    /// Theoretical option price derived from the vol oracle, when available.
856    #[serde(default, skip_serializing_if = "Option::is_none")]
857    pub theoretical_price: Option<f64>,
858    /// Best bid price
859    pub best_bid_price: f64,
860    /// Best ask price
861    pub best_ask_price: f64,
862    /// Theoretical implied volatility used for mark price and greeks.
863    #[serde(default, skip_serializing_if = "Option::is_none")]
864    pub mark_iv: Option<f64>,
865    /// Quote-derived ask-side implied volatility
866    #[serde(default, skip_serializing_if = "Option::is_none")]
867    pub ask_iv: Option<f64>,
868    /// Quote-derived bid-side implied volatility
869    #[serde(default, skip_serializing_if = "Option::is_none")]
870    pub bid_iv: Option<f64>,
871    /// Underlying price
872    pub underlying_price: f64,
873    /// Underlying index name
874    pub underlying_index: String,
875    /// Interest rate
876    pub interest_rate: f64,
877    /// Estimated delivery price
878    pub estimated_delivery_price: f64,
879    /// Best ask amount in human-readable contracts.
880    pub best_ask_amount: f64,
881    /// Best bid amount in human-readable contracts.
882    pub best_bid_amount: f64,
883}
884
885// =============================================================================
886// Historical PnL / Equity Response Types
887// =============================================================================
888
889/// Supported historical equity snapshot intervals.
890pub const HISTORICAL_PNL_INTERVAL_5M_MS: i64 = 5 * 60 * 1000;
891/// Supported historical equity snapshot intervals.
892pub const HISTORICAL_PNL_INTERVAL_1H_MS: i64 = 60 * 60 * 1000;
893/// Supported historical equity snapshot intervals.
894pub const HISTORICAL_PNL_INTERVAL_1D_MS: i64 = 24 * 60 * 60 * 1000;
895
896/// Historical equity interval identifier.
897#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
898#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
899pub enum HistoricalPnlInterval {
900    /// 5-minute snapshots.
901    #[serde(rename = "5m")]
902    FiveMinutes,
903    /// 1-hour snapshots.
904    #[serde(rename = "1h")]
905    OneHour,
906    /// 1-day snapshots.
907    #[serde(rename = "1d")]
908    OneDay,
909}
910
911impl HistoricalPnlInterval {
912    /// Interval duration in milliseconds.
913    pub fn as_ms(self) -> i64 {
914        match self {
915            Self::FiveMinutes => HISTORICAL_PNL_INTERVAL_5M_MS,
916            Self::OneHour => HISTORICAL_PNL_INTERVAL_1H_MS,
917            Self::OneDay => HISTORICAL_PNL_INTERVAL_1D_MS,
918        }
919    }
920
921    /// Interval identifier used in query params and API responses.
922    pub fn as_str(self) -> &'static str {
923        match self {
924            Self::FiveMinutes => "5m",
925            Self::OneHour => "1h",
926            Self::OneDay => "1d",
927        }
928    }
929}
930
931/// A single historical equity point.
932#[derive(Debug, Clone, Serialize, Deserialize)]
933#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
934pub struct HistoricalPnlPoint {
935    /// Interval bucket start timestamp in milliseconds since epoch.
936    pub timestamp: i64,
937    /// Total account equity at the bucket timestamp.
938    #[serde(deserialize_with = "string_decimal")]
939    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
940    pub equity: Decimal,
941    /// Per-symbol PnL attribution. Keys are symbol names, values are [position, entry_price, realized, unrealized, total].
942    #[serde(default, skip_serializing_if = "Option::is_none")]
943    pub attribution: Option<std::collections::HashMap<String, [f64; 5]>>,
944    /// Cumulative net deposits (deposits minus withdraws) as of this bucket's
945    /// timestamp. The frontend uses this as the P&L baseline so multi-deposit
946    /// wallets stay correctly anchored historically; the delta between
947    /// consecutive points also surfaces deposit/withdraw events for annotation.
948    #[serde(
949        default,
950        skip_serializing_if = "Option::is_none",
951        deserialize_with = "option_string_decimal"
952    )]
953    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
954    pub net_deposits: Option<Decimal>,
955}
956
957/// Historical equity response for a wallet and interval.
958#[derive(Debug, Clone, Serialize, Deserialize)]
959#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
960pub struct HistoricalPnlResponse {
961    /// Account wallet address.
962    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
963    pub wallet_address: WalletAddress,
964    /// Historical interval identifier.
965    pub interval: HistoricalPnlInterval,
966    /// Returned points in ascending timestamp order.
967    #[serde(default)]
968    pub points: Vec<HistoricalPnlPoint>,
969}
970
971/// Supported historical theo snapshot intervals.
972pub const HISTORICAL_THEO_INTERVAL_5M_MS: i64 = HISTORICAL_PNL_INTERVAL_5M_MS;
973/// Supported historical theo snapshot intervals.
974pub const HISTORICAL_THEO_INTERVAL_1H_MS: i64 = HISTORICAL_PNL_INTERVAL_1H_MS;
975/// Supported historical theo snapshot intervals.
976pub const HISTORICAL_THEO_INTERVAL_1D_MS: i64 = HISTORICAL_PNL_INTERVAL_1D_MS;
977
978/// Historical theo interval identifier.
979#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
980#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
981pub enum HistoricalTheoInterval {
982    /// 5-minute snapshots.
983    #[serde(rename = "5m")]
984    FiveMinutes,
985    /// 1-hour snapshots.
986    #[serde(rename = "1h")]
987    OneHour,
988    /// 1-day snapshots.
989    #[serde(rename = "1d")]
990    OneDay,
991}
992
993impl HistoricalTheoInterval {
994    /// Interval duration in milliseconds.
995    pub fn as_ms(self) -> i64 {
996        match self {
997            Self::FiveMinutes => HISTORICAL_THEO_INTERVAL_5M_MS,
998            Self::OneHour => HISTORICAL_THEO_INTERVAL_1H_MS,
999            Self::OneDay => HISTORICAL_THEO_INTERVAL_1D_MS,
1000        }
1001    }
1002
1003    /// Interval identifier used in query params and API responses.
1004    pub fn as_str(self) -> &'static str {
1005        match self {
1006            Self::FiveMinutes => "5m",
1007            Self::OneHour => "1h",
1008            Self::OneDay => "1d",
1009        }
1010    }
1011}
1012
1013/// A single historical theoretical-price point.
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1015#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1016pub struct HistoricalTheoPoint {
1017    /// Interval bucket start timestamp in milliseconds since epoch.
1018    pub timestamp: i64,
1019    /// Theoretical option price at the bucket timestamp.
1020    pub theoretical_price: f64,
1021}
1022
1023/// Historical theo response for an instrument and interval.
1024#[derive(Debug, Clone, Serialize, Deserialize)]
1025#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1026pub struct HistoricalTheoResponse {
1027    /// Option instrument symbol.
1028    pub instrument_name: String,
1029    /// Historical interval identifier.
1030    pub interval: HistoricalTheoInterval,
1031    /// Returned points in ascending timestamp order.
1032    #[serde(default)]
1033    pub points: Vec<HistoricalTheoPoint>,
1034}
1035
1036// =============================================================================
1037// Portfolio Response Types
1038// =============================================================================
1039
1040/// Unified margin summary that works for both Standard and Portfolio modes.
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1043pub struct MarginSummary {
1044    /// Margin mode: "standard" or "portfolio"
1045    pub mode: String,
1046    /// Total account equity (balance + unrealized PnL)
1047    #[serde(deserialize_with = "string_decimal")]
1048    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1049    pub equity: Decimal,
1050    /// Initial Margin required from positions
1051    #[serde(deserialize_with = "string_decimal")]
1052    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1053    pub position_im: Decimal,
1054    /// Initial Margin from open orders
1055    #[serde(deserialize_with = "string_decimal")]
1056    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1057    pub open_orders_im: Decimal,
1058    /// Excess Initial Margin (equity - position_im - open_orders_im)
1059    #[serde(deserialize_with = "string_decimal")]
1060    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1061    pub initial_margin: Decimal,
1062    /// Excess Maintenance Margin (equity - position_mm)
1063    #[serde(deserialize_with = "string_decimal")]
1064    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1065    pub maintenance_margin: Decimal,
1066    /// (Standard mode only) USDC premium reserved for open BUY orders
1067    #[serde(
1068        default,
1069        skip_serializing_if = "Option::is_none",
1070        deserialize_with = "option_string_decimal"
1071    )]
1072    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1073    pub open_orders_premium_reserved: Option<Decimal>,
1074}
1075
1076impl MarginSummary {
1077    /// Calculate the maintenance margin required (position_mm).
1078    /// Since the API returns excess MM (equity - position_mm), we derive position_mm.
1079    pub fn maintenance_margin_required(&self) -> Decimal {
1080        // maintenance_margin = equity - position_mm
1081        // Therefore: position_mm = equity - maintenance_margin
1082        (self.equity - self.maintenance_margin).max(Decimal::ZERO)
1083    }
1084
1085    /// Calculate margin utilization (0-1 scale).
1086    /// Returns the ratio of maintenance margin required to equity.
1087    pub fn margin_utilization(&self) -> Decimal {
1088        if self.equity <= Decimal::ZERO {
1089            return Decimal::ONE;
1090        }
1091        (self.maintenance_margin_required() / self.equity).min(Decimal::ONE)
1092    }
1093}
1094
1095/// Position information in portfolio response.
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1097#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1098pub struct PortfolioPosition {
1099    /// Wallet address
1100    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1101    pub wallet_address: WalletAddress,
1102    /// Option symbol (e.g., "BTC-20260115-100000-C")
1103    pub symbol: String,
1104    /// Position size in contracts (positive = long, negative = short)
1105    #[serde(deserialize_with = "string_decimal")]
1106    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1107    pub amount: Decimal,
1108    /// Average entry price
1109    #[serde(deserialize_with = "string_decimal")]
1110    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1111    pub entry_price: Decimal,
1112    /// Margin posted for this position
1113    #[serde(deserialize_with = "string_decimal")]
1114    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1115    pub margin_posted: Decimal,
1116    /// Realized profit/loss from closed positions
1117    #[serde(deserialize_with = "string_decimal")]
1118    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1119    pub realized_pnl: Decimal,
1120    /// Unrealized profit/loss from mark-to-market
1121    #[serde(deserialize_with = "string_decimal")]
1122    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1123    pub unrealized_pnl: Decimal,
1124    /// Notional value of position
1125    #[serde(default, deserialize_with = "option_string_decimal_default")]
1126    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1127    pub notional_value: Decimal,
1128    /// Required maintenance margin (deprecated, use portfolio-level margin)
1129    #[serde(default, deserialize_with = "option_string_decimal_default")]
1130    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1131    pub maintenance_margin: Decimal,
1132    /// Liquidation price (deprecated, use portfolio-level SPAN)
1133    #[serde(default, deserialize_with = "option_string_decimal_default")]
1134    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1135    pub liquidation_price: Decimal,
1136    /// Current margin ratio
1137    #[serde(default, deserialize_with = "option_string_decimal_default")]
1138    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1139    pub margin_ratio: Decimal,
1140}
1141
1142/// Portfolio response from the API.
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1144#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1145pub struct PortfolioResponse {
1146    /// Account wallet address
1147    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1148    pub wallet_address: WalletAddress,
1149    /// List of positions
1150    #[serde(default)]
1151    pub positions: Vec<PortfolioPosition>,
1152    /// Total margin used (position IM + open orders IM)
1153    #[serde(deserialize_with = "string_decimal")]
1154    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1155    pub total_margin_used: Decimal,
1156    /// Available balance for new positions
1157    #[serde(deserialize_with = "string_decimal")]
1158    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1159    pub available_balance: Decimal,
1160    /// Margin mode: "standard" or "portfolio"
1161    #[serde(default = "default_margin_mode")]
1162    pub margin_mode: String,
1163    /// Unified margin summary
1164    pub margin_summary: Option<MarginSummary>,
1165}
1166
1167fn default_margin_mode() -> String {
1168    "standard".to_string()
1169}
1170
1171impl PortfolioResponse {
1172    /// Get equity from margin summary.
1173    pub fn equity(&self) -> Option<Decimal> {
1174        self.margin_summary.as_ref().map(|m| m.equity)
1175    }
1176
1177    /// Get maintenance margin required from margin summary.
1178    pub fn maintenance_margin_required(&self) -> Option<Decimal> {
1179        self.margin_summary
1180            .as_ref()
1181            .map(|m| m.maintenance_margin_required())
1182    }
1183
1184    /// Calculate margin utilization (0-1 scale).
1185    pub fn margin_utilization(&self) -> Option<Decimal> {
1186        self.margin_summary.as_ref().map(|m| m.margin_utilization())
1187    }
1188
1189    /// Get position IM from margin summary.
1190    pub fn position_im(&self) -> Option<Decimal> {
1191        self.margin_summary.as_ref().map(|m| m.position_im)
1192    }
1193
1194    /// Get open orders IM from margin summary.
1195    pub fn open_orders_im(&self) -> Option<Decimal> {
1196        self.margin_summary.as_ref().map(|m| m.open_orders_im)
1197    }
1198}
1199
1200/// Deserialize a string as Decimal, defaulting to zero if missing or null.
1201fn option_string_decimal_default<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
1202where
1203    D: Deserializer<'de>,
1204{
1205    use serde::de;
1206
1207    struct OptStringDecimalVisitor;
1208
1209    impl<'de> de::Visitor<'de> for OptStringDecimalVisitor {
1210        type Value = Decimal;
1211
1212        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1213            formatter.write_str("a string representing Decimal or null")
1214        }
1215
1216        fn visit_none<E>(self) -> Result<Self::Value, E>
1217        where
1218            E: de::Error,
1219        {
1220            Ok(Decimal::ZERO)
1221        }
1222
1223        fn visit_unit<E>(self) -> Result<Self::Value, E>
1224        where
1225            E: de::Error,
1226        {
1227            Ok(Decimal::ZERO)
1228        }
1229
1230        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1231        where
1232            E: de::Error,
1233        {
1234            v.parse::<Decimal>().map_err(de::Error::custom)
1235        }
1236
1237        fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
1238        where
1239            D2: Deserializer<'de>,
1240        {
1241            deserializer.deserialize_str(OptStringDecimalVisitor)
1242        }
1243    }
1244
1245    deserializer.deserialize_any(OptStringDecimalVisitor)
1246}
1247
1248// RFQ Response Types
1249
1250/// Response for an RFQ quote leg.
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1252#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1253pub struct RfqQuoteLegResponse {
1254    pub instrument: String,
1255    pub side: Side,
1256    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1257    pub price: Decimal,
1258    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1259    pub size: Decimal,
1260}
1261
1262/// A single quote from a QP.
1263#[derive(Debug, Clone, Serialize, Deserialize)]
1264#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1265pub struct RfqQuoteResponse {
1266    pub quote_id: String,
1267    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1268    pub net_premium: Decimal,
1269    pub legs: Vec<RfqQuoteLegResponse>,
1270    pub expires_at: u64,
1271}
1272
1273/// RFQ leg in status response.
1274#[derive(Debug, Clone, Serialize, Deserialize)]
1275#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1276pub struct RfqLegResponse {
1277    pub instrument: String,
1278    pub side: Side,
1279    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1280    pub size: Decimal,
1281}
1282
1283/// Full RFQ status response.
1284#[derive(Debug, Clone, Serialize, Deserialize)]
1285#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1286pub struct RfqStatusResponse {
1287    pub rfq_id: String,
1288    pub status: RfqStatus,
1289    pub underlying: String,
1290    pub legs: Vec<RfqLegResponse>,
1291    pub quotes: Vec<RfqQuoteResponse>,
1292    pub created_at: u64,
1293    pub expires_at: u64,
1294}
1295
1296/// Response after accepting an RFQ quote.
1297#[derive(Debug, Clone, Serialize, Deserialize)]
1298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1299pub struct RfqAcceptResponse {
1300    pub rfq_id: String,
1301    pub quote_id: String,
1302    pub status: RfqStatus,
1303    pub fill_id: String,
1304}
1305
1306/// RFQ history response.
1307#[derive(Debug, Clone, Serialize, Deserialize)]
1308#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1309pub struct RfqHistoryResponse {
1310    pub rfqs: Vec<RfqStatusResponse>,
1311}
1312
1313/// Quote provider info (admin response).
1314#[derive(Debug, Clone, Serialize, Deserialize)]
1315#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1316pub struct QuoteProviderResponse {
1317    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1318    pub wallet_address: WalletAddress,
1319    pub tier: String,
1320    pub status: String,
1321    pub allowed_underlyings: Option<Vec<String>>,
1322    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1323    pub max_notional_per_quote: Decimal,
1324    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1325    pub max_open_notional: Decimal,
1326}
1327
1328// Backward-compatible re-exports. These types were moved to api_models as the
1329// canonical Decimal-based definitions. Code that imported them from responses
1330// still compiles via these aliases.
1331pub use crate::api_models::Instrument;
1332pub use crate::api_models::MarketInfo;
1333pub use crate::api_models::MarketsResponse;