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
+7 -2
View File
@@ -566,6 +566,8 @@ VALID_JOB_NAMES = {
"event_study",
"backtest",
"daily_pipeline",
"near_close_pipeline",
"after_close_pipeline",
"intraday_pipeline",
}
@@ -583,17 +585,20 @@ JOB_LABELS = {
"regime_monitor": "Regime Monitor",
"event_study": "Event Study",
"backtest": "Backtest",
"daily_pipeline": "Daily Pipeline",
"daily_pipeline": "Morning Pipeline",
"near_close_pipeline": "Near-Close Pipeline (scan+alert)",
"after_close_pipeline": "After-Close Pipeline (outcome)",
"intraday_pipeline": "Intraday Pipeline",
}
# Jobs driven by the daily_pipeline (in order) rather than their own timer.
# Jobs driven by a pipeline (in order) rather than their own auto timer.
PIPELINE_MEMBERS = {
"data_collector",
"benchmark_collector",
"sentiment_collector",
"rr_scanner",
"outcome_evaluator",
"alerts",
"market_regime",
"regime_monitor",
}
+4
View File
@@ -333,6 +333,9 @@ async def create_trade(
target=target,
status="open",
opened_at=datetime.now(timezone.utc),
# Near-close cutover era — Track Record must not mix with morning-scan
# fills or future broker-routed fills when comparing to backtests.
fill_mode="near_close",
)
db.add(trade)
await db.commit()
@@ -386,6 +389,7 @@ def _to_dict(
"alpha_pct": alpha_pct,
"alpha_usd": alpha_usd,
"close_reason": trade.close_reason,
"fill_mode": trade.fill_mode,
"trailing_stop": trailing[0] if trailing else None,
"trailing_distance_pct": trailing[1] if trailing else None,
}
+20 -3
View File
@@ -3,13 +3,24 @@
from __future__ import annotations
from collections.abc import Iterable
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from zoneinfo import ZoneInfo
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.paper_trade import PaperTrade
# Gate-reset "day" boundary matches US cash equities session calendar, not UTC.
_REENTRY_DAY_TZ = ZoneInfo("America/New_York")
def _ny_trading_date(moment: datetime) -> date:
"""Calendar date in America/New_York for a gate-reset observation."""
if moment.tzinfo is None:
moment = moment.replace(tzinfo=timezone.utc)
return moment.astimezone(_REENTRY_DAY_TZ).date()
async def _latest_initial_stop_trades(
db: AsyncSession,
@@ -93,8 +104,14 @@ async def observe_reentry_gate_transitions(
trade.reentry_gate_failed_at = timestamp
updated.add(ticker_id)
elif ticker_id in qualified:
trade.reentry_gate_requalified_at = timestamp
updated.add(ticker_id)
# Study semantics: requalify only on a *subsequent* daily observation.
# Same America/New_York calendar day as the failure does not unlock,
# even if multiple full-universe scans run (manual + near-close).
if _ny_trading_date(trade.reentry_gate_failed_at) < _ny_trading_date(
timestamp
):
trade.reentry_gate_requalified_at = timestamp
updated.add(ticker_id)
if updated:
await db.flush()