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