@@ -506,3 +506,137 @@ def test_get_datahandler(testdatadir):
506506 assert isinstance (dh , JsonGzDataHandler )
507507 dh1 = get_datahandler (testdatadir , "jsongz" , dh )
508508 assert id (dh1 ) == id (dh )
509+
510+
511+ @pytest .fixture
512+ def feather_dh (testdatadir ):
513+ return FeatherDataHandler (testdatadir )
514+
515+
516+ @pytest .fixture
517+ def trades_full (feather_dh ):
518+ df = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT )
519+ assert not df .empty
520+ return df
521+
522+
523+ @pytest .fixture
524+ def timerange_mid (trades_full ):
525+ # Pick a mid-range window using actual timestamps
526+ mid_start = int (trades_full ["timestamp" ].iloc [len (trades_full ) // 3 ])
527+ mid_end = int (trades_full ["timestamp" ].iloc [(2 * len (trades_full )) // 3 ])
528+ return TimeRange ("date" , "date" , startts = mid_start , stopts = mid_end )
529+
530+
531+ def test_feather_trades_timerange_filter_fullspan (feather_dh , trades_full ):
532+ timerange_full = TimeRange (
533+ "date" ,
534+ "date" ,
535+ startts = int (trades_full ["timestamp" ].min ()),
536+ stopts = int (trades_full ["timestamp" ].max ()),
537+ )
538+ # Full-span filter should equal unfiltered
539+ filtered = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT , timerange = timerange_full )
540+ assert_frame_equal (
541+ trades_full .reset_index (drop = True ), filtered .reset_index (drop = True ), check_exact = True
542+ )
543+
544+
545+ def test_feather_trades_timerange_filter_subset (feather_dh , trades_full , timerange_mid ):
546+ # Subset filter should be a subset of the full-span filter
547+ subset = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT , timerange = timerange_mid )
548+ assert not subset .empty
549+ assert subset ["timestamp" ].min () >= timerange_mid .startts
550+ assert subset ["timestamp" ].max () <= timerange_mid .stopts
551+ assert len (subset ) < len (trades_full )
552+
553+
554+ def test_feather_trades_timerange_pushdown_fallback (
555+ feather_dh , trades_full , timerange_mid , monkeypatch , caplog
556+ ):
557+ # Pushdown filter should fail, so fallback should load the entire file
558+ import freqtrade .data .history .datahandlers .featherdatahandler as fdh
559+
560+ def raise_err (* args , ** kwargs ):
561+ raise ValueError ("fail" )
562+
563+ # Mock the dataset loading to raise an error
564+ monkeypatch .setattr (fdh .dataset , "dataset" , raise_err )
565+
566+ with caplog .at_level ("WARNING" ):
567+ out = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT , timerange = timerange_mid )
568+
569+ assert len (out ) == len (trades_full )
570+ assert any (
571+ "Unable to use Arrow filtering, loading entire trades file" in r .message
572+ for r in caplog .records
573+ )
574+
575+
576+ def test_feather_trades_timerange_open_start (feather_dh , trades_full ):
577+ # Open start: stop timestamp but no start (startts=0)
578+ stop_ts = int (trades_full ["timestamp" ].iloc [(2 * len (trades_full )) // 3 ])
579+ tr = TimeRange (None , "date" , startts = 0 , stopts = stop_ts )
580+
581+ filtered = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT , timerange = tr )
582+ assert 0 < len (filtered ) < len (trades_full )
583+ assert filtered ["timestamp" ].max () <= stop_ts
584+ # First row should match full's first row
585+ assert filtered .iloc [0 ]["timestamp" ] == trades_full .iloc [0 ]["timestamp" ]
586+
587+
588+ def test_feather_trades_timerange_open_end (feather_dh , trades_full ):
589+ # Open end: start timestamp but no stop (stopts=0)
590+ start_ts = int (trades_full ["timestamp" ].iloc [len (trades_full ) // 3 ])
591+ tr = TimeRange ("date" , None , startts = start_ts , stopts = 0 )
592+
593+ filtered = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT , timerange = tr )
594+ assert 0 < len (filtered ) < len (trades_full )
595+ assert filtered ["timestamp" ].min () >= start_ts
596+ # Last row should match full's last row
597+ assert filtered .iloc [- 1 ]["timestamp" ] == trades_full .iloc [- 1 ]["timestamp" ]
598+
599+
600+ def test_feather_trades_timerange_fully_open (feather_dh , trades_full ):
601+ # Fully open: no start or stop bounds (both 0)
602+ tr = TimeRange (None , None , startts = 0 , stopts = 0 )
603+
604+ filtered = feather_dh .trades_load ("XRP/ETH" , TradingMode .SPOT , timerange = tr )
605+ # Should equal unfiltered load
606+ assert_frame_equal (
607+ trades_full .reset_index (drop = True ), filtered .reset_index (drop = True ), check_exact = True
608+ )
609+
610+
611+ def test_feather_build_arrow_time_filter (feather_dh ):
612+ # None timerange should return None
613+ assert feather_dh ._build_arrow_time_filter (None ) is None
614+
615+ # Fully open (both bounds 0) should return None
616+ tr_fully_open = TimeRange (None , None , startts = 0 , stopts = 0 )
617+ assert feather_dh ._build_arrow_time_filter (tr_fully_open ) is None
618+
619+ # Open start (startts=0) should return stop filter only
620+ tr_open_start = TimeRange (None , "date" , startts = 0 , stopts = 1000 )
621+ filter_open_start = feather_dh ._build_arrow_time_filter (tr_open_start )
622+ assert filter_open_start is not None
623+ # Should be a single expression (timestamp <= stopts)
624+ assert str (filter_open_start ).count ("<=" ) == 1
625+ assert str (filter_open_start ).count (">=" ) == 0
626+
627+ # Open end (stopts=0) should return start filter only
628+ tr_open_end = TimeRange ("date" , None , startts = 500 , stopts = 0 )
629+ filter_open_end = feather_dh ._build_arrow_time_filter (tr_open_end )
630+ assert filter_open_end is not None
631+ # Should be a single expression (timestamp >= startts)
632+ assert str (filter_open_end ).count (">=" ) == 1
633+ assert str (filter_open_end ).count ("<=" ) == 0
634+
635+ # Closed range should return combined filter
636+ tr_closed = TimeRange ("date" , "date" , startts = 500 , stopts = 1000 )
637+ filter_closed = feather_dh ._build_arrow_time_filter (tr_closed )
638+ assert filter_closed is not None
639+ # Should contain both >= and <= (combined with &)
640+ filter_str = str (filter_closed )
641+ assert ">=" in filter_str
642+ assert "<=" in filter_str
0 commit comments