| 1 | from datetime import date, datetime, time, timezone |
| 2 | |
| 3 | from app.market_sessions import MarketCalendarException, MarketTradingSchedule, class_due, market_session_status, price_sync_eligible |
| 4 | from app.persistence import SqliteResearchPersistence |
| 5 | |
| 6 | |
| 7 | NSE = [MarketTradingSchedule("NSE", "XNSE", "IN", "Asia/Kolkata", day, time(9, 15), time(15, 30)) for day in range(5)] |
| 8 | |
| 9 | |
| 10 | def test_nse_session_holiday_and_exception_rules_are_data_driven(): |
| 11 | assert market_session_status("NSE", NSE, [], datetime(2026, 9, 7, 3, 30, tzinfo=timezone.utc)) == "CLOSED" |
| 12 | assert market_session_status("NSE", NSE, [], datetime(2026, 9, 7, 5, 0, tzinfo=timezone.utc)) == "OPEN" |
| 13 | assert market_session_status("NSE", NSE, [], datetime(2026, 9, 7, 11, 0, tzinfo=timezone.utc)) == "CLOSED" |
| 14 | assert market_session_status("NSE", NSE, [], datetime(2026, 9, 6, 5, 0, tzinfo=timezone.utc)) == "CLOSED" |
| 15 | holiday = [MarketCalendarException("NSE", date(2026, 9, 7), "CLOSED")] |
| 16 | assert market_session_status("NSE", NSE, holiday, datetime(2026, 9, 7, 5, 0, tzinfo=timezone.utc)) == "HOLIDAY" |
| 17 | early = [MarketCalendarException("NSE", date(2026, 9, 7), "EARLY_CLOSE", close_time=time(12, 0))] |
| 18 | assert market_session_status("NSE", NSE, early, datetime(2026, 9, 7, 6, 0, tzinfo=timezone.utc)) == "OPEN" |
| 19 | assert market_session_status("NSE", NSE, early, datetime(2026, 9, 7, 7, 0, tzinfo=timezone.utc)) == "CLOSED" |
| 20 | |
| 21 | |
| 22 | def test_unknown_fails_closed_and_dst_iana_zone_is_used(): |
| 23 | assert market_session_status("MISSING", NSE, [], datetime.now(timezone.utc)) == "UNKNOWN" |
| 24 | nyse = [MarketTradingSchedule("NYSE", "XNYS", "US", "America/New_York", day, time(9, 30), time(16)) for day in range(5)] |
| 25 | assert market_session_status("NYSE", nyse, [], datetime(2026, 7, 6, 14, 0, tzinfo=timezone.utc)) == "OPEN" |
| 26 | assert market_session_status("NYSE", nyse, [], datetime(2026, 1, 6, 14, 0, tzinfo=timezone.utc)) == "CLOSED" |
| 27 | |
| 28 | |
| 29 | def test_price_eligibility_is_open_only_but_other_class_due_is_independent(): |
| 30 | now = datetime(2026, 9, 7, 5, tzinfo=timezone.utc) |
| 31 | assert price_sync_eligible("OPEN", None, 300, now) |
| 32 | assert not price_sync_eligible("OPEN", now, 300, now) |
| 33 | assert not price_sync_eligible("CLOSED", now.replace(hour=0), 300, now) |
| 34 | assert class_due(now.replace(day=6), 3600, now) |
| 35 | |
| 36 | |
| 37 | def test_market_schedule_and_exception_time_deserialization_accepts_postgres_time_and_sqlite_strings(): |
| 38 | class Result: |
| 39 | def __init__(self, rows): self.rows = rows |
| 40 | def fetchall(self): return self.rows |
| 41 | class Connection: |
| 42 | def execute(self, sql): |
| 43 | if "market_trading_schedules" in sql: |
| 44 | return Result([{ |
| 45 | "market_code": "NSE", "mic": "XNSE", "country_code": "IN", "timezone": "Asia/Kolkata", "trading_day": 0, |
| 46 | "regular_open_time": time(9, 15), "regular_close_time": time(15, 30), "enabled": True, |
| 47 | }]) |
| 48 | return Result([{ |
| 49 | "market_code": "NSE", "trading_date": "2026-09-07", "exception_type": "EARLY_CLOSE", |
| 50 | "open_time": "09:15:00", "close_time": time(12, 0), "reason": "fixture", |
| 51 | }]) |
| 52 | |
| 53 | persistence = SqliteResearchPersistence() |
| 54 | persistence._connection = Connection() |
| 55 | schedule = persistence.load_market_schedules({"NSE"})[0] |
| 56 | exception = persistence.load_market_calendar_exceptions({"NSE"})[0] |
| 57 | assert schedule.regular_open_time == time(9, 15) |
| 58 | assert schedule.regular_close_time == time(15, 30) |
| 59 | assert exception.open_time == time(9, 15) |
| 60 | assert exception.close_time == time(12, 0) |