@@ -278,6 +278,7 @@ impl DatabentoDataLoader {
278
278
instrument_id : Option < InstrumentId > ,
279
279
price_precision : Option < u8 > ,
280
280
include_trades : bool ,
281
+ bars_timestamp_on_close : Option < bool > ,
281
282
) -> anyhow:: Result < impl Iterator < Item = anyhow:: Result < ( Option < Data > , Option < Data > ) > > + ' _ >
282
283
where
283
284
T : dbn:: Record + dbn:: HasRType + ' static ,
@@ -313,6 +314,7 @@ impl DatabentoDataLoader {
313
314
price_precision,
314
315
None ,
315
316
include_trades,
317
+ bars_timestamp_on_close. unwrap_or ( true ) ,
316
318
) ?;
317
319
Ok ( Some ( ( item1, item2) ) )
318
320
} else {
@@ -349,7 +351,7 @@ impl DatabentoDataLoader {
349
351
instrument_id : Option < InstrumentId > ,
350
352
price_precision : Option < u8 > ,
351
353
) -> anyhow:: Result < Vec < OrderBookDelta > > {
352
- self . read_records :: < dbn:: MboMsg > ( filepath, instrument_id, price_precision, false ) ?
354
+ self . read_records :: < dbn:: MboMsg > ( filepath, instrument_id, price_precision, false , None ) ?
353
355
. filter_map ( |result| match result {
354
356
Ok ( ( Some ( item1) , _) ) => {
355
357
if let Data :: Delta ( delta) = item1 {
@@ -373,7 +375,7 @@ impl DatabentoDataLoader {
373
375
instrument_id : Option < InstrumentId > ,
374
376
price_precision : Option < u8 > ,
375
377
) -> anyhow:: Result < Vec < OrderBookDepth10 > > {
376
- self . read_records :: < dbn:: Mbp10Msg > ( filepath, instrument_id, price_precision, false ) ?
378
+ self . read_records :: < dbn:: Mbp10Msg > ( filepath, instrument_id, price_precision, false , None ) ?
377
379
. filter_map ( |result| match result {
378
380
Ok ( ( Some ( item1) , _) ) => {
379
381
if let Data :: Depth10 ( depth) = item1 {
@@ -397,7 +399,7 @@ impl DatabentoDataLoader {
397
399
instrument_id : Option < InstrumentId > ,
398
400
price_precision : Option < u8 > ,
399
401
) -> anyhow:: Result < Vec < QuoteTick > > {
400
- self . read_records :: < dbn:: Mbp1Msg > ( filepath, instrument_id, price_precision, false ) ?
402
+ self . read_records :: < dbn:: Mbp1Msg > ( filepath, instrument_id, price_precision, false , None ) ?
401
403
. filter_map ( |result| match result {
402
404
Ok ( ( Some ( item1) , _) ) => {
403
405
if let Data :: Quote ( quote) = item1 {
@@ -421,7 +423,7 @@ impl DatabentoDataLoader {
421
423
instrument_id : Option < InstrumentId > ,
422
424
price_precision : Option < u8 > ,
423
425
) -> anyhow:: Result < Vec < QuoteTick > > {
424
- self . read_records :: < dbn:: BboMsg > ( filepath, instrument_id, price_precision, false ) ?
426
+ self . read_records :: < dbn:: BboMsg > ( filepath, instrument_id, price_precision, false , None ) ?
425
427
. filter_map ( |result| match result {
426
428
Ok ( ( Some ( item1) , _) ) => {
427
429
if let Data :: Quote ( quote) = item1 {
@@ -445,7 +447,7 @@ impl DatabentoDataLoader {
445
447
instrument_id : Option < InstrumentId > ,
446
448
price_precision : Option < u8 > ,
447
449
) -> anyhow:: Result < Vec < TradeTick > > {
448
- self . read_records :: < dbn:: TbboMsg > ( filepath, instrument_id, price_precision, false ) ?
450
+ self . read_records :: < dbn:: TbboMsg > ( filepath, instrument_id, price_precision, false , None ) ?
449
451
. filter_map ( |result| match result {
450
452
Ok ( ( _, maybe_item2) ) => {
451
453
if let Some ( Data :: Trade ( trade) ) = maybe_item2 {
@@ -468,7 +470,7 @@ impl DatabentoDataLoader {
468
470
instrument_id : Option < InstrumentId > ,
469
471
price_precision : Option < u8 > ,
470
472
) -> anyhow:: Result < Vec < TradeTick > > {
471
- self . read_records :: < dbn:: TradeMsg > ( filepath, instrument_id, price_precision, false ) ?
473
+ self . read_records :: < dbn:: TradeMsg > ( filepath, instrument_id, price_precision, false , None ) ?
472
474
. filter_map ( |result| match result {
473
475
Ok ( ( Some ( item1) , _) ) => {
474
476
if let Data :: Trade ( trade) = item1 {
@@ -491,20 +493,27 @@ impl DatabentoDataLoader {
491
493
filepath : & Path ,
492
494
instrument_id : Option < InstrumentId > ,
493
495
price_precision : Option < u8 > ,
496
+ timestamp_on_close : Option < bool > ,
494
497
) -> anyhow:: Result < Vec < Bar > > {
495
- self . read_records :: < dbn:: OhlcvMsg > ( filepath, instrument_id, price_precision, false ) ?
496
- . filter_map ( |result| match result {
497
- Ok ( ( Some ( item1) , _) ) => {
498
- if let Data :: Bar ( bar) = item1 {
499
- Some ( Ok ( bar) )
500
- } else {
501
- None
502
- }
498
+ self . read_records :: < dbn:: OhlcvMsg > (
499
+ filepath,
500
+ instrument_id,
501
+ price_precision,
502
+ false ,
503
+ timestamp_on_close,
504
+ ) ?
505
+ . filter_map ( |result| match result {
506
+ Ok ( ( Some ( item1) , _) ) => {
507
+ if let Data :: Bar ( bar) = item1 {
508
+ Some ( Ok ( bar) )
509
+ } else {
510
+ None
503
511
}
504
- Ok ( ( None , _) ) => None ,
505
- Err ( e) => Some ( Err ( e) ) ,
506
- } )
507
- . collect ( )
512
+ }
513
+ Ok ( ( None , _) ) => None ,
514
+ Err ( e) => Some ( Err ( e) ) ,
515
+ } )
516
+ . collect ( )
508
517
}
509
518
510
519
/// # Errors
@@ -792,8 +801,97 @@ mod tests {
792
801
#[ case( test_data_path( ) . join( "test_data.ohlcv-1s.dbn.zst" ) ) ]
793
802
fn test_load_bars ( loader : DatabentoDataLoader , #[ case] path : PathBuf ) {
794
803
let instrument_id = InstrumentId :: from ( "ESM4.GLBX" ) ;
795
- let bars = loader. load_bars ( & path, Some ( instrument_id) , None ) . unwrap ( ) ;
804
+ let bars = loader
805
+ . load_bars ( & path, Some ( instrument_id) , None , None )
806
+ . unwrap ( ) ;
807
+
808
+ assert_eq ! ( bars. len( ) , 2 ) ;
809
+ }
810
+
811
+ #[ rstest]
812
+ #[ case( test_data_path( ) . join( "test_data.ohlcv-1s.dbn.zst" ) ) ]
813
+ fn test_load_bars_timestamp_on_close_true ( loader : DatabentoDataLoader , #[ case] path : PathBuf ) {
814
+ let instrument_id = InstrumentId :: from ( "ESM4.GLBX" ) ;
815
+ let bars = loader
816
+ . load_bars ( & path, Some ( instrument_id) , None , Some ( true ) )
817
+ . unwrap ( ) ;
796
818
797
819
assert_eq ! ( bars. len( ) , 2 ) ;
820
+
821
+ // When bars_timestamp_on_close is true, both ts_event and ts_init should be equal (close time)
822
+ for bar in & bars {
823
+ assert_eq ! (
824
+ bar. ts_event, bar. ts_init,
825
+ "ts_event and ts_init should be equal when bars_timestamp_on_close=true"
826
+ ) ;
827
+ // For 1-second bars, ts_event should be 1 second after the open time
828
+ // This confirms the bar is timestamped at close
829
+ }
830
+ }
831
+
832
+ #[ rstest]
833
+ #[ case( test_data_path( ) . join( "test_data.ohlcv-1s.dbn.zst" ) ) ]
834
+ fn test_load_bars_timestamp_on_close_false ( loader : DatabentoDataLoader , #[ case] path : PathBuf ) {
835
+ let instrument_id = InstrumentId :: from ( "ESM4.GLBX" ) ;
836
+ let bars = loader
837
+ . load_bars ( & path, Some ( instrument_id) , None , Some ( false ) )
838
+ . unwrap ( ) ;
839
+
840
+ assert_eq ! ( bars. len( ) , 2 ) ;
841
+
842
+ // When bars_timestamp_on_close is false, both ts_event and ts_init should be equal (open time)
843
+ for bar in & bars {
844
+ assert_eq ! (
845
+ bar. ts_event, bar. ts_init,
846
+ "ts_event and ts_init should be equal when bars_timestamp_on_close=false"
847
+ ) ;
848
+ }
849
+ }
850
+
851
+ #[ rstest]
852
+ #[ case( test_data_path( ) . join( "test_data.ohlcv-1s.dbn.zst" ) , 0 ) ]
853
+ #[ case( test_data_path( ) . join( "test_data.ohlcv-1s.dbn.zst" ) , 1 ) ]
854
+ fn test_load_bars_timestamp_comparison (
855
+ loader : DatabentoDataLoader ,
856
+ #[ case] path : PathBuf ,
857
+ #[ case] bar_index : usize ,
858
+ ) {
859
+ let instrument_id = InstrumentId :: from ( "ESM4.GLBX" ) ;
860
+
861
+ let bars_close = loader
862
+ . load_bars ( & path, Some ( instrument_id) , None , Some ( true ) )
863
+ . unwrap ( ) ;
864
+
865
+ let bars_open = loader
866
+ . load_bars ( & path, Some ( instrument_id) , None , Some ( false ) )
867
+ . unwrap ( ) ;
868
+
869
+ assert_eq ! ( bars_close. len( ) , bars_open. len( ) ) ;
870
+ assert_eq ! ( bars_close. len( ) , 2 ) ;
871
+
872
+ let bar_close = & bars_close[ bar_index] ;
873
+ let bar_open = & bars_open[ bar_index] ;
874
+
875
+ // Bars should have the same OHLCV data
876
+ assert_eq ! ( bar_close. open, bar_open. open) ;
877
+ assert_eq ! ( bar_close. high, bar_open. high) ;
878
+ assert_eq ! ( bar_close. low, bar_open. low) ;
879
+ assert_eq ! ( bar_close. close, bar_open. close) ;
880
+ assert_eq ! ( bar_close. volume, bar_open. volume) ;
881
+
882
+ // The close-timestamped bar should have later timestamp than open-timestamped bar
883
+ // For 1-second bars, this should be exactly 1 second difference
884
+ assert ! (
885
+ bar_close. ts_event > bar_open. ts_event,
886
+ "Close-timestamped bar should have later timestamp than open-timestamped bar"
887
+ ) ;
888
+
889
+ // The difference should be exactly 1 second (1_000_000_000 nanoseconds) for 1s bars
890
+ const ONE_SECOND_NS : u64 = 1_000_000_000 ;
891
+ assert_eq ! (
892
+ bar_close. ts_event. as_u64( ) - bar_open. ts_event. as_u64( ) ,
893
+ ONE_SECOND_NS ,
894
+ "Timestamp difference should be exactly 1 second for 1s bars"
895
+ ) ;
798
896
}
799
897
}
0 commit comments