Skip to main content

nautilus_betfair/
data_types.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Betfair-specific custom data types.
17//!
18//! These types carry Betfair domain data through the Nautilus data engine as
19//! [`CustomData`](nautilus_model::data::CustomData). Each type uses the
20//! `#[custom_data(pyo3)]` and `#[arrow_custom_data(pyo3)]` macros, which generate the model and
21//! Arrow persistence implementations respectively.
22//!
23//! Call [`register_betfair_custom_data`] once (e.g. during client `connect()`)
24//! to register all types for JSON and Arrow encoding.
25//!
26//! Absent optional race telemetry values use `f64::NAN` as the sentinel.
27
28use nautilus_core::UnixNanos;
29use nautilus_model::{
30    custom_data,
31    enums::{BookAction, OrderSide},
32    identifiers::InstrumentId,
33};
34use nautilus_serialization::arrow_custom_data;
35use rust_decimal::Decimal;
36
37/// Serde adapter for f64 fields that use NaN as a sentinel for absent values.
38/// Serializes NaN as JSON `null` and deserializes `null` back to NaN,
39/// avoiding `serde_json` errors on non-finite floats.
40mod nan_as_null {
41    pub(super) fn serialize<S: serde::Serializer>(v: &f64, s: S) -> Result<S::Ok, S::Error> {
42        if v.is_nan() {
43            s.serialize_none()
44        } else {
45            s.serialize_f64(*v)
46        }
47    }
48
49    pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
50        use serde::Deserialize;
51        Ok(Option::<f64>::deserialize(d)?.unwrap_or(f64::NAN))
52    }
53}
54
55/// Betfair ticker data from MCM runner changes.
56///
57/// Carries last traded price, traded volume, and starting price
58/// near/far values per runner.
59#[arrow_custom_data(pyo3)]
60#[custom_data(pyo3)]
61pub struct BetfairTicker {
62    /// The instrument ID for this ticker.
63    pub instrument_id: InstrumentId,
64    /// Last traded price.
65    pub last_traded_price: Option<Decimal>,
66    /// Total traded volume.
67    pub traded_volume: Option<Decimal>,
68    /// Starting price near (projected BSP from matched portion).
69    pub starting_price_near: Option<Decimal>,
70    /// Starting price far (projected BSP from unmatched portion).
71    pub starting_price_far: Option<Decimal>,
72    /// UNIX timestamp (nanoseconds) when the data event occurred.
73    pub ts_event: UnixNanos,
74    /// UNIX timestamp (nanoseconds) when the instance was initialized.
75    pub ts_init: UnixNanos,
76}
77
78/// Realized Betfair Starting Price (BSP) for a runner.
79///
80/// Emitted from the market definition when a runner's BSP is determined.
81#[arrow_custom_data(pyo3)]
82#[custom_data(pyo3)]
83pub struct BetfairStartingPrice {
84    /// The instrument ID for this starting price.
85    pub instrument_id: InstrumentId,
86    /// The realized best starting price value.
87    pub bsp: Decimal,
88    /// UNIX timestamp (nanoseconds) when the data event occurred.
89    pub ts_event: UnixNanos,
90    /// UNIX timestamp (nanoseconds) when the instance was initialized.
91    pub ts_init: UnixNanos,
92}
93
94/// BSP order book delta from starting price back/lay arrays.
95///
96/// Mirrors `OrderBookDelta` fields as a custom data type so strategies
97/// can subscribe specifically to BSP book updates (spb/spl) separately
98/// from the exchange order book (atb/atl).
99#[arrow_custom_data(pyo3)]
100#[custom_data(pyo3)]
101pub struct BetfairBspBookDelta {
102    /// The instrument ID for this BSP delta.
103    pub instrument_id: InstrumentId,
104    /// The book action (add/update/delete/clear).
105    #[custom_data_field(native_enum)]
106    pub action: BookAction,
107    /// The order side.
108    #[custom_data_field(native_enum)]
109    pub side: OrderSide,
110    /// The price level.
111    pub price: Decimal,
112    /// The size at this price level.
113    pub size: Decimal,
114    /// UNIX timestamp (nanoseconds) when the data event occurred.
115    pub ts_event: UnixNanos,
116    /// UNIX timestamp (nanoseconds) when the instance was initialized.
117    pub ts_init: UnixNanos,
118}
119
120/// Marker emitted after all changes in an MCM sequence are processed.
121///
122/// Strategies can use this to know when a coherent set of market updates
123/// has been fully delivered.
124#[arrow_custom_data(pyo3)]
125#[custom_data(pyo3)]
126pub struct BetfairSequenceCompleted {
127    /// UNIX timestamp (nanoseconds) when the data event occurred.
128    pub ts_event: UnixNanos,
129    /// UNIX timestamp (nanoseconds) when the instance was initialized.
130    pub ts_init: UnixNanos,
131}
132
133/// Betfair order void event (e.g. VAR void).
134///
135/// Published when a matched bet is retroactively voided by Betfair, such as
136/// when a goal is disallowed following a VAR review.
137#[arrow_custom_data(pyo3)]
138#[custom_data(pyo3)]
139pub struct BetfairOrderVoided {
140    /// The instrument ID for the voided order.
141    pub instrument_id: InstrumentId,
142    /// The client order ID.
143    pub client_order_id: String,
144    /// The venue (Betfair) order ID (bet ID).
145    pub venue_order_id: String,
146    /// The size that was voided.
147    pub size_voided: Decimal,
148    /// The order price.
149    pub price: Decimal,
150    /// The original order size.
151    pub size: Decimal,
152    /// The order side ("BACK" or "LAY").
153    pub side: String,
154    /// The average price matched.
155    pub avg_price_matched: Option<Decimal>,
156    /// The total size matched.
157    pub size_matched: Option<Decimal>,
158    /// The void reason. Empty string if absent.
159    pub reason: String,
160    /// UNIX timestamp (nanoseconds) when the data event occurred.
161    pub ts_event: UnixNanos,
162    /// UNIX timestamp (nanoseconds) when the instance was initialized.
163    pub ts_init: UnixNanos,
164}
165
166/// GPS tracking data for a single runner from RCM (Race Change Messages).
167///
168/// Betfair's Total Performance Data (TPD) provides real-time GPS positions,
169/// speed, and stride frequency for each runner in supported races.
170#[arrow_custom_data(pyo3)]
171#[custom_data(pyo3)]
172pub struct BetfairRaceRunnerData {
173    /// Race identifier (e.g. "28587288.1650").
174    pub race_id: String,
175    /// Betfair market identifier.
176    pub market_id: String,
177    /// Betfair selection identifier.
178    pub selection_id: i64,
179    /// GPS latitude coordinate. `f64::NAN` if absent.
180    #[serde(
181        serialize_with = "nan_as_null::serialize",
182        deserialize_with = "nan_as_null::deserialize"
183    )]
184    pub latitude: f64,
185    /// GPS longitude coordinate. `f64::NAN` if absent.
186    #[serde(
187        serialize_with = "nan_as_null::serialize",
188        deserialize_with = "nan_as_null::deserialize"
189    )]
190    pub longitude: f64,
191    /// Speed in m/s (Doppler-derived). `f64::NAN` if absent.
192    #[serde(
193        serialize_with = "nan_as_null::serialize",
194        deserialize_with = "nan_as_null::deserialize"
195    )]
196    pub speed: f64,
197    /// Distance to finish in meters. `f64::NAN` if absent.
198    #[serde(
199        serialize_with = "nan_as_null::serialize",
200        deserialize_with = "nan_as_null::deserialize"
201    )]
202    pub progress: f64,
203    /// Stride frequency in Hz. `f64::NAN` if absent.
204    #[serde(
205        serialize_with = "nan_as_null::serialize",
206        deserialize_with = "nan_as_null::deserialize"
207    )]
208    pub stride_frequency: f64,
209    /// UNIX timestamp (nanoseconds) when the data event occurred.
210    pub ts_event: UnixNanos,
211    /// UNIX timestamp (nanoseconds) when the instance was initialized.
212    pub ts_init: UnixNanos,
213}
214
215/// Race-level progress from RCM (Race Change Messages).
216///
217/// Provides sectional timing, race order, and obstacle data for the
218/// overall race rather than individual runners.
219#[arrow_custom_data(pyo3)]
220#[custom_data(pyo3)]
221pub struct BetfairRaceProgress {
222    /// Race identifier (e.g. "28587288.1650").
223    pub race_id: String,
224    /// Betfair market identifier.
225    pub market_id: String,
226    /// Gate/sectional name (e.g. "1f", "2f", "Finish"). Empty if absent.
227    pub gate_name: String,
228    /// Sectional time in seconds. `f64::NAN` if absent.
229    #[serde(
230        serialize_with = "nan_as_null::serialize",
231        deserialize_with = "nan_as_null::deserialize"
232    )]
233    pub sectional_time: f64,
234    /// Running time since race start in seconds. `f64::NAN` if absent.
235    #[serde(
236        serialize_with = "nan_as_null::serialize",
237        deserialize_with = "nan_as_null::deserialize"
238    )]
239    pub running_time: f64,
240    /// Speed of lead horse in m/s. `f64::NAN` if absent.
241    #[serde(
242        serialize_with = "nan_as_null::serialize",
243        deserialize_with = "nan_as_null::deserialize"
244    )]
245    pub speed: f64,
246    /// Distance to finish for leading horse in meters. `f64::NAN` if absent.
247    #[serde(
248        serialize_with = "nan_as_null::serialize",
249        deserialize_with = "nan_as_null::deserialize"
250    )]
251    pub progress: f64,
252    /// Runner order by selection ID (JSON-encoded array). Empty if absent.
253    pub order: String,
254    /// Jump obstacles (JSON-encoded array of {"J":int,"L":float}). Empty if absent.
255    pub jumps: String,
256    /// UNIX timestamp (nanoseconds) when the data event occurred.
257    pub ts_event: UnixNanos,
258    /// UNIX timestamp (nanoseconds) when the instance was initialized.
259    pub ts_init: UnixNanos,
260}
261
262#[arrow_custom_data(pyo3)]
263#[custom_data(pyo3)]
264pub struct BetfairCricketMatch {
265    /// Betfair event identifier.
266    pub event_id: String,
267    /// Betfair market identifier.
268    pub market_id: String,
269    /// Fixture metadata (JSON-encoded object). Empty if absent.
270    pub fixture_info: String,
271    /// Home team metadata (JSON-encoded object). Empty if absent.
272    pub home_team: String,
273    /// Away team metadata (JSON-encoded object). Empty if absent.
274    pub away_team: String,
275    /// Match statistics (JSON-encoded object). Empty if absent.
276    pub match_stats: String,
277    /// Match incidents (JSON-encoded object). Empty if absent.
278    pub incident_list_wrapper: String,
279    /// UNIX timestamp (nanoseconds) when the data event occurred.
280    pub ts_event: UnixNanos,
281    /// UNIX timestamp (nanoseconds) when the instance was initialized.
282    pub ts_init: UnixNanos,
283}
284
285/// Registers all Betfair custom data types for JSON and Arrow encoding.
286///
287/// This must be called once before emitting or persisting Betfair custom data.
288/// Safe to call multiple times (idempotent via internal `Once` guards).
289pub fn register_betfair_custom_data() {
290    nautilus_serialization::ensure_custom_data_registered::<BetfairTicker>();
291    nautilus_serialization::ensure_custom_data_registered::<BetfairStartingPrice>();
292    nautilus_serialization::ensure_custom_data_registered::<BetfairBspBookDelta>();
293    nautilus_serialization::ensure_custom_data_registered::<BetfairSequenceCompleted>();
294    nautilus_serialization::ensure_custom_data_registered::<BetfairOrderVoided>();
295    nautilus_serialization::ensure_custom_data_registered::<BetfairRaceRunnerData>();
296    nautilus_serialization::ensure_custom_data_registered::<BetfairRaceProgress>();
297    nautilus_serialization::ensure_custom_data_registered::<BetfairCricketMatch>();
298}
299
300#[cfg(test)]
301mod tests {
302    use nautilus_serialization::arrow::ArrowSchemaProvider;
303    use rstest::rstest;
304    use rust_decimal::Decimal;
305
306    use super::*;
307
308    #[rstest]
309    fn test_betfair_ticker_schema() {
310        let schema = BetfairTicker::get_schema(None);
311        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
312        assert!(field_names.contains(&"instrument_id".to_string()));
313        assert!(field_names.contains(&"last_traded_price".to_string()));
314        assert!(field_names.contains(&"traded_volume".to_string()));
315        assert!(field_names.contains(&"starting_price_near".to_string()));
316        assert!(field_names.contains(&"starting_price_far".to_string()));
317        assert!(field_names.contains(&"ts_event".to_string()));
318        assert!(field_names.contains(&"ts_init".to_string()));
319    }
320
321    #[rstest]
322    fn test_betfair_starting_price_schema() {
323        let schema = BetfairStartingPrice::get_schema(None);
324        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
325        assert!(field_names.contains(&"instrument_id".to_string()));
326        assert!(field_names.contains(&"bsp".to_string()));
327        assert!(field_names.contains(&"ts_event".to_string()));
328        assert!(field_names.contains(&"ts_init".to_string()));
329    }
330
331    #[rstest]
332    fn test_betfair_bsp_book_delta_schema() {
333        let schema = BetfairBspBookDelta::get_schema(None);
334        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
335        assert!(field_names.contains(&"instrument_id".to_string()));
336        assert!(field_names.contains(&"action".to_string()));
337        assert!(field_names.contains(&"side".to_string()));
338        assert!(field_names.contains(&"price".to_string()));
339        assert!(field_names.contains(&"size".to_string()));
340        assert!(field_names.contains(&"ts_event".to_string()));
341        assert!(field_names.contains(&"ts_init".to_string()));
342        assert_eq!(
343            schema.field_with_name("action").unwrap().data_type(),
344            &nautilus_serialization::arrow::enum_dictionary_data_type(),
345        );
346        assert_eq!(
347            schema.field_with_name("side").unwrap().data_type(),
348            &nautilus_serialization::arrow::enum_dictionary_data_type(),
349        );
350    }
351
352    #[rstest]
353    fn test_betfair_sequence_completed_schema() {
354        let schema = BetfairSequenceCompleted::get_schema(None);
355        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
356        assert!(field_names.contains(&"ts_event".to_string()));
357        assert!(field_names.contains(&"ts_init".to_string()));
358    }
359
360    #[rstest]
361    fn test_betfair_order_voided_schema() {
362        let schema = BetfairOrderVoided::get_schema(None);
363        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
364        assert!(field_names.contains(&"instrument_id".to_string()));
365        assert!(field_names.contains(&"client_order_id".to_string()));
366        assert!(field_names.contains(&"venue_order_id".to_string()));
367        assert!(field_names.contains(&"size_voided".to_string()));
368        assert!(field_names.contains(&"reason".to_string()));
369    }
370
371    #[rstest]
372    fn test_register_betfair_custom_data_is_idempotent() {
373        register_betfair_custom_data();
374        register_betfair_custom_data();
375    }
376
377    #[rstest]
378    fn test_betfair_race_runner_data_schema() {
379        let schema = BetfairRaceRunnerData::get_schema(None);
380        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
381        assert!(field_names.contains(&"race_id".to_string()));
382        assert!(field_names.contains(&"market_id".to_string()));
383        assert!(field_names.contains(&"selection_id".to_string()));
384        assert!(field_names.contains(&"latitude".to_string()));
385        assert!(field_names.contains(&"longitude".to_string()));
386        assert!(field_names.contains(&"speed".to_string()));
387        assert!(field_names.contains(&"progress".to_string()));
388        assert!(field_names.contains(&"stride_frequency".to_string()));
389    }
390
391    #[rstest]
392    fn test_betfair_race_progress_schema() {
393        let schema = BetfairRaceProgress::get_schema(None);
394        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
395        assert!(field_names.contains(&"race_id".to_string()));
396        assert!(field_names.contains(&"market_id".to_string()));
397        assert!(field_names.contains(&"gate_name".to_string()));
398        assert!(field_names.contains(&"sectional_time".to_string()));
399        assert!(field_names.contains(&"running_time".to_string()));
400        assert!(field_names.contains(&"speed".to_string()));
401        assert!(field_names.contains(&"progress".to_string()));
402        assert!(field_names.contains(&"order".to_string()));
403        assert!(field_names.contains(&"jumps".to_string()));
404    }
405
406    #[rstest]
407    fn test_betfair_cricket_match_schema() {
408        let schema = BetfairCricketMatch::get_schema(None);
409        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
410        assert!(field_names.contains(&"event_id".to_string()));
411        assert!(field_names.contains(&"market_id".to_string()));
412        assert!(field_names.contains(&"match_stats".to_string()));
413        assert!(field_names.contains(&"incident_list_wrapper".to_string()));
414    }
415
416    #[rstest]
417    fn test_race_runner_data_nan_json_roundtrip() {
418        let data = BetfairRaceRunnerData::new(
419            "28587288.1650".to_string(),
420            "1.1234567".to_string(),
421            7390417,
422            51.4189543,
423            -0.4058491,
424            17.8,
425            f64::NAN,
426            f64::NAN,
427            UnixNanos::from(1_000_000_000u64),
428            UnixNanos::from(1_000_000_000u64),
429        );
430
431        let json = serde_json::to_string(&data).unwrap();
432        assert!(json.contains("\"progress\":null"));
433        assert!(json.contains("\"stride_frequency\":null"));
434        assert!(json.contains("\"latitude\":51.4189543"));
435
436        let parsed: BetfairRaceRunnerData = serde_json::from_str(&json).unwrap();
437        assert!(parsed.progress.is_nan());
438        assert!(parsed.stride_frequency.is_nan());
439        assert_eq!(parsed.latitude, 51.4189543);
440        assert_eq!(parsed.selection_id, 7390417);
441    }
442
443    #[rstest]
444    fn test_betfair_ticker_optional_decimal_json_roundtrip() {
445        let ticker = BetfairTicker::new(
446            InstrumentId::from("1.234-56789-0.0.BETFAIR"),
447            Some(Decimal::new(15, 1)),
448            Some(Decimal::new(100, 0)),
449            None,
450            None,
451            UnixNanos::from(1_000_000_000u64),
452            UnixNanos::from(1_000_000_000u64),
453        );
454
455        let json = serde_json::to_string(&ticker).unwrap();
456        assert!(json.contains("\"starting_price_near\":null"));
457        assert!(json.contains("\"starting_price_far\":null"));
458
459        let parsed: BetfairTicker = serde_json::from_str(&json).unwrap();
460        assert!(parsed.starting_price_near.is_none());
461        assert!(parsed.starting_price_far.is_none());
462        assert_eq!(parsed.last_traded_price, Some(Decimal::new(15, 1)));
463        assert_eq!(parsed.traded_volume, Some(Decimal::new(100, 0)));
464    }
465
466    #[rstest]
467    fn test_betfair_ticker_optional_decimal_arrow_roundtrip() {
468        use arrow::{array::Array, datatypes::DataType};
469        use nautilus_model::data::Data;
470        use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
471
472        let original = BetfairTicker::new(
473            InstrumentId::from("1.234-56789-0.0.BETFAIR"),
474            Some(Decimal::new(15, 1)),
475            Some(Decimal::new(123_456_789_012_345_678, 16)),
476            None,
477            Some(Decimal::new(425, 2)),
478            UnixNanos::from(1_000_000_000),
479            UnixNanos::from(1_000_000_001),
480        );
481        let metadata = original.metadata();
482        let batch =
483            BetfairTicker::encode_batch(&metadata, std::slice::from_ref(&original)).unwrap();
484        let schema = batch.schema();
485
486        assert_eq!(
487            schema
488                .field_with_name("last_traded_price")
489                .unwrap()
490                .data_type(),
491            &DataType::Decimal128(38, 16),
492        );
493        assert!(
494            batch
495                .column_by_name("starting_price_near")
496                .unwrap()
497                .is_null(0)
498        );
499
500        let decoded = BetfairTicker::decode_data_batch(schema.metadata(), batch).unwrap();
501        let Data::Custom(custom) = &decoded[0] else {
502            panic!("expected custom data");
503        };
504        let decoded = custom
505            .data
506            .as_any()
507            .downcast_ref::<BetfairTicker>()
508            .unwrap();
509
510        assert_eq!(decoded, &original);
511    }
512
513    #[rstest]
514    fn test_betfair_starting_price_decimal_json_roundtrip() {
515        let starting_price = BetfairStartingPrice::new(
516            InstrumentId::from("1.234-56789-0.0.BETFAIR"),
517            Decimal::new(573, 2),
518            UnixNanos::from(1_000_000_000u64),
519            UnixNanos::from(1_000_000_000u64),
520        );
521
522        let json = serde_json::to_string(&starting_price).unwrap();
523        let parsed: BetfairStartingPrice = serde_json::from_str(&json).unwrap();
524
525        assert_eq!(parsed.instrument_id, starting_price.instrument_id);
526        assert_eq!(parsed.bsp, Decimal::new(573, 2));
527        assert_eq!(parsed.ts_event, starting_price.ts_event);
528        assert_eq!(parsed.ts_init, starting_price.ts_init);
529    }
530
531    #[rstest]
532    fn test_betfair_bsp_book_delta_decimal_json_roundtrip() {
533        let delta = BetfairBspBookDelta::new(
534            InstrumentId::from("1.234-56789-0.0.BETFAIR"),
535            BookAction::Update,
536            OrderSide::Buy,
537            Decimal::new(1000, 0),
538            Decimal::new(3338, 2),
539            UnixNanos::from(1_000_000_000u64),
540            UnixNanos::from(1_000_000_000u64),
541        );
542
543        let json = serde_json::to_string(&delta).unwrap();
544        let parsed: BetfairBspBookDelta = serde_json::from_str(&json).unwrap();
545
546        assert_eq!(parsed.instrument_id, delta.instrument_id);
547        assert_eq!(parsed.action, delta.action);
548        assert_eq!(parsed.side, delta.side);
549        assert_eq!(parsed.price, Decimal::new(1000, 0));
550        assert_eq!(parsed.size, Decimal::new(3338, 2));
551        assert_eq!(parsed.ts_event, delta.ts_event);
552        assert_eq!(parsed.ts_init, delta.ts_init);
553    }
554
555    #[rstest]
556    fn test_betfair_order_voided_decimal_json_roundtrip() {
557        let voided = BetfairOrderVoided::new(
558            InstrumentId::from("1.234-56789-0.0.BETFAIR"),
559            "client-001".to_string(),
560            "430069890490".to_string(),
561            Decimal::new(40, 0),
562            Decimal::new(25, 1),
563            Decimal::new(100, 0),
564            "BACK".to_string(),
565            Some(Decimal::new(25, 1)),
566            Some(Decimal::new(60, 0)),
567            "VAR".to_string(),
568            UnixNanos::from(1_000_000_000u64),
569            UnixNanos::from(1_000_000_000u64),
570        );
571
572        let json = serde_json::to_string(&voided).unwrap();
573        let parsed: BetfairOrderVoided = serde_json::from_str(&json).unwrap();
574
575        assert_eq!(parsed.instrument_id, voided.instrument_id);
576        assert_eq!(parsed.client_order_id, voided.client_order_id);
577        assert_eq!(parsed.venue_order_id, voided.venue_order_id);
578        assert_eq!(parsed.size_voided, Decimal::new(40, 0));
579        assert_eq!(parsed.price, Decimal::new(25, 1));
580        assert_eq!(parsed.size, Decimal::new(100, 0));
581        assert_eq!(parsed.side, "BACK");
582        assert_eq!(parsed.avg_price_matched, Some(Decimal::new(25, 1)));
583        assert_eq!(parsed.size_matched, Some(Decimal::new(60, 0)));
584        assert_eq!(parsed.reason, "VAR");
585        assert_eq!(parsed.ts_event, voided.ts_event);
586        assert_eq!(parsed.ts_init, voided.ts_init);
587    }
588}