feat: require gate reset before post-stop reentry
This commit is contained in:
@@ -28,8 +28,12 @@ from app.models.ticker import Ticker
|
||||
from app.models.trade_setup import TradeSetup
|
||||
from app.services.indicator_service import _extract_ohlcv, compute_atr
|
||||
from app.services.price_service import query_ohlcv
|
||||
from app.services.qualification import setup_qualifies
|
||||
from app.services.sr_service import detect_gate_target_ladder
|
||||
from app.services.trade_policy import get_reentry_lockdowns
|
||||
from app.services.trade_policy import (
|
||||
get_reentry_gate_locks,
|
||||
observe_reentry_gate_transitions,
|
||||
)
|
||||
from app.services.recommendation_service import (
|
||||
_risk_level_from_conflicts,
|
||||
build_recommendation_snapshot,
|
||||
@@ -700,12 +704,24 @@ async def scan_all_tickers(
|
||||
``progress_callback(processed, total, current_symbol)`` is invoked as each
|
||||
ticker is scanned so callers (e.g. the scheduler) can surface live progress.
|
||||
"""
|
||||
# Plain strings, not Ticker instances: the rollbacks below expire any ORM
|
||||
# objects held across them, and touching an expired attribute afterwards
|
||||
# Plain ids/strings, not Ticker instances: the rollbacks below expire any
|
||||
# ORM objects held across them, and touching an expired attribute afterwards
|
||||
# triggers sync lazy-loading, which raises on an AsyncSession.
|
||||
result = await db.execute(select(Ticker.symbol).order_by(Ticker.symbol))
|
||||
symbols = list(result.scalars().all())
|
||||
total = len(symbols)
|
||||
result = await db.execute(select(Ticker.id, Ticker.symbol).order_by(Ticker.symbol))
|
||||
ticker_rows = [(int(ticker_id), symbol) for ticker_id, symbol in result.all()]
|
||||
total = len(ticker_rows)
|
||||
|
||||
# Gate-reset observations must use the same runtime activation settings as
|
||||
# the live setup list. If the config cannot be loaded, scan normally but do
|
||||
# not mutate reset state from an evaluation whose rules are unknown.
|
||||
activation: dict | None = None
|
||||
try:
|
||||
from app.services.admin_service import get_activation_config
|
||||
|
||||
activation = await get_activation_config(db)
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
logger.exception("Activation config load for re-entry gate reset failed")
|
||||
|
||||
# Rank the universe up front so each new setup carries both the residual
|
||||
# activation gate percentile and the promoted production ordering score.
|
||||
@@ -721,7 +737,10 @@ async def scan_all_tickers(
|
||||
ranks = {}
|
||||
|
||||
all_setups: list[TradeSetup] = []
|
||||
for index, symbol in enumerate(symbols):
|
||||
evaluated_ticker_ids: set[int] = set()
|
||||
qualified_ticker_ids: set[int] = set()
|
||||
gate_observation_started_at = datetime.now(timezone.utc)
|
||||
for index, (ticker_id, symbol) in enumerate(ticker_rows):
|
||||
if progress_callback is not None:
|
||||
progress_callback(index, total, symbol)
|
||||
# Refresh scores first so the scheduled scan works off current data.
|
||||
@@ -754,10 +773,33 @@ async def scan_all_tickers(
|
||||
primary_min_rr=PRIMARY_TARGET_MIN_RR,
|
||||
)
|
||||
all_setups.extend(setups)
|
||||
if activation is not None:
|
||||
try:
|
||||
if any(setup_qualifies(setup, activation) for setup in setups):
|
||||
qualified_ticker_ids.add(ticker_id)
|
||||
evaluated_ticker_ids.add(ticker_id)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Gate-reset qualification observation failed for %s", symbol
|
||||
)
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
logger.exception("Error scanning ticker %s", symbol)
|
||||
|
||||
if activation is not None:
|
||||
transitioned_ticker_ids = await observe_reentry_gate_transitions(
|
||||
db,
|
||||
evaluated_ticker_ids=evaluated_ticker_ids,
|
||||
qualified_ticker_ids=qualified_ticker_ids,
|
||||
observed_at=gate_observation_started_at,
|
||||
)
|
||||
await db.commit()
|
||||
if transitioned_ticker_ids:
|
||||
logger.info(
|
||||
"Updated post-stop gate-reset state for %d ticker(s)",
|
||||
len(transitioned_ticker_ids),
|
||||
)
|
||||
|
||||
if progress_callback is not None and total:
|
||||
progress_callback(total, total, "")
|
||||
|
||||
@@ -772,8 +814,8 @@ async def get_trade_setups(
|
||||
symbol: str | None = None,
|
||||
live_recommendation: bool = False,
|
||||
exclude_open_trade_tickers: bool = False,
|
||||
exclude_reentry_lockdown_tickers: bool = False,
|
||||
include_reentry_lockdown: bool = False,
|
||||
exclude_reentry_gate_locked_tickers: bool = False,
|
||||
include_reentry_gate_lock: bool = False,
|
||||
) -> list[dict]:
|
||||
"""Get latest stored trade setups, optionally filtered.
|
||||
|
||||
@@ -798,7 +840,7 @@ async def get_trade_setups(
|
||||
if recommended_action is not None and not live_recommendation:
|
||||
stmt = stmt.where(TradeSetup.recommended_action == recommended_action)
|
||||
excluded_ticker_ids: set[int] = set()
|
||||
reentry_lockdowns: dict[int, int] = {}
|
||||
reentry_gate_locks: dict[int, datetime] = {}
|
||||
if exclude_open_trade_tickers:
|
||||
open_trade_result = await db.execute(
|
||||
select(PaperTrade.ticker_id)
|
||||
@@ -808,10 +850,10 @@ async def get_trade_setups(
|
||||
excluded_ticker_ids.update(
|
||||
ticker_id for ticker_id, in open_trade_result.all()
|
||||
)
|
||||
if exclude_reentry_lockdown_tickers or include_reentry_lockdown:
|
||||
reentry_lockdowns = await get_reentry_lockdowns(db)
|
||||
if exclude_reentry_lockdown_tickers:
|
||||
excluded_ticker_ids.update(reentry_lockdowns)
|
||||
if exclude_reentry_gate_locked_tickers or include_reentry_gate_lock:
|
||||
reentry_gate_locks = await get_reentry_gate_locks(db)
|
||||
if exclude_reentry_gate_locked_tickers:
|
||||
excluded_ticker_ids.update(reentry_gate_locks)
|
||||
if excluded_ticker_ids:
|
||||
stmt = stmt.where(~TradeSetup.ticker_id.in_(excluded_ticker_ids))
|
||||
|
||||
@@ -866,14 +908,14 @@ async def get_trade_setups(
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
if include_reentry_lockdown:
|
||||
if include_reentry_gate_lock:
|
||||
ticker_by_setup_id = {
|
||||
setup.id: setup.ticker_id for setup, _ in latest_rows
|
||||
}
|
||||
for row in rows_out:
|
||||
ticker_id = ticker_by_setup_id.get(row["id"])
|
||||
row["reentry_lockdown_remaining_sessions"] = (
|
||||
reentry_lockdowns.get(ticker_id) if ticker_id is not None else None
|
||||
row["reentry_gate_reset_required"] = (
|
||||
ticker_id in reentry_gate_locks if ticker_id is not None else False
|
||||
)
|
||||
return rows_out
|
||||
|
||||
|
||||
Reference in New Issue
Block a user