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:
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user