1use nautilus_model::events::{OrderSnapshot, PositionSnapshot};
17
18use super::json::{JsonFieldSpec, impl_json_arrow};
19
20const ORDER_SNAPSHOT_FIELDS: &[JsonFieldSpec] = &[
21 JsonFieldSpec::utf8("trader_id", false),
22 JsonFieldSpec::utf8("strategy_id", false),
23 JsonFieldSpec::utf8("instrument_id", false),
24 JsonFieldSpec::utf8("client_order_id", false),
25 JsonFieldSpec::utf8("venue_order_id", true),
26 JsonFieldSpec::utf8("position_id", true),
27 JsonFieldSpec::utf8("account_id", true),
28 JsonFieldSpec::utf8("last_trade_id", true),
29 JsonFieldSpec::utf8("order_type", false),
30 JsonFieldSpec::utf8("order_side", false),
31 JsonFieldSpec::utf8("quantity", false),
32 JsonFieldSpec::utf8("price", true),
33 JsonFieldSpec::utf8("trigger_price", true),
34 JsonFieldSpec::utf8("trigger_type", true),
35 JsonFieldSpec::utf8("limit_offset", true),
36 JsonFieldSpec::utf8("trailing_offset", true),
37 JsonFieldSpec::utf8("trailing_offset_type", true),
38 JsonFieldSpec::utf8("time_in_force", false),
39 JsonFieldSpec::u64("expire_time", true),
40 JsonFieldSpec::utf8("filled_qty", false),
41 JsonFieldSpec::utf8("liquidity_side", true),
42 JsonFieldSpec::decimal_str("avg_px", true),
43 JsonFieldSpec::decimal_str("slippage", true),
44 JsonFieldSpec::utf8_json("commissions", false),
45 JsonFieldSpec::utf8("status", false),
46 JsonFieldSpec::boolean("is_post_only", false),
47 JsonFieldSpec::boolean("is_reduce_only", false),
48 JsonFieldSpec::boolean("is_quote_quantity", false),
49 JsonFieldSpec::utf8("display_qty", true),
50 JsonFieldSpec::utf8("emulation_trigger", true),
51 JsonFieldSpec::utf8("trigger_instrument_id", true),
52 JsonFieldSpec::utf8("contingency_type", true),
53 JsonFieldSpec::utf8("order_list_id", true),
54 JsonFieldSpec::utf8_json("linked_order_ids", true),
55 JsonFieldSpec::utf8("parent_order_id", true),
56 JsonFieldSpec::utf8("exec_algorithm_id", true),
57 JsonFieldSpec::utf8_json("exec_algorithm_params", true),
58 JsonFieldSpec::utf8("exec_spawn_id", true),
59 JsonFieldSpec::utf8_json("tags", true),
60 JsonFieldSpec::utf8("init_id", false),
61 JsonFieldSpec::u64("ts_init", false),
62 JsonFieldSpec::u64("ts_last", false),
63 JsonFieldSpec::utf8("activation_price", true),
66];
67
68const POSITION_SNAPSHOT_FIELDS: &[JsonFieldSpec] = &[
69 JsonFieldSpec::utf8("trader_id", false),
70 JsonFieldSpec::utf8("strategy_id", false),
71 JsonFieldSpec::utf8("instrument_id", false),
72 JsonFieldSpec::utf8("position_id", false),
73 JsonFieldSpec::utf8("account_id", false),
74 JsonFieldSpec::utf8("opening_order_id", false),
75 JsonFieldSpec::utf8("closing_order_id", true),
76 JsonFieldSpec::utf8("entry", false),
77 JsonFieldSpec::utf8("side", false),
78 JsonFieldSpec::f64("signed_qty", false),
79 JsonFieldSpec::utf8("quantity", false),
80 JsonFieldSpec::utf8("peak_qty", false),
81 JsonFieldSpec::utf8("quote_currency", false),
82 JsonFieldSpec::utf8("base_currency", true),
83 JsonFieldSpec::utf8("settlement_currency", false),
84 JsonFieldSpec::f64("avg_px_open", false),
85 JsonFieldSpec::f64("avg_px_close", true),
86 JsonFieldSpec::f64("realized_return", true),
87 JsonFieldSpec::utf8("realized_pnl", true),
88 JsonFieldSpec::utf8("unrealized_pnl", true),
89 JsonFieldSpec::utf8_json("commissions", false),
90 JsonFieldSpec::u64("duration_ns", true),
91 JsonFieldSpec::u64("ts_opened", false),
92 JsonFieldSpec::u64("ts_closed", true),
93 JsonFieldSpec::u64("ts_init", false),
94 JsonFieldSpec::u64("ts_last", false),
95 JsonFieldSpec::utf8_json("replay_state", true),
96];
97
98impl_json_arrow!(instrument OrderSnapshot, "OrderSnapshot", ORDER_SNAPSHOT_FIELDS);
99impl_json_arrow!(instrument PositionSnapshot,
100 "PositionSnapshot",
101 POSITION_SNAPSHOT_FIELDS
102);
103
104#[cfg(test)]
105mod tests {
106 use std::str::FromStr;
107
108 use arrow::datatypes::DataType;
109 use nautilus_core::{DurationNanos, UnixNanos};
110 use nautilus_model::{
111 enums::{OrderSide, OrderType, PositionSide, TrailingOffsetType},
112 identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
113 orders::OrderTestBuilder,
114 types::{Currency, Money, Price, Quantity},
115 };
116 use rstest::rstest;
117 use rust_decimal::Decimal;
118 use rust_decimal_macros::dec;
119
120 use super::*;
121 use crate::arrow::{DecodeTypedFromRecordBatch, EncodeToRecordBatch, json::encode_batch};
122
123 #[rstest]
124 fn test_order_snapshot_round_trip_preserves_decimal_precision() {
125 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
126 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
127 .side(OrderSide::Buy)
128 .price(Price::from("50000"))
129 .trigger_price(Price::from("50500"))
130 .limit_offset(Decimal::from_str("0.123456789123456789").unwrap())
131 .trailing_offset(Decimal::from_str("0.987654321987654321").unwrap())
132 .trailing_offset_type(TrailingOffsetType::Price)
133 .quantity(Quantity::from("0.5"))
134 .build();
135 let snapshot = OrderSnapshot::from(order);
136 let metadata = snapshot.metadata();
137 let batch =
138 OrderSnapshot::encode_batch(&metadata, std::slice::from_ref(&snapshot)).unwrap();
139 let decoded = OrderSnapshot::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
140
141 assert_eq!(decoded, vec![snapshot]);
142 }
143
144 fn make_order_snapshot(avg_px: Option<Decimal>, slippage: Option<Decimal>) -> OrderSnapshot {
145 let order = OrderTestBuilder::new(OrderType::Limit)
146 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
147 .side(OrderSide::Buy)
148 .price(Price::from("50000"))
149 .quantity(Quantity::from("0.5"))
150 .build();
151 let mut snapshot = OrderSnapshot::from(order);
152 snapshot.avg_px = avg_px;
153 snapshot.slippage = slippage;
154 snapshot
155 }
156
157 fn legacy_float64_fields() -> Vec<JsonFieldSpec> {
159 ORDER_SNAPSHOT_FIELDS
160 .iter()
161 .map(|spec| match spec.name {
162 "avg_px" | "slippage" => JsonFieldSpec::f64(spec.name, spec.nullable),
163 _ => *spec,
164 })
165 .collect()
166 }
167
168 #[rstest]
169 fn test_order_snapshot_round_trip_preserves_exact_avg_px_and_slippage() {
170 let snapshot = make_order_snapshot(
173 Some(Decimal::from_str("1.6666666666666666666666666667").unwrap()),
174 Some(Decimal::from_str("0.0000000000000000000000000001").unwrap()),
175 );
176 let metadata = snapshot.metadata();
177 let batch =
178 OrderSnapshot::encode_batch(&metadata, std::slice::from_ref(&snapshot)).unwrap();
179
180 let avg_px_field = batch.schema().field_with_name("avg_px").unwrap().clone();
181 let slippage_field = batch.schema().field_with_name("slippage").unwrap().clone();
182 let decoded = OrderSnapshot::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
183
184 assert_eq!(avg_px_field.data_type(), &DataType::Utf8);
185 assert_eq!(slippage_field.data_type(), &DataType::Utf8);
186 assert_eq!(decoded, vec![snapshot]);
187 }
188
189 #[rstest]
190 fn test_order_snapshot_round_trip_null_avg_px_and_slippage() {
191 let snapshot = make_order_snapshot(None, None);
192 let metadata = snapshot.metadata();
193 let batch =
194 OrderSnapshot::encode_batch(&metadata, std::slice::from_ref(&snapshot)).unwrap();
195 let decoded = OrderSnapshot::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
196
197 assert_eq!(decoded, vec![snapshot]);
198 }
199
200 #[rstest]
201 fn test_order_snapshot_decodes_legacy_float64_columns() {
202 let snapshot = make_order_snapshot(Some(dec!(1.07)), Some(dec!(0.07)));
205 let metadata = snapshot.metadata();
206 let legacy_batch = encode_batch(
207 "OrderSnapshot",
208 &metadata,
209 std::slice::from_ref(&snapshot),
210 &legacy_float64_fields(),
211 )
212 .unwrap();
213
214 assert_eq!(
215 legacy_batch
216 .schema()
217 .field_with_name("avg_px")
218 .unwrap()
219 .data_type(),
220 &DataType::Float64
221 );
222
223 let decoded =
224 OrderSnapshot::decode_typed_batch(legacy_batch.schema().metadata(), legacy_batch)
225 .unwrap();
226
227 assert_eq!(decoded, vec![snapshot]);
228 }
229
230 #[rstest]
231 fn test_order_snapshot_decodes_legacy_float64_null_columns() {
232 let snapshot = make_order_snapshot(None, None);
233 let metadata = snapshot.metadata();
234 let legacy_batch = encode_batch(
235 "OrderSnapshot",
236 &metadata,
237 std::slice::from_ref(&snapshot),
238 &legacy_float64_fields(),
239 )
240 .unwrap();
241 let decoded =
242 OrderSnapshot::decode_typed_batch(legacy_batch.schema().metadata(), legacy_batch)
243 .unwrap();
244
245 assert_eq!(decoded, vec![snapshot]);
246 }
247
248 fn make_position_snapshot() -> PositionSnapshot {
249 PositionSnapshot {
250 trader_id: TraderId::from("TRADER-001"),
251 strategy_id: StrategyId::from("EMA-CROSS"),
252 instrument_id: InstrumentId::from("EURUSD.SIM"),
253 position_id: PositionId::from("P-001"),
254 account_id: AccountId::from("SIM-001"),
255 opening_order_id: ClientOrderId::from("O-1"),
256 closing_order_id: Some(ClientOrderId::from("O-2")),
257 entry: OrderSide::Buy,
258 side: PositionSide::Long,
259 signed_qty: 100.0,
260 quantity: Quantity::from("100"),
261 peak_qty: Quantity::from("100"),
262 quote_currency: Currency::USD(),
263 base_currency: Some(Currency::EUR()),
264 settlement_currency: Currency::USD(),
265 avg_px_open: 1.0500,
266 avg_px_close: Some(1.0600),
267 realized_return: Some(0.0095),
268 realized_pnl: Some(Money::new(100.0, Currency::USD())),
269 unrealized_pnl: Some(Money::new(50.0, Currency::USD())),
270 commissions: vec![Money::new(2.0, Currency::USD())],
271 duration_ns: Some(DurationNanos::from_hours(1)),
272 ts_opened: UnixNanos::from(1_000_000_000),
273 ts_closed: Some(UnixNanos::from(4_600_000_000)),
274 ts_init: UnixNanos::from(2_000_000_000),
275 ts_last: UnixNanos::from(4_600_000_000),
276 replay_state: None,
277 }
278 }
279
280 #[rstest]
281 fn test_position_snapshot_round_trip() {
282 let mut snapshot = make_position_snapshot();
283 snapshot.replay_state = Some(serde_json::json!({"fill_voids": []}));
284 let metadata = snapshot.metadata();
285 let batch =
286 PositionSnapshot::encode_batch(&metadata, std::slice::from_ref(&snapshot)).unwrap();
287 let decoded =
288 PositionSnapshot::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
289
290 assert_eq!(decoded, vec![snapshot]);
291 }
292
293 #[rstest]
294 fn test_position_snapshot_round_trip_null_optionals() {
295 let mut snapshot = make_position_snapshot();
296 snapshot.closing_order_id = None;
297 snapshot.base_currency = None;
298 snapshot.avg_px_close = None;
299 snapshot.realized_return = None;
300 snapshot.realized_pnl = None;
301 snapshot.unrealized_pnl = None;
302 snapshot.duration_ns = None;
303 snapshot.ts_closed = None;
304
305 let metadata = snapshot.metadata();
306 let batch =
307 PositionSnapshot::encode_batch(&metadata, std::slice::from_ref(&snapshot)).unwrap();
308 let decoded =
309 PositionSnapshot::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
310
311 assert_eq!(decoded, vec![snapshot]);
312 }
313}