Skip to main content

nautilus_serialization/arrow/display/
instrument.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//! Display-mode Arrow encoder for [`InstrumentAny`].
17//!
18//! Emits a single schema built from the common [`Instrument`] trait surface,
19//! plus the dated/option accessors (`strike_price`, `activation_ns`,
20//! `expiration_ns`, `option_kind`) that are uniformly reachable across
21//! variants. Variants that do not expose a given accessor emit a null for
22//! that row, so mixed-type instrument batches (spot, perp, future, option,
23//! equity, etc.) flow through one Perspective table.
24//!
25//! Variant-only metadata that is not reachable through the trait (e.g.
26//! `BettingInstrument::market_id`, `BinaryOption::outcome`,
27//! `FuturesSpread::strategy_type`) is intentionally not emitted. Consumers
28//! that need those fields should encode the concrete variant through the
29//! FixedSizeBinary encoders in the parent [`crate::arrow`] module.
30
31use std::sync::Arc;
32
33use arrow::{
34    array::{
35        BooleanBuilder, Float64Builder, StringBuilder, TimestampNanosecondBuilder, UInt8Builder,
36    },
37    datatypes::Schema,
38    error::ArrowError,
39    record_batch::RecordBatch,
40};
41use nautilus_model::instruments::{Instrument, InstrumentAny};
42use rust_decimal::prelude::ToPrimitive;
43
44use super::{
45    bool_field, float64_field, money_to_f64, price_to_f64, quantity_to_f64, timestamp_field,
46    uint8_field, unix_nanos_to_i64, utf8_field,
47};
48
49/// Returns the display-mode Arrow schema for [`InstrumentAny`].
50#[must_use]
51pub fn instrument_schema() -> Schema {
52    Schema::new(vec![
53        utf8_field("instrument_id", false),
54        utf8_field("symbol", false),
55        utf8_field("venue", false),
56        utf8_field("instrument_type", false),
57        utf8_field("raw_symbol", false),
58        utf8_field("asset_class", false),
59        utf8_field("instrument_class", false),
60        utf8_field("underlying", true),
61        utf8_field("base_currency", true),
62        utf8_field("quote_currency", false),
63        utf8_field("settlement_currency", false),
64        utf8_field("isin", true),
65        utf8_field("option_kind", true),
66        utf8_field("exchange", true),
67        float64_field("strike_price", true),
68        timestamp_field("activation_ns", true),
69        timestamp_field("expiration_ns", true),
70        bool_field("is_inverse", false),
71        bool_field("is_quanto", false),
72        uint8_field("price_precision", false),
73        uint8_field("size_precision", false),
74        float64_field("price_increment", false),
75        float64_field("size_increment", false),
76        float64_field("multiplier", false),
77        float64_field("lot_size", true),
78        float64_field("max_quantity", true),
79        float64_field("min_quantity", true),
80        float64_field("max_notional_amount", true),
81        utf8_field("max_notional_currency", true),
82        float64_field("min_notional_amount", true),
83        utf8_field("min_notional_currency", true),
84        float64_field("max_price", true),
85        float64_field("min_price", true),
86        float64_field("margin_init", false),
87        float64_field("margin_maint", false),
88        float64_field("maker_fee", false),
89        float64_field("taker_fee", false),
90        timestamp_field("ts_event", false),
91        timestamp_field("ts_init", false),
92    ])
93}
94
95/// Returns a stable name for the [`InstrumentAny`] variant.
96fn instrument_type_name(instrument: &InstrumentAny) -> &'static str {
97    match instrument {
98        InstrumentAny::Betting(_) => "BettingInstrument",
99        InstrumentAny::BinaryOption(_) => "BinaryOption",
100        InstrumentAny::Cfd(_) => "Cfd",
101        InstrumentAny::Commodity(_) => "Commodity",
102        InstrumentAny::CryptoFuture(_) => "CryptoFuture",
103        InstrumentAny::CryptoFuturesSpread(_) => "CryptoFuturesSpread",
104        InstrumentAny::CryptoOption(_) => "CryptoOption",
105        InstrumentAny::CryptoOptionSpread(_) => "CryptoOptionSpread",
106        InstrumentAny::CryptoPerpetual(_) => "CryptoPerpetual",
107        InstrumentAny::CurrencyPair(_) => "CurrencyPair",
108        InstrumentAny::Equity(_) => "Equity",
109        InstrumentAny::FuturesContract(_) => "FuturesContract",
110        InstrumentAny::FuturesSpread(_) => "FuturesSpread",
111        InstrumentAny::IndexInstrument(_) => "IndexInstrument",
112        InstrumentAny::OptionContract(_) => "OptionContract",
113        InstrumentAny::OptionSpread(_) => "OptionSpread",
114        InstrumentAny::PerpetualContract(_) => "PerpetualContract",
115        InstrumentAny::TokenizedAsset(_) => "TokenizedAsset",
116    }
117}
118
119/// Encodes instruments as a display-friendly Arrow [`RecordBatch`].
120///
121/// Emits a single schema built from the common [`Instrument`] trait surface.
122/// `Utf8` columns carry identifiers and enum names, `Float64` columns carry
123/// prices/quantities/fees, `Timestamp(Nanosecond)` columns carry activation,
124/// expiration, and bookkeeping timestamps, and `Boolean` columns carry
125/// `is_inverse`/`is_quanto`. Trait accessors that are not applicable to a
126/// row (e.g. `strike_price` on a spot pair) emit as nulls, so mixed-type
127/// batches round-trip cleanly. Variant-only metadata not reachable through
128/// the trait is intentionally omitted; see the module-level comment.
129///
130/// Returns an empty [`RecordBatch`] with the correct schema when `data` is empty.
131///
132/// # Errors
133///
134/// Returns an [`ArrowError`] if the Arrow `RecordBatch` cannot be constructed.
135pub fn encode_instruments(data: &[InstrumentAny]) -> Result<RecordBatch, ArrowError> {
136    let mut instrument_id = StringBuilder::new();
137    let mut symbol = StringBuilder::new();
138    let mut venue = StringBuilder::new();
139    let mut instrument_type = StringBuilder::new();
140    let mut raw_symbol = StringBuilder::new();
141    let mut asset_class = StringBuilder::new();
142    let mut instrument_class = StringBuilder::new();
143    let mut underlying = StringBuilder::new();
144    let mut base_currency = StringBuilder::new();
145    let mut quote_currency = StringBuilder::new();
146    let mut settlement_currency = StringBuilder::new();
147    let mut isin = StringBuilder::new();
148    let mut option_kind = StringBuilder::new();
149    let mut exchange = StringBuilder::new();
150    let mut strike_price = Float64Builder::with_capacity(data.len());
151    let mut activation_ns = TimestampNanosecondBuilder::with_capacity(data.len());
152    let mut expiration_ns = TimestampNanosecondBuilder::with_capacity(data.len());
153    let mut is_inverse = BooleanBuilder::with_capacity(data.len());
154    let mut is_quanto = BooleanBuilder::with_capacity(data.len());
155    let mut price_precision = UInt8Builder::with_capacity(data.len());
156    let mut size_precision = UInt8Builder::with_capacity(data.len());
157    let mut price_increment = Float64Builder::with_capacity(data.len());
158    let mut size_increment = Float64Builder::with_capacity(data.len());
159    let mut multiplier = Float64Builder::with_capacity(data.len());
160    let mut lot_size = Float64Builder::with_capacity(data.len());
161    let mut max_quantity = Float64Builder::with_capacity(data.len());
162    let mut min_quantity = Float64Builder::with_capacity(data.len());
163    let mut max_notional_amount = Float64Builder::with_capacity(data.len());
164    let mut max_notional_currency = StringBuilder::new();
165    let mut min_notional_amount = Float64Builder::with_capacity(data.len());
166    let mut min_notional_currency = StringBuilder::new();
167    let mut max_price = Float64Builder::with_capacity(data.len());
168    let mut min_price = Float64Builder::with_capacity(data.len());
169    let mut margin_init = Float64Builder::with_capacity(data.len());
170    let mut margin_maint = Float64Builder::with_capacity(data.len());
171    let mut maker_fee = Float64Builder::with_capacity(data.len());
172    let mut taker_fee = Float64Builder::with_capacity(data.len());
173    let mut ts_event = TimestampNanosecondBuilder::with_capacity(data.len());
174    let mut ts_init = TimestampNanosecondBuilder::with_capacity(data.len());
175
176    for instrument in data {
177        instrument_id.append_value(instrument.id().to_string());
178        symbol.append_value(instrument.symbol());
179        venue.append_value(instrument.venue());
180        instrument_type.append_value(instrument_type_name(instrument));
181        raw_symbol.append_value(instrument.raw_symbol());
182        asset_class.append_value(format!("{}", instrument.asset_class()));
183        instrument_class.append_value(format!("{}", instrument.instrument_class()));
184        underlying.append_option(instrument.underlying().map(|v| v.to_string()));
185        base_currency.append_option(instrument.base_currency().map(|v| v.to_string()));
186        quote_currency.append_value(instrument.quote_currency().to_string());
187        settlement_currency.append_value(instrument.settlement_currency().to_string());
188        isin.append_option(instrument.isin().map(|v| v.to_string()));
189        option_kind.append_option(instrument.option_kind().map(|v| format!("{v}")));
190        exchange.append_option(instrument.exchange().map(|v| v.to_string()));
191        strike_price.append_option(instrument.strike_price().map(|v| price_to_f64(&v)));
192        activation_ns.append_option(
193            instrument
194                .activation_ns()
195                .map(|v| unix_nanos_to_i64(v.as_u64())),
196        );
197        expiration_ns.append_option(
198            instrument
199                .expiration_ns()
200                .map(|v| unix_nanos_to_i64(v.as_u64())),
201        );
202        is_inverse.append_value(instrument.is_inverse());
203        is_quanto.append_value(instrument.is_quanto());
204        price_precision.append_value(instrument.price_precision());
205        size_precision.append_value(instrument.size_precision());
206        price_increment.append_value(price_to_f64(&instrument.price_increment()));
207        size_increment.append_value(quantity_to_f64(&instrument.size_increment()));
208        multiplier.append_value(quantity_to_f64(&instrument.multiplier()));
209        lot_size.append_option(instrument.lot_size().map(|v| quantity_to_f64(&v)));
210        max_quantity.append_option(instrument.max_quantity().map(|v| quantity_to_f64(&v)));
211        min_quantity.append_option(instrument.min_quantity().map(|v| quantity_to_f64(&v)));
212        max_notional_amount.append_option(instrument.max_notional().map(|v| money_to_f64(&v)));
213        max_notional_currency
214            .append_option(instrument.max_notional().map(|v| v.currency.to_string()));
215        min_notional_amount.append_option(instrument.min_notional().map(|v| money_to_f64(&v)));
216        min_notional_currency
217            .append_option(instrument.min_notional().map(|v| v.currency.to_string()));
218        max_price.append_option(instrument.max_price().map(|v| price_to_f64(&v)));
219        min_price.append_option(instrument.min_price().map(|v| price_to_f64(&v)));
220        margin_init.append_value(instrument.margin_init().to_f64().unwrap_or(f64::NAN));
221        margin_maint.append_value(instrument.margin_maint().to_f64().unwrap_or(f64::NAN));
222        maker_fee.append_value(instrument.maker_fee().to_f64().unwrap_or(f64::NAN));
223        taker_fee.append_value(instrument.taker_fee().to_f64().unwrap_or(f64::NAN));
224        ts_event.append_value(unix_nanos_to_i64(instrument.ts_event().as_u64()));
225        ts_init.append_value(unix_nanos_to_i64(instrument.ts_init().as_u64()));
226    }
227
228    RecordBatch::try_new(
229        Arc::new(instrument_schema()),
230        vec![
231            Arc::new(instrument_id.finish()),
232            Arc::new(symbol.finish()),
233            Arc::new(venue.finish()),
234            Arc::new(instrument_type.finish()),
235            Arc::new(raw_symbol.finish()),
236            Arc::new(asset_class.finish()),
237            Arc::new(instrument_class.finish()),
238            Arc::new(underlying.finish()),
239            Arc::new(base_currency.finish()),
240            Arc::new(quote_currency.finish()),
241            Arc::new(settlement_currency.finish()),
242            Arc::new(isin.finish()),
243            Arc::new(option_kind.finish()),
244            Arc::new(exchange.finish()),
245            Arc::new(strike_price.finish()),
246            Arc::new(activation_ns.finish()),
247            Arc::new(expiration_ns.finish()),
248            Arc::new(is_inverse.finish()),
249            Arc::new(is_quanto.finish()),
250            Arc::new(price_precision.finish()),
251            Arc::new(size_precision.finish()),
252            Arc::new(price_increment.finish()),
253            Arc::new(size_increment.finish()),
254            Arc::new(multiplier.finish()),
255            Arc::new(lot_size.finish()),
256            Arc::new(max_quantity.finish()),
257            Arc::new(min_quantity.finish()),
258            Arc::new(max_notional_amount.finish()),
259            Arc::new(max_notional_currency.finish()),
260            Arc::new(min_notional_amount.finish()),
261            Arc::new(min_notional_currency.finish()),
262            Arc::new(max_price.finish()),
263            Arc::new(min_price.finish()),
264            Arc::new(margin_init.finish()),
265            Arc::new(margin_maint.finish()),
266            Arc::new(maker_fee.finish()),
267            Arc::new(taker_fee.finish()),
268            Arc::new(ts_event.finish()),
269            Arc::new(ts_init.finish()),
270        ],
271    )
272}
273
274#[cfg(test)]
275mod tests {
276    use arrow::{
277        array::{Array, BooleanArray, Float64Array, StringArray, TimestampNanosecondArray},
278        datatypes::{DataType, TimeUnit},
279    };
280    use nautilus_model::{
281        instruments::{
282            InstrumentAny,
283            stubs::{
284                betting, binary_option, cfd_gold, commodity_gold, crypto_future_btcusdt,
285                crypto_option_btc_deribit, crypto_perpetual_ethusdt, currency_pair_btcusdt,
286                equity_aapl, futures_contract_es, futures_spread_es, index_instrument_spx,
287                option_contract_appl, option_spread, perpetual_contract_eurusd,
288                tokenized_asset_aaplx, xbtusd_bitmex,
289            },
290        },
291        types::{Price, Quantity},
292    };
293    use rstest::rstest;
294
295    use super::*;
296
297    fn spot() -> InstrumentAny {
298        InstrumentAny::CurrencyPair(currency_pair_btcusdt())
299    }
300
301    fn all_variants() -> Vec<(InstrumentAny, &'static str)> {
302        vec![
303            (InstrumentAny::Betting(betting()), "BettingInstrument"),
304            (InstrumentAny::BinaryOption(binary_option()), "BinaryOption"),
305            (InstrumentAny::Cfd(cfd_gold()), "Cfd"),
306            (InstrumentAny::Commodity(commodity_gold()), "Commodity"),
307            (
308                InstrumentAny::CryptoFuture(crypto_future_btcusdt(
309                    2,
310                    6,
311                    Price::from("0.01"),
312                    Quantity::from("0.000001"),
313                )),
314                "CryptoFuture",
315            ),
316            (
317                InstrumentAny::CryptoOption(crypto_option_btc_deribit(
318                    3,
319                    1,
320                    Price::from("0.001"),
321                    Quantity::from("0.1"),
322                )),
323                "CryptoOption",
324            ),
325            (
326                InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()),
327                "CryptoPerpetual",
328            ),
329            (
330                InstrumentAny::CurrencyPair(currency_pair_btcusdt()),
331                "CurrencyPair",
332            ),
333            (InstrumentAny::Equity(equity_aapl()), "Equity"),
334            (
335                InstrumentAny::FuturesContract(futures_contract_es(None, None)),
336                "FuturesContract",
337            ),
338            (
339                InstrumentAny::FuturesSpread(futures_spread_es()),
340                "FuturesSpread",
341            ),
342            (
343                InstrumentAny::IndexInstrument(index_instrument_spx()),
344                "IndexInstrument",
345            ),
346            (
347                InstrumentAny::OptionContract(option_contract_appl()),
348                "OptionContract",
349            ),
350            (InstrumentAny::OptionSpread(option_spread()), "OptionSpread"),
351            (
352                InstrumentAny::PerpetualContract(perpetual_contract_eurusd()),
353                "PerpetualContract",
354            ),
355            (
356                InstrumentAny::TokenizedAsset(tokenized_asset_aaplx()),
357                "TokenizedAsset",
358            ),
359        ]
360    }
361
362    #[rstest]
363    fn test_encode_instruments_schema() {
364        let batch = encode_instruments(&[]).unwrap();
365        let schema = batch.schema();
366        let fields = schema.fields();
367        assert_eq!(fields.len(), 39);
368        assert_eq!(fields[0].name(), "instrument_id");
369        assert_eq!(fields[0].data_type(), &DataType::Utf8);
370        assert_eq!(fields[14].name(), "strike_price");
371        assert_eq!(fields[14].data_type(), &DataType::Float64);
372        assert_eq!(fields[17].name(), "is_inverse");
373        assert_eq!(fields[17].data_type(), &DataType::Boolean);
374        assert_eq!(fields[19].name(), "price_precision");
375        assert_eq!(fields[19].data_type(), &DataType::UInt8);
376        assert_eq!(fields[33].name(), "margin_init");
377        assert_eq!(fields[33].data_type(), &DataType::Float64);
378        assert_eq!(fields[36].name(), "taker_fee");
379        assert_eq!(fields[37].name(), "ts_event");
380        assert_eq!(
381            fields[37].data_type(),
382            &DataType::Timestamp(TimeUnit::Nanosecond, None)
383        );
384    }
385
386    #[rstest]
387    fn test_encode_instruments_empty() {
388        let batch = encode_instruments(&[]).unwrap();
389        assert_eq!(batch.num_rows(), 0);
390        assert_eq!(batch.schema().fields().len(), 39);
391    }
392
393    #[rstest]
394    fn test_encode_instruments_spot_values() {
395        let instruments = vec![spot()];
396        let batch = encode_instruments(&instruments).unwrap();
397
398        assert_eq!(batch.num_rows(), 1);
399
400        let instrument_type_col = batch
401            .column(3)
402            .as_any()
403            .downcast_ref::<StringArray>()
404            .unwrap();
405        let strike_price_col = batch
406            .column(14)
407            .as_any()
408            .downcast_ref::<Float64Array>()
409            .unwrap();
410        let activation_col = batch
411            .column(15)
412            .as_any()
413            .downcast_ref::<TimestampNanosecondArray>()
414            .unwrap();
415        let expiration_col = batch
416            .column(16)
417            .as_any()
418            .downcast_ref::<TimestampNanosecondArray>()
419            .unwrap();
420        let is_inverse_col = batch
421            .column(17)
422            .as_any()
423            .downcast_ref::<BooleanArray>()
424            .unwrap();
425        let price_increment_col = batch
426            .column(21)
427            .as_any()
428            .downcast_ref::<Float64Array>()
429            .unwrap();
430
431        assert_eq!(instrument_type_col.value(0), "CurrencyPair");
432        assert!(strike_price_col.is_null(0));
433        assert!(activation_col.is_null(0));
434        assert!(expiration_col.is_null(0));
435        assert!(!is_inverse_col.value(0));
436        assert!(price_increment_col.value(0) > 0.0);
437    }
438
439    #[rstest]
440    fn test_encode_instruments_mixed_variants_preserves_per_row_nulls() {
441        let instruments = vec![
442            spot(),
443            InstrumentAny::Equity(equity_aapl()),
444            InstrumentAny::OptionContract(option_contract_appl()),
445        ];
446        let batch = encode_instruments(&instruments).unwrap();
447
448        assert_eq!(batch.num_rows(), 3);
449
450        let instrument_type_col = batch
451            .column(3)
452            .as_any()
453            .downcast_ref::<StringArray>()
454            .unwrap();
455        let strike_price_col = batch
456            .column(14)
457            .as_any()
458            .downcast_ref::<Float64Array>()
459            .unwrap();
460        let expiration_col = batch
461            .column(16)
462            .as_any()
463            .downcast_ref::<TimestampNanosecondArray>()
464            .unwrap();
465        let base_currency_col = batch
466            .column(8)
467            .as_any()
468            .downcast_ref::<StringArray>()
469            .unwrap();
470
471        assert_eq!(instrument_type_col.value(0), "CurrencyPair");
472        assert_eq!(instrument_type_col.value(1), "Equity");
473        assert_eq!(instrument_type_col.value(2), "OptionContract");
474
475        // Only the option carries a strike + expiration
476        assert!(strike_price_col.is_null(0));
477        assert!(strike_price_col.is_null(1));
478        assert!(!strike_price_col.is_null(2));
479        assert!(expiration_col.is_null(0));
480        assert!(expiration_col.is_null(1));
481        assert!(!expiration_col.is_null(2));
482
483        // Only the spot pair carries a base currency
484        assert!(!base_currency_col.is_null(0));
485        assert!(base_currency_col.is_null(1));
486    }
487
488    #[rstest]
489    fn test_encode_instruments_shared_schema_across_batches() {
490        let a = encode_instruments(&[spot()]).unwrap();
491        let b = encode_instruments(&[InstrumentAny::Equity(equity_aapl())]).unwrap();
492        assert_eq!(a.schema(), b.schema());
493    }
494
495    #[rstest]
496    fn test_encode_instruments_all_variant_names() {
497        let variants = all_variants();
498        assert_eq!(variants.len(), 16, "all InstrumentAny variants covered");
499
500        let instruments: Vec<InstrumentAny> = variants.iter().map(|(v, _)| v.clone()).collect();
501        let batch = encode_instruments(&instruments).unwrap();
502        let instrument_type_col = batch
503            .column(3)
504            .as_any()
505            .downcast_ref::<StringArray>()
506            .unwrap();
507
508        for (row, (_, expected)) in variants.iter().enumerate() {
509            assert_eq!(instrument_type_col.value(row), *expected);
510        }
511    }
512
513    #[rstest]
514    fn test_encode_instruments_inverse_perpetual() {
515        let instruments = vec![InstrumentAny::CryptoPerpetual(xbtusd_bitmex())];
516        let batch = encode_instruments(&instruments).unwrap();
517
518        let instrument_type_col = batch
519            .column(3)
520            .as_any()
521            .downcast_ref::<StringArray>()
522            .unwrap();
523        let settlement_currency_col = batch
524            .column(10)
525            .as_any()
526            .downcast_ref::<StringArray>()
527            .unwrap();
528        let is_inverse_col = batch
529            .column(17)
530            .as_any()
531            .downcast_ref::<BooleanArray>()
532            .unwrap();
533        let max_notional_amount_col = batch
534            .column(27)
535            .as_any()
536            .downcast_ref::<Float64Array>()
537            .unwrap();
538        let max_notional_currency_col = batch
539            .column(28)
540            .as_any()
541            .downcast_ref::<StringArray>()
542            .unwrap();
543        let min_notional_amount_col = batch
544            .column(29)
545            .as_any()
546            .downcast_ref::<Float64Array>()
547            .unwrap();
548        let min_notional_currency_col = batch
549            .column(30)
550            .as_any()
551            .downcast_ref::<StringArray>()
552            .unwrap();
553
554        assert_eq!(instrument_type_col.value(0), "CryptoPerpetual");
555        assert_eq!(settlement_currency_col.value(0), "BTC");
556        assert!(is_inverse_col.value(0));
557        assert!((max_notional_amount_col.value(0) - 10_000_000.0).abs() < 1e-9);
558        assert_eq!(max_notional_currency_col.value(0), "USD");
559        assert!((min_notional_amount_col.value(0) - 1.0).abs() < 1e-9);
560        assert_eq!(min_notional_currency_col.value(0), "USD");
561
562        let margin_init_col = batch
563            .column(33)
564            .as_any()
565            .downcast_ref::<Float64Array>()
566            .unwrap();
567        let margin_maint_col = batch
568            .column(34)
569            .as_any()
570            .downcast_ref::<Float64Array>()
571            .unwrap();
572        let maker_fee_col = batch
573            .column(35)
574            .as_any()
575            .downcast_ref::<Float64Array>()
576            .unwrap();
577        let taker_fee_col = batch
578            .column(36)
579            .as_any()
580            .downcast_ref::<Float64Array>()
581            .unwrap();
582
583        assert!((margin_init_col.value(0) - 0.01).abs() < 1e-9);
584        assert!((margin_maint_col.value(0) - 0.0035).abs() < 1e-9);
585        assert!((maker_fee_col.value(0) - (-0.00025)).abs() < 1e-9);
586        assert!((taker_fee_col.value(0) - 0.00075).abs() < 1e-9);
587    }
588}