1from __future__ import annotations23from datetime import UTC, date, datetime4from pathlib import Path56import pyarrow as pa78from findata_market_firstrate.archive import parse_bar_file, ticker_from_filename9from findata_schema import BARS_SCHEMA, Adjustment, AssetClass, Interval1011FIX = Path(__file__).parent / "fixtures" / "firstrate"12NOW = datetime(2026, 11, 1, 12, 0, tzinfo=UTC)131415def _parse(text: str, symbol="TEST", interval=Interval.m1):16 return parse_bar_file(text.encode(), symbol, AssetClass.stock, interval, Adjustment.raw, "firstrate", NOW)171819def test_ticker_from_filename_variants():20 assert ticker_from_filename("AAPL_day_1min_UNADJUSTED.txt") == "AAPL"21 assert ticker_from_filename("AAPL_1min.txt") == "AAPL"22 assert ticker_from_filename("BRK.B_full_1min_adj_split.txt") == "BRK.B"23 assert ticker_from_filename("TWTR-DELISTED_1min.txt") == "TWTR-DELISTED"24 assert ticker_from_filename("nested.zip!/ES_contin_adj_ratio_1min.txt") == "ES"252627def test_parse_real_session_bar_open_convention():28 t, raw = parse_bar_file((FIX / "AAPL_day_1min_UNADJUSTED.txt").read_bytes(), "AAPL", AssetClass.stock, Interval.m1, Adjustment.raw, "firstrate", NOW)29 assert raw == 900 and t.num_rows == 90030 assert t.schema.equals(BARS_SCHEMA)31 first = t.slice(0, 1).to_pylist()[0]32 # 2026-10-30 04:00 America/New_York (EDT, UTC-4) == 08:00 UTC ; timestamp marks the bar OPEN33 assert first["timestamp_utc"] == datetime(2026, 10, 30, 8, 0, tzinfo=UTC)34 assert first["session_date"] == date(2026, 10, 30)35 assert first["open"] == 350.76 and first["volume"] == 4802.036 assert first["instrument_id"] == "firstrate:stock:AAPL" and first["adjustment_type"] == "raw"37 last = t.slice(899, 1).to_pylist()[0]38 assert last["timestamp_utc"] == datetime(2026, 10, 30, 23, 59, tzinfo=UTC) # 19:59 EDT394041def test_dst_spring_forward_and_fall_back():42 # 2026-03-06 (EST, UTC-5): 09:30 ET = 14:30 UTC ; 2026-03-09 (EDT, UTC-4): 09:30 ET = 13:30 UTC43 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")44 ts = t.column("timestamp_utc").to_pylist()45 assert ts == [datetime(2026, 3, 6, 14, 30, tzinfo=UTC), datetime(2026, 3, 9, 13, 30, tzinfo=UTC)]46 # fall back 2026-11-01: 2026-10-30 09:30 EDT = 13:30 UTC ; 2026-11-02 09:30 EST = 14:30 UTC47 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")48 ts = t.column("timestamp_utc").to_pylist()49 assert ts == [datetime(2026, 10, 30, 13, 30, tzinfo=UTC), datetime(2026, 11, 2, 14, 30, tzinfo=UTC)]505152def test_nonexistent_local_time_is_dropped_and_counted():53 # 02:30 on 2026-03-08 does not exist in America/New_York54 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")55 assert raw == 2 and t.num_rows == 1565758def test_crlf_header_and_fx_two_field_layout():59 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")60 assert raw == 2 and t.num_rows == 261 t, raw = _parse("20261030,09:30:00,1.1,1.2,1.0,1.15,0\n")62 assert t.num_rows == 1 and t.column("timestamp_utc")[0].as_py() == datetime(2026, 10, 30, 13, 30, tzinfo=UTC)636465def test_daily_layout_with_open_interest_column():66 t, raw = _parse("2026-10-30,1,2,0.5,1.5,10,1234\n", interval=Interval.d1)67 assert t.num_rows == 1 and t.column("session_date")[0].as_py() == date(2026, 10, 30)686970def test_duplicates_are_kept_for_flagging_not_dropped():71 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")72 assert raw == 2 and t.num_rows == 273