69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""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()}
|