Fix manual refresh dropping qualified ranks and clarify trade UI.
Single-ticker fetch now attaches residual-momentum ranks so setups do not silently fail the activation gate. Exit plan is a timeline, chart labels move left of the price scale, and missing ranks surface explicitly.
This commit is contained in:
@@ -428,6 +428,77 @@ async def _create_signal_context_snapshots(
|
||||
)
|
||||
|
||||
|
||||
async def resolve_activation_ranks_for_symbol(
|
||||
db: AsyncSession,
|
||||
symbol: str,
|
||||
) -> dict[str, float | None]:
|
||||
"""Universe activation ranks for one symbol (manual single-ticker scans).
|
||||
|
||||
The daily ``scan_all_tickers`` path ranks the whole universe once and passes
|
||||
percentiles into ``scan_ticker``. Manual refresh must do the same: without
|
||||
``momentum_percentile`` the activation gate treats the setup as unranked and
|
||||
it silently drops out of qualified trades.
|
||||
|
||||
Prefer a fresh cross-sectional rank; if ranking fails or the symbol is
|
||||
missing from the universe slice, fall back to the most recent prior setup
|
||||
that still carries ranks so a refresh never zeroes the gate inputs.
|
||||
"""
|
||||
symbol_u = symbol.strip().upper()
|
||||
empty: dict[str, float | None] = {
|
||||
"momentum_percentile": None,
|
||||
"strategy_rank": None,
|
||||
"volatility_percentile": None,
|
||||
}
|
||||
|
||||
try:
|
||||
from app.services import momentum_service
|
||||
|
||||
ranks = await momentum_service.compute_activation_ranks(db)
|
||||
hit = ranks.get(symbol_u)
|
||||
if hit is not None and hit.get("momentum_percentile") is not None:
|
||||
return {
|
||||
"momentum_percentile": hit.get("momentum_percentile"),
|
||||
"strategy_rank": hit.get("strategy_rank"),
|
||||
"volatility_percentile": hit.get("volatility_percentile"),
|
||||
}
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Activation ranking failed for single-ticker scan of %s", symbol_u
|
||||
)
|
||||
|
||||
ticker_result = await db.execute(
|
||||
select(Ticker.id).where(Ticker.symbol == symbol_u)
|
||||
)
|
||||
ticker_id = ticker_result.scalar_one_or_none()
|
||||
if ticker_id is None:
|
||||
return empty
|
||||
|
||||
prev_result = await db.execute(
|
||||
select(TradeSetup)
|
||||
.where(
|
||||
TradeSetup.ticker_id == ticker_id,
|
||||
TradeSetup.momentum_percentile.is_not(None),
|
||||
)
|
||||
.order_by(TradeSetup.detected_at.desc(), TradeSetup.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
prev = prev_result.scalar_one_or_none()
|
||||
if prev is None:
|
||||
return empty
|
||||
|
||||
return {
|
||||
"momentum_percentile": (
|
||||
float(prev.momentum_percentile) if prev.momentum_percentile is not None else None
|
||||
),
|
||||
"strategy_rank": (
|
||||
float(prev.strategy_rank) if prev.strategy_rank is not None else None
|
||||
),
|
||||
"volatility_percentile": (
|
||||
float(prev.volatility_percentile) if prev.volatility_percentile is not None else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def scan_ticker(
|
||||
db: AsyncSession,
|
||||
symbol: str,
|
||||
|
||||
Reference in New Issue
Block a user