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