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