from __future__ import annotations from datetime import UTC, date, datetime from pathlib import Path from findata_market_firstrate.archive import parse_bar_file, ticker_from_filename from findata_schema import BARS_SCHEMA, Adjustment, AssetClass, Interval FIX = Path(__file__).parent / "fixtures" / "firstrate" NOW = datetime(2026, 11, 1, 12, 0, tzinfo=UTC) def _parse(text: str, symbol="TEST", interval=Interval.m1): return parse_bar_file(text.encode(), symbol, AssetClass.stock, interval, Adjustment.raw, "firstrate", NOW) def test_ticker_from_filename_variants(): assert ticker_from_filename("AAPL_day_1min_UNADJUSTED.txt") == "AAPL" assert ticker_from_filename("AAPL_1min.txt") == "AAPL" assert ticker_from_filename("BRK.B_full_1min_adj_split.txt") == "BRK.B" assert ticker_from_filename("TWTR-DELISTED_1min.txt") == "TWTR-DELISTED" assert ticker_from_filename("nested.zip!/ES_contin_adj_ratio_1min.txt") == "ES" def test_parse_real_session_bar_open_convention(): t, raw = parse_bar_file((FIX / "AAPL_day_1min_UNADJUSTED.txt").read_bytes(), "AAPL", AssetClass.stock, Interval.m1, Adjustment.raw, "firstrate", NOW) assert raw == 900 and t.num_rows == 900 assert t.schema.equals(BARS_SCHEMA) first = t.slice(0, 1).to_pylist()[0] # 2026-10-30 04:00 America/New_York (EDT, UTC-4) == 08:00 UTC ; timestamp marks the bar OPEN assert first["timestamp_utc"] == datetime(2026, 10, 30, 8, 0, tzinfo=UTC) assert first["session_date"] == date(2026, 10, 30) assert first["open"] == 350.76 and first["volume"] == 4802.0 assert first["instrument_id"] == "firstrate:stock:AAPL" and first["adjustment_type"] == "raw" last = t.slice(899, 1).to_pylist()[0] assert last["timestamp_utc"] == datetime(2026, 10, 30, 23, 59, tzinfo=UTC) # 19:59 EDT def test_dst_spring_forward_and_fall_back(): # 2026-03-06 (EST, UTC-5): 09:30 ET = 14:30 UTC ; 2026-03-09 (EDT, UTC-4): 09:30 ET = 13:30 UTC t, _ = _parse("2026-03-06 09:30:00,1,2,0.5,1.5,10\n2026-03-09 09:30:00,1,2,0.5,1.5,10\n") ts = t.column("timestamp_utc").to_pylist() assert ts == [datetime(2026, 3, 6, 14, 30, tzinfo=UTC), datetime(2026, 3, 9, 13, 30, tzinfo=UTC)] # fall back 2026-11-01: 2026-10-30 09:30 EDT = 13:30 UTC ; 2026-11-02 09:30 EST = 14:30 UTC t, _ = _parse("2026-10-30 09:30:00,1,2,0.5,1.5,10\n2026-11-02 09:30:00,1,2,0.5,1.5,10\n") ts = t.column("timestamp_utc").to_pylist() assert ts == [datetime(2026, 10, 30, 13, 30, tzinfo=UTC), datetime(2026, 11, 2, 14, 30, tzinfo=UTC)] def test_nonexistent_local_time_is_dropped_and_counted(): # 02:30 on 2026-03-08 does not exist in America/New_York t, raw = _parse("2026-03-08 02:30:00,1,2,0.5,1.5,10\n2026-03-08 04:00:00,1,2,0.5,1.5,10\n") assert raw == 2 and t.num_rows == 1 def test_crlf_header_and_fx_two_field_layout(): t, raw = _parse("DateTime,Open,High,Low,Close,Volume\r\n2026-10-30 09:30:00,1,2,0.5,1.5,10\r\n2026-10-30 09:31:00,1,2,0.5,1.5,0\r\n") assert raw == 2 and t.num_rows == 2 t, raw = _parse("20261030,09:30:00,1.1,1.2,1.0,1.15,0\n") assert t.num_rows == 1 and t.column("timestamp_utc")[0].as_py() == datetime(2026, 10, 30, 13, 30, tzinfo=UTC) def test_daily_layout_with_open_interest_column(): t, raw = _parse("2026-10-30,1,2,0.5,1.5,10,1234\n", interval=Interval.d1) assert t.num_rows == 1 and t.column("session_date")[0].as_py() == date(2026, 10, 30) def test_duplicates_are_kept_for_flagging_not_dropped(): t, raw = _parse("2026-10-30 09:30:00,1,2,0.5,1.5,10\n2026-10-30 09:30:00,1,2,0.5,1.5,10\n") assert raw == 2 and t.num_rows == 2