feat: near-close scan schedule and distinct-day gate reset

Move the only qualifying R:R scan to 15:30 ET with chained Telegram alerts,
put outcome eval after a final-bar OHLCV fetch, enforce NY trading-day
requalify semantics, stamp paper trades fill_mode=near_close, and migrate
stored schedule_* keys to America/New_York.
This commit is contained in:
2026-07-18 17:55:39 +02:00
parent 5a61b164f6
commit 736451e26f
14 changed files with 428 additions and 83 deletions
+4
View File
@@ -115,6 +115,8 @@ class TestConfigureScheduler:
"event_study",
"backtest",
"daily_pipeline",
"near_close_pipeline",
"after_close_pipeline",
"intraday_pipeline",
}
@@ -125,6 +127,7 @@ class TestConfigureScheduler:
job_ids = [j.id for j in scheduler.get_jobs()]
# Each ID should appear exactly once
assert sorted(job_ids) == sorted([
"after_close_pipeline",
"alerts",
"backtest",
"benchmark_collector",
@@ -134,6 +137,7 @@ class TestConfigureScheduler:
"data_backfill",
"fundamental_collector",
"market_regime",
"near_close_pipeline",
"regime_monitor",
"event_study",
"outcome_evaluator",
+49
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import select
from app.models.paper_trade import PaperTrade
from app.models.ticker import Ticker
@@ -129,6 +130,54 @@ async def test_latest_stop_starts_a_new_gate_reset_episode(session):
assert ticker.id in await get_reentry_gate_locks(session)
async def test_same_day_fail_then_qualify_stays_locked(session):
"""Fail at 10:00 NY and qualify at 15:35 NY same day must not unlock."""
session.add(User(id=1, username="u", password_hash="x", role="user", has_access=True))
ticker = Ticker(symbol="SAMEDAY")
session.add(ticker)
await session.flush()
stopped_at = datetime(2026, 7, 15, 14, 0, tzinfo=timezone.utc) # 10:00 ET
session.add(_stopped_trade(ticker.id, closed_at=stopped_at))
await session.commit()
fail_at = datetime(2026, 7, 15, 14, 5, tzinfo=timezone.utc) # ~10:05 ET
updated = await observe_reentry_gate_transitions(
session,
evaluated_ticker_ids={ticker.id},
qualified_ticker_ids=set(),
observed_at=fail_at,
)
assert updated == {ticker.id}
qualify_same_day = datetime(2026, 7, 15, 19, 35, tzinfo=timezone.utc) # 15:35 ET
updated = await observe_reentry_gate_transitions(
session,
evaluated_ticker_ids={ticker.id},
qualified_ticker_ids={ticker.id},
observed_at=qualify_same_day,
)
assert updated == set()
assert ticker.id in await get_reentry_gate_locks(session)
trade = (
await session.execute(select(PaperTrade).where(PaperTrade.ticker_id == ticker.id))
).scalar_one()
assert trade.reentry_gate_failed_at is not None
assert trade.reentry_gate_requalified_at is None
qualify_next_day = datetime(2026, 7, 16, 19, 35, tzinfo=timezone.utc) # next day 15:35 ET
updated = await observe_reentry_gate_transitions(
session,
evaluated_ticker_ids={ticker.id},
qualified_ticker_ids={ticker.id},
observed_at=qualify_next_day,
)
assert updated == {ticker.id}
await session.refresh(trade)
assert trade.reentry_gate_requalified_at is not None
assert ticker.id not in await get_reentry_gate_locks(session)
async def test_newer_non_stop_exit_supersedes_historical_stop(session):
session.add(User(id=1, username="u", password_hash="x", role="user", has_access=True))
ticker = Ticker(symbol="LATEREXIT")