"""Shared live/backtest trading-policy constants and availability checks.""" from __future__ import annotations from datetime import date, datetime, time, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.ohlcv import OHLCVRecord from app.models.paper_trade import PaperTrade # A ticker stopped at its initial stop may qualify again immediately, but the # July 2026 event study showed that waiting five market sessions materially # improved the production book. The stop session is wait_session=0; the first # permitted re-entry is wait_session=5, provided the normal gate still passes. REENTRY_LOCKDOWN_SESSIONS = 5 async def get_reentry_lockdown_ticker_ids( db: AsyncSession, *, as_of: date | None = None, sessions: int = REENTRY_LOCKDOWN_SESSIONS, ) -> set[int]: """Ticker ids still inside the post-initial-stop market-session lockdown. The market calendar is derived from stored OHLCV dates, not calendar days. A stop on session D is released once five later stored sessions exist. Only an initial-stop close (``close_reason == "stop"``) starts the lockdown; trailing, target, time, and manual exits do not. """ sessions = max(0, int(sessions)) if sessions == 0: return set() session_cutoff = as_of or datetime.now(timezone.utc).date() session_result = await db.execute( select(OHLCVRecord.date) .where(OHLCVRecord.date <= session_cutoff) .distinct() .order_by(OHLCVRecord.date.desc()) .limit(sessions) ) recent_sessions = [row[0] for row in session_result.all()] if not recent_sessions: return set() # Stops on or after the oldest of the latest N sessions have fewer than N # later completed sessions. Once that oldest session rolls forward, the # corresponding stop automatically leaves the result set. lockdown_threshold = min(recent_sessions) threshold_start = datetime.combine( lockdown_threshold, time.min, tzinfo=timezone.utc, ) result = await db.execute( select(PaperTrade.ticker_id) .where( PaperTrade.status == "closed", PaperTrade.close_reason == "stop", PaperTrade.closed_at.is_not(None), PaperTrade.closed_at >= threshold_start, ) .distinct() ) return {ticker_id for ticker_id, in result.all()}