nautilus_coinbase_intx/fix/
parse.rs1use chrono::{DateTime, Utc};
17use nautilus_core::{UnixNanos, time::get_atomic_clock_realtime};
18use nautilus_model::{
19 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType},
20 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, TradeId, VenueOrderId},
21 reports::{FillReport, OrderStatusReport},
22 types::{Currency, Money, Price, Quantity},
23};
24use ustr::Ustr;
25
26use super::messages::{FixMessage, fix_tag};
27use crate::common::{consts::COINBASE_INTX_VENUE, parse::parse_instrument_id};
28
29const DEFAULT_PRECISION: u8 = 8;
31
32pub fn convert_to_order_status_report(
38 message: &FixMessage,
39 account_id: AccountId,
40 ts_init: UnixNanos,
41) -> anyhow::Result<OrderStatusReport> {
42 let venue_order_id = VenueOrderId::new(message.get_field_checked(fix_tag::ORDER_ID)?);
43 let client_order_id = message
44 .get_field(fix_tag::CL_ORD_ID)
45 .map(ClientOrderId::new); let symbol = message.get_field_checked(fix_tag::SYMBOL)?;
48 let instrument_id = parse_instrument_id(Ustr::from(symbol));
49
50 let side = message.get_field_checked(fix_tag::SIDE)?;
51 let order_side = match side {
52 "1" => OrderSide::Buy,
53 "2" => OrderSide::Sell,
54 _ => anyhow::bail!("Unknown order side: {side}"),
55 };
56
57 let ord_type = message.get_field_checked(fix_tag::ORD_TYPE)?;
58 let order_type = match ord_type {
59 "1" => OrderType::Market,
60 "2" => OrderType::Limit,
61 "3" => OrderType::StopLimit,
62 "4" => OrderType::StopMarket,
63 _ => anyhow::bail!("Unknown order type: {ord_type}"),
64 };
65
66 let tif = message.get_field_checked(fix_tag::TIME_IN_FORCE)?;
67 let time_in_force = match tif {
68 "1" => TimeInForce::Gtc, "3" => TimeInForce::Ioc, "4" => TimeInForce::Fok, "6" => TimeInForce::Gtd, _ => anyhow::bail!("Unknown time in force: {tif}"),
73 };
74
75 let status = message.get_field_checked(fix_tag::ORD_STATUS)?;
76 let order_status = match status {
77 "0" => OrderStatus::Accepted, "1" => OrderStatus::PartiallyFilled,
79 "2" => OrderStatus::Filled,
80 "4" => OrderStatus::Canceled,
81 "5" => OrderStatus::Rejected,
82 "6" => OrderStatus::PendingCancel,
83 "8" => OrderStatus::Rejected,
84 "A" => OrderStatus::Submitted, "E" => OrderStatus::PendingUpdate, "C" => OrderStatus::Expired,
87 _ => anyhow::bail!("Unknown order status: {status}"),
88 };
89
90 let order_qty = message.get_field_checked(fix_tag::ORDER_QTY)?;
91 let quantity = Quantity::new(order_qty.parse::<f64>()?, DEFAULT_PRECISION);
92
93 let _leaves_qty = message.get_field_checked(fix_tag::LEAVES_QTY)?;
94 let cum_qty = message.get_field_checked(fix_tag::CUM_QTY)?;
95 let filled_qty = Quantity::new(cum_qty.parse::<f64>()?, DEFAULT_PRECISION);
96
97 let ts_last = if let Some(transact_time) = message.get_field(fix_tag::TRANSACT_TIME) {
100 parse_fix_timestamp(transact_time)?
101 } else {
102 ts_init
103 };
104
105 let ts_accepted = ts_last;
108
109 let mut report = OrderStatusReport::new(
111 account_id,
112 instrument_id,
113 client_order_id,
114 venue_order_id,
115 order_side,
116 order_type,
117 time_in_force,
118 order_status,
119 quantity,
120 filled_qty,
121 ts_accepted,
122 ts_last,
123 ts_init,
124 None, );
126
127 if let Some(price_str) = message.get_field(fix_tag::PRICE)
128 && let Ok(price_val) = price_str.parse::<f64>()
129 {
130 report = report.with_price(Price::new(price_val, DEFAULT_PRECISION));
131 }
132
133 if let Some(stop_px) = message.get_field(fix_tag::STOP_PX)
134 && let Ok(stop_val) = stop_px.parse::<f64>()
135 {
136 report = report.with_trigger_price(Price::new(stop_val, DEFAULT_PRECISION));
137 report = report.with_trigger_type(TriggerType::LastPrice);
138 }
139
140 if let Some(avg_px) = message.get_field(fix_tag::AVG_PX)
141 && let Ok(avg_val) = avg_px.parse::<f64>()
142 && avg_val > 0.0
143 {
144 report = report.with_avg_px(avg_val);
145 }
146
147 if let Some(exec_inst) = message.get_field(fix_tag::EXEC_INST) {
149 let flags: Vec<&str> = exec_inst.split(' ').collect();
151 for flag in flags {
152 match flag {
153 "6" => report = report.with_post_only(true), "E" => report = report.with_reduce_only(true), _ => {} }
157 }
158 }
159
160 if let Some(expire_time) = message.get_field(fix_tag::EXPIRE_TIME)
161 && let Ok(dt) = parse_fix_timestamp(expire_time)
162 {
163 report = report.with_expire_time(dt);
164 }
165
166 if let Some(text) = message.get_field(fix_tag::TEXT)
167 && !text.is_empty()
168 {
169 report = report.with_cancel_reason(text.to_string());
170 }
171
172 Ok(report)
173}
174
175pub fn convert_to_fill_report(
181 message: &FixMessage,
182 account_id: AccountId,
183 ts_init: UnixNanos,
184) -> anyhow::Result<FillReport> {
185 let client_order_id = message.get_field_checked(fix_tag::CL_ORD_ID)?;
186 let venue_order_id = message.get_field_checked(fix_tag::ORDER_ID)?;
187 let trade_id = message.get_field_checked(fix_tag::TRD_MATCH_ID)?;
188 let symbol = message.get_field_checked(fix_tag::SYMBOL)?;
189 let side_str = message.get_field_checked(fix_tag::SIDE)?;
190 let last_qty_str = message.get_field_checked(fix_tag::LAST_QTY)?;
191 let last_px_str = message.get_field_checked(fix_tag::LAST_PX)?;
192 let currency = message.get_field_checked(fix_tag::CURRENCY)?.parse()?;
193 let liquidity_indicator = message.get_field(fix_tag::LAST_LIQUIDITY_IND);
194
195 let mut commission = Money::new(0.0, currency);
196
197 if let Some(num_fees) = message.get_field(fix_tag::NO_MISC_FEES)
198 && let Ok(n) = num_fees.parse::<usize>()
199 {
200 if n > 0
202 && let (Some(fee_amt), Some(fee_curr)) = (
203 message.get_field(fix_tag::MISC_FEE_AMT),
204 message.get_field(fix_tag::MISC_FEE_CURR),
205 )
206 && let Ok(amt) = fee_amt.parse::<f64>()
207 {
208 let fee_currency = fee_curr
210 .parse::<Currency>()
211 .map_err(|e| anyhow::anyhow!("Invalid fee currency '{fee_curr}': {e}"))?;
212 commission = Money::new(amt, fee_currency);
213 }
214 }
215
216 let client_order_id = ClientOrderId::new(client_order_id);
217 let venue_order_id = VenueOrderId::new(venue_order_id);
218 let trade_id = TradeId::new(trade_id);
219
220 let order_side = match side_str {
221 "1" => OrderSide::Buy,
222 "2" => OrderSide::Sell,
223 _ => anyhow::bail!("Unknown order side: {side_str}"),
224 };
225
226 let last_qty = match last_qty_str.parse::<f64>() {
227 Ok(qty) => Quantity::new(qty, DEFAULT_PRECISION),
228 Err(e) => anyhow::bail!(format!("Invalid last quantity: {e}")),
229 };
230
231 let last_px = match last_px_str.parse::<f64>() {
232 Ok(px) => Price::new(px, DEFAULT_PRECISION),
233 Err(e) => anyhow::bail!(format!("Invalid last price: {e}")),
234 };
235
236 let liquidity_side = match liquidity_indicator {
237 Some("1") => LiquiditySide::Maker,
238 Some("2") => LiquiditySide::Taker,
239 _ => LiquiditySide::NoLiquiditySide,
240 };
241
242 let ts_event = if let Some(transact_time) = message.get_field(fix_tag::TRANSACT_TIME) {
244 if let Ok(dt) = DateTime::parse_from_str(transact_time, "%Y%m%d-%H:%M:%S%.3f") {
245 UnixNanos::from(dt.with_timezone(&Utc))
246 } else {
247 ts_init
248 }
249 } else {
250 ts_init
251 };
252
253 let instrument_id = InstrumentId::new(Symbol::from_str_unchecked(symbol), *COINBASE_INTX_VENUE);
254
255 let report = FillReport::new(
256 account_id,
257 instrument_id,
258 venue_order_id,
259 trade_id,
260 order_side,
261 last_qty,
262 last_px,
263 commission,
264 liquidity_side,
265 Some(client_order_id),
266 None, ts_event,
268 get_atomic_clock_realtime().get_time_ns(),
269 None, );
271
272 Ok(report)
273}
274
275fn parse_fix_timestamp(timestamp: &str) -> Result<UnixNanos, anyhow::Error> {
277 let dt = DateTime::parse_from_str(timestamp, "%Y%m%d-%H:%M:%S%.3f")?;
278 Ok(UnixNanos::from(dt.with_timezone(&Utc)))
279}