architect_api/cpty/
cqg.rs

1use crate::{
2    folio::FolioMessage,
3    orderflow::{
4        AberrantFill, Ack, Cancel, CancelAll, Fill, Order, OrderStateFlags,
5        OrderflowMessage, Out, Reject, RejectReason,
6    },
7    symbology::{market::NormalizedMarketInfo, MarketId},
8    Dir, OrderId,
9};
10use arcstr::ArcStr;
11use chrono::{DateTime, Utc};
12#[cfg(feature = "netidx")]
13use derive::FromValue;
14use enumflags2::BitFlags;
15#[cfg(feature = "netidx")]
16use netidx_derive::Pack;
17use rust_decimal::Decimal;
18use schemars::JsonSchema;
19use serde_derive::{Deserialize, Serialize};
20use std::{ops::Deref, sync::Arc};
21
22#[derive(Clone, Debug, Deserialize)]
23pub struct CqgIcsFileRow {
24    #[serde(rename = "CQG Contract Symbol")]
25    pub contract_symbol: String,
26    #[serde(rename = "Exchange")]
27    pub exchange: String,
28    #[serde(rename = "CQG Instrument Group Symbol")]
29    pub cqg_instrument_group_symbol: String,
30    #[serde(rename = "SSL Underlying Group Symbol")]
31    pub ssl_underlying_group_symbol: String,
32    #[serde(rename = "OSL Underlying Group Symbol")]
33    pub osl_underlying_group_symbol: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37#[cfg_attr(feature = "netidx", derive(Pack))]
38pub struct CqgMarketInfo {
39    pub deleted: bool,
40    // Don't store contract_id because it's session-specific
41    pub symbol_id: Option<String>,
42    pub description: String,
43    pub cfi_code: Option<String>,
44    pub cqg_contract_symbol: Option<String>,
45    pub correct_price_scale: Decimal,
46    pub exchange_symbol: String,
47    pub exchange_group_symbol: String,
48    pub tick_size: Decimal,  // uncorrected
49    pub tick_value: Decimal, // uncorrected
50    pub trade_size_increment: Decimal,
51    pub market_data_delay_ms: i64,
52    pub initial_margin: Option<Decimal>,
53    pub maintenance_margin: Option<Decimal>,
54    pub last_trading_date: DateTime<Utc>,
55    pub first_notice_date: Option<DateTime<Utc>>,
56}
57
58impl NormalizedMarketInfo for CqgMarketInfo {
59    fn tick_size(&self) -> Decimal {
60        self.tick_size
61    }
62
63    fn step_size(&self) -> Decimal {
64        self.trade_size_increment
65    }
66
67    fn is_delisted(&self) -> bool {
68        self.deleted
69    }
70
71    fn initial_margin(&self) -> Option<Decimal> {
72        self.initial_margin
73    }
74
75    fn maintenance_margin(&self) -> Option<Decimal> {
76        self.maintenance_margin
77    }
78}
79
80impl std::fmt::Display for CqgMarketInfo {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}", serde_json::to_string_pretty(self).unwrap())?;
83        Ok(())
84    }
85}
86
87#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
88#[cfg_attr(feature = "netidx", derive(Pack))]
89pub struct CqgOrder {
90    #[serde(flatten)]
91    pub order: Order,
92}
93
94impl Deref for CqgOrder {
95    type Target = Order;
96
97    fn deref(&self) -> &Self::Target {
98        &self.order
99    }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
103#[cfg_attr(feature = "netidx", derive(Pack))]
104pub struct CqgTrade {
105    pub order_id: OrderId,
106    pub contract_symbol: Option<String>,
107    pub exec_id: String,
108    pub scaled_price: i64,
109    pub qty: Decimal,
110    pub time: DateTime<Utc>,
111    pub side: Dir,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
115#[cfg_attr(feature = "netidx", derive(Pack, FromValue))]
116pub struct CqgAccountSummary {
117    /// True if this is a snapshot related message.
118    /// Since snapshot might be sent in several messages (including none), client should use
119    /// TradeSnapshotCompletion message as an indicator of complete snapshot delivery for a particular subscription.
120    pub is_snapshot: Option<bool>,
121
122    /// Account id of this status.
123    /// It is required field.
124    pub account_id: Option<i32>,
125
126    /// Currency code of account values (ISO 4217 based).
127    /// It is required field in snapshot and included into updates only if changed.
128    pub currency: Option<String>,
129
130    /// Identifiers of fields being cleared.
131    /// E.g. to clear total_margin server will include value 6 into the collection.
132    pub cleared_fields: Vec<u32>,
133
134    /// Margin requirement calculated for worst-case based on open positions and working orders.
135    pub total_margin: Option<f64>,
136
137    /// Margin requirement based on current positions only.
138    pub position_margin: Option<f64>,
139
140    /// Available account funds including balance, realized profit (or loss), collateral and credits.
141    /// OTE and MVO are included regarding the account risk parameters.
142    /// For a group account, purchasing power is a recent snapshot calculated by the server.
143    /// It uses data from all accounts in the group, so it will not be synchronized with values
144    /// reported for only this account. Also, for group accounts, OTE and MVO components of
145    /// purchasing power will not be synchronized with market data updates.
146    /// See trading_account_2.Account.is_group_member.
147    pub purchasing_power: Option<f64>,
148
149    /// Open trade equity, or potential profit (or loss) from futures and future-style options positions
150    /// based on opening price of the position and the current future trade/best bid/best ask
151    /// (regarding to the risk account settings) or settlement price if trade is not available.
152    pub ote: Option<f64>,
153
154    /// Market value of options calculated as the current market trade/best bid/best ask of the option
155    /// (regarding to the risk account settings) times the number of options
156    /// (positive for long options and negative for short options) in the portfolio.
157    pub mvo: Option<f64>,
158
159    /// Market value of futures calculated as the current market trade/best bid/best ask
160    /// (regarding to the risk account settings) times the number of futures
161    /// (positive for long and negative for short) in the portfolio.
162    pub mvf: Option<f64>,
163
164    /// Allowable margin credit of the account.
165    pub margin_credit: Option<f64>,
166
167    /// Cash Excess.
168    pub cash_excess: Option<f64>,
169
170    /// Current account's balance. In particular includes: yesterday balance, profit/loss, option premium,
171    /// commission and Forex instrument positions.
172    pub current_balance: Option<f64>,
173
174    /// Realized profit/loss.
175    pub profit_loss: Option<f64>,
176
177    /// Unrealized profit/loss for options.
178    pub unrealized_profit_loss: Option<f64>,
179
180    /// Cash balance from the last statement.
181    pub yesterday_balance: Option<f64>,
182
183    /// Open trade equity for futures and futures-style options from the last statement.
184    pub yesterday_ote: Option<f64>,
185
186    /// Market value of premium-style options and fixed income from the last statement.
187    pub yesterday_mvo: Option<f64>,
188
189    /// Collateral on deposit.
190    pub yesterday_collateral: Option<f64>,
191
192    /// (profit_loss / abs(yesterday_balance)) in percentage.
193    pub net_change_pc: Option<f64>,
194
195    /// Sum of all fill sizes for the current day.
196    pub total_filled_qty: Option<Decimal>,
197
198    /// Count of filled orders for the current day.
199    pub total_filled_orders: Option<u32>,
200
201    /// Sum of position quantities among all long open positions on the account.
202    pub long_open_positions_qty: Option<Decimal>,
203
204    /// Sum of position quantities among all short open positions on the account.
205    pub short_open_positions_qty: Option<Decimal>,
206
207    /// Minimal value of days till contract expiration (in calendar days, not trading) among
208    /// all open positions on the account.
209    /// Not set if there are no open positions on the account.
210    pub min_days_till_position_contract_expiration: Option<u32>,
211
212    /// Limit of the maximum value of purchasing power for the account.
213    /// Can be empty e.g. when the account is a group account member.
214    /// See trading_account_2.Account.is_group_member.
215    pub purchasing_power_limit: Option<f64>,
216}
217
218impl CqgAccountSummary {
219    pub fn merge(&mut self, other: &CqgAccountSummary) {
220        let CqgAccountSummary {
221            is_snapshot: _,
222            account_id,
223            currency,
224            cleared_fields,
225            total_margin,
226            position_margin,
227            purchasing_power,
228            ote,
229            mvo,
230            mvf,
231            margin_credit,
232            cash_excess,
233            current_balance,
234            profit_loss,
235            unrealized_profit_loss,
236            yesterday_balance,
237            yesterday_ote,
238            yesterday_mvo,
239            yesterday_collateral,
240            net_change_pc,
241            total_filled_qty,
242            total_filled_orders,
243            long_open_positions_qty,
244            short_open_positions_qty,
245            min_days_till_position_contract_expiration,
246            purchasing_power_limit,
247        } = other;
248        self.account_id = account_id.or(self.account_id);
249        self.currency = currency.clone().or_else(|| self.currency.clone());
250        self.total_margin = total_margin.or(self.total_margin);
251        self.position_margin = position_margin.or(self.position_margin);
252        self.purchasing_power = purchasing_power.or(self.purchasing_power);
253        self.ote = ote.or(self.ote);
254        self.mvo = mvo.or(self.mvo);
255        self.mvf = mvf.or(self.mvf);
256        self.margin_credit = margin_credit.or(self.margin_credit);
257        self.cash_excess = cash_excess.or(self.cash_excess);
258        self.current_balance = current_balance.or(self.current_balance);
259        self.profit_loss = profit_loss.or(self.profit_loss);
260        self.unrealized_profit_loss =
261            unrealized_profit_loss.or(self.unrealized_profit_loss);
262        self.yesterday_balance = yesterday_balance.or(self.yesterday_balance);
263        self.yesterday_ote = yesterday_ote.or(self.yesterday_ote);
264        self.yesterday_mvo = yesterday_mvo.or(self.yesterday_mvo);
265        self.yesterday_collateral = yesterday_collateral.or(self.yesterday_collateral);
266        self.net_change_pc = net_change_pc.or(self.net_change_pc);
267        self.total_filled_qty = total_filled_qty.or(self.total_filled_qty);
268        self.total_filled_orders = total_filled_orders.or(self.total_filled_orders);
269        self.long_open_positions_qty =
270            long_open_positions_qty.or(self.long_open_positions_qty);
271        self.short_open_positions_qty =
272            short_open_positions_qty.or(self.short_open_positions_qty);
273        self.min_days_till_position_contract_expiration =
274            min_days_till_position_contract_expiration
275                .or(self.min_days_till_position_contract_expiration);
276        self.purchasing_power_limit =
277            purchasing_power_limit.or(self.purchasing_power_limit);
278        for field in cleared_fields {
279            match field {
280                2 => {
281                    self.is_snapshot = None;
282                }
283                3 => {
284                    self.account_id = None;
285                }
286                4 => {
287                    self.currency = None;
288                }
289                6 => {
290                    self.total_margin = None;
291                }
292                7 => {
293                    self.position_margin = None;
294                }
295                8 => {
296                    self.purchasing_power = None;
297                }
298                9 => {
299                    self.ote = None;
300                }
301                10 => {
302                    self.mvo = None;
303                }
304                11 => {
305                    self.mvf = None;
306                }
307                12 => {
308                    self.margin_credit = None;
309                }
310                13 => {
311                    self.cash_excess = None;
312                }
313                15 => {
314                    self.current_balance = None;
315                }
316                16 => {
317                    self.profit_loss = None;
318                }
319                17 => {
320                    self.unrealized_profit_loss = None;
321                }
322                18 => {
323                    self.yesterday_balance = None;
324                }
325                24 => {
326                    self.yesterday_ote = None;
327                }
328                25 => {
329                    self.yesterday_mvo = None;
330                }
331                14 => {
332                    self.yesterday_collateral = None;
333                }
334                26 => {
335                    self.net_change_pc = None;
336                }
337                19 => {
338                    self.total_filled_qty = None;
339                }
340                20 => {
341                    self.total_filled_orders = None;
342                }
343                21 => {
344                    self.long_open_positions_qty = None;
345                }
346                22 => {
347                    self.short_open_positions_qty = None;
348                }
349                23 => {
350                    self.min_days_till_position_contract_expiration = None;
351                }
352                27 => {
353                    self.purchasing_power_limit = None;
354                }
355                _ => {}
356            }
357        }
358    }
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
362#[cfg_attr(feature = "netidx", derive(Pack, FromValue))]
363pub struct CqgPositionStatus {
364    pub account: i32,
365    pub market: MarketId,
366    pub positions: Vec<CqgPosition>,
367    #[serde(default)]
368    #[cfg_attr(feature = "netidx", pack(default))]
369    pub is_snapshot: bool,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
373#[cfg_attr(feature = "netidx", derive(Pack, FromValue))]
374pub struct CqgPosition {
375    /// Surrogate id as a key for updates.
376    pub id: i32,
377
378    /// Position size, zero means that this position is deleted.
379    /// Note: quantity can be safely compared to zero, because this is an integral number of
380    /// ContractMetadata.volume_scale units.
381    pub qty: Option<Decimal>,
382
383    /// Position average price.
384    /// NOTE: Since it could be an aggregated position price is sent in correct format directly.
385    pub price_correct: Decimal,
386
387    /// Exchange specific trade date when the position was open or last changed (date only value).
388    pub trade_date: i64,
389
390    /// Statement date (date value only).
391    pub statement_date: i64,
392
393    /// UTC trade time (including date) if available, it might not be available e.g. for the previous day positions.
394    pub trade_utc_timestamp: Option<DateTime<Utc>>,
395
396    /// True if the price is an aggregated position price.
397    pub is_aggregated: bool,
398
399    /// True if the open position is short (result of a sell operation), long otherwise.
400    /// Undefined for deleted position (qty is 0).
401    pub is_short: bool,
402
403    /// Whether it is a yesterday or a today position.
404    /// NOTE: where available, this attribute is from the exchange trade date perspective. It is used for
405    /// position tracking and open/close instructions. It is not the same as previous day (associated
406    /// with brokerage statement) vs. intraday. It is also not static. For example, an intraday fill
407    /// with open_close_effect=OPEN will appear, when it is received during the trading session, in an open
408    /// position or matched trade with is_yesterday=false. After the exchange trade date rolls over for
409    /// that contract, and before the brokerage statement arrives reflecting it as a previous day position,
410    /// the same open position or matched trade will contain is_yesterday=true.
411    pub is_yesterday: Option<bool>,
412
413    /// Speculation type of the position. One of SpeculationType enum.
414    pub speculation_type: Option<u32>,
415}
416
417impl PartialOrd for CqgPosition {
418    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
419        self.id.partial_cmp(&other.id)
420    }
421}
422
423impl Ord for CqgPosition {
424    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
425        self.id.cmp(&other.id)
426    }
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
430#[cfg_attr(feature = "netidx", derive(Pack, FromValue))]
431pub struct CancelReject {
432    pub cancel_id: ArcStr,
433    pub order_id: OrderId,
434    pub reason: RejectReason,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
438#[cfg_attr(feature = "netidx", derive(Pack, FromValue))]
439pub enum CqgMessage {
440    Order(CqgOrder),
441    Cancel(Cancel),
442    CancelAll,
443    Ack(Ack),
444    Out(Out),
445    Fill(Result<Fill, AberrantFill>),
446    Reject(Reject),
447    CancelReject(CancelReject),
448    Folio(FolioMessage),
449    CqgTrades(Vec<CqgTrade>),
450    CqgAccountSummary(CqgAccountSummary),
451    CqgPositionStatus(CqgPositionStatus),
452    CqgOrderSnapshot(Arc<Vec<(i32, BitFlags<OrderStateFlags, u8>, Order)>>),
453}
454
455impl TryInto<OrderflowMessage> for &CqgMessage {
456    type Error = ();
457
458    fn try_into(self) -> Result<OrderflowMessage, ()> {
459        match self {
460            CqgMessage::Order(o) => Ok(OrderflowMessage::Order(**o)),
461            CqgMessage::Cancel(c) => Ok(OrderflowMessage::Cancel(*c)),
462            CqgMessage::CancelAll => {
463                Ok(OrderflowMessage::CancelAll(CancelAll { venue_id: None }))
464            }
465            CqgMessage::Ack(a) => Ok(OrderflowMessage::Ack(*a)),
466            CqgMessage::Out(o) => Ok(OrderflowMessage::Out(*o)),
467            CqgMessage::Fill(f) => Ok(OrderflowMessage::Fill(*f)),
468            CqgMessage::Reject(r) => Ok(OrderflowMessage::Reject(r.clone())),
469            CqgMessage::CancelReject(_)
470            | CqgMessage::Folio(_)
471            | CqgMessage::CqgTrades(_)
472            | CqgMessage::CqgAccountSummary(_)
473            | CqgMessage::CqgOrderSnapshot(..)
474            | CqgMessage::CqgPositionStatus(_) => Err(()),
475        }
476    }
477}
478
479impl TryInto<CqgMessage> for &OrderflowMessage {
480    type Error = ();
481
482    fn try_into(self) -> Result<CqgMessage, ()> {
483        match self {
484            OrderflowMessage::Order(o) => Ok(CqgMessage::Order(CqgOrder { order: *o })),
485            OrderflowMessage::Cancel(c) => Ok(CqgMessage::Cancel(*c)),
486            OrderflowMessage::CancelAll(_) => Ok(CqgMessage::CancelAll),
487            OrderflowMessage::Ack(a) => Ok(CqgMessage::Ack(*a)),
488            OrderflowMessage::Out(o) => Ok(CqgMessage::Out(*o)),
489            OrderflowMessage::Reject(r) => Ok(CqgMessage::Reject(r.clone())),
490            OrderflowMessage::Fill(f) => Ok(CqgMessage::Fill(*f)),
491        }
492    }
493}
494
495impl TryInto<FolioMessage> for &CqgMessage {
496    type Error = ();
497
498    fn try_into(self) -> Result<FolioMessage, ()> {
499        match self {
500            CqgMessage::Folio(f) => Ok(f.clone()),
501            _ => Err(()),
502        }
503    }
504}
505
506impl TryFrom<&FolioMessage> for CqgMessage {
507    type Error = ();
508
509    fn try_from(f: &FolioMessage) -> Result<Self, ()> {
510        Ok(Self::Folio(f.clone()))
511    }
512}