fix: align production defaults and close review parity gaps
Ship greenfield min_rr=2.0 and conf=0, read-only Structural S/R, indicator cache invalidation, and UI/gate language that treats GTL as screening not exit. Align strategy_rank missing-vol fallback live vs backtest, single-source PRIMARY_TARGET_MIN_RR, expand prod parity tests, and drop dead FE clients.
This commit is contained in:
@@ -34,6 +34,28 @@ STRATEGY_RANK_MOMENTUM_WEIGHT = 0.8
|
||||
STRATEGY_RANK_VOL_WEIGHT = 1.0 - STRATEGY_RANK_MOMENTUM_WEIGHT
|
||||
|
||||
|
||||
def blend_strategy_rank(
|
||||
momentum_percentile: float | None,
|
||||
volatility_percentile: float | None,
|
||||
*,
|
||||
momentum_weight: float = STRATEGY_RANK_MOMENTUM_WEIGHT,
|
||||
) -> float | None:
|
||||
"""80/20 production rank with mom-only fallback when vol is missing.
|
||||
|
||||
Live and backtest must share this policy: missing vol must not send a
|
||||
residual-qualified name to the bottom of the book (that was the old
|
||||
backtest behaviour when either leg was None).
|
||||
"""
|
||||
if momentum_percentile is not None and volatility_percentile is not None:
|
||||
vol_weight = 1.0 - momentum_weight
|
||||
return round(
|
||||
float(momentum_percentile) * momentum_weight
|
||||
+ float(volatility_percentile) * vol_weight,
|
||||
2,
|
||||
)
|
||||
return float(momentum_percentile) if momentum_percentile is not None else None
|
||||
|
||||
|
||||
def compute_12_1_momentum(closes: list[float]) -> float | None:
|
||||
"""Return over the window ending ~1 month ago, starting ~12 months ago.
|
||||
None when there isn't a full year of history."""
|
||||
@@ -100,41 +122,17 @@ async def _load_activation_benchmark(db: AsyncSession) -> dict[date, float]:
|
||||
|
||||
|
||||
async def compute_momentum_percentiles(db: AsyncSession) -> dict[str, float]:
|
||||
"""Compute each ticker's activation momentum rank.
|
||||
"""Momentum leg only — thin view of ``compute_activation_ranks``.
|
||||
|
||||
Production uses residual 12-1 momentum when benchmark data is available. If
|
||||
SPY data is absent, fall back to raw 12-1 momentum rather than disabling the
|
||||
scanner. Tickers without enough stock/benchmark history are absent.
|
||||
Prefer ``compute_activation_ranks`` in new code (includes vol + strategy_rank).
|
||||
Kept so tests/helpers that only need the residual/raw percentile map stay simple.
|
||||
"""
|
||||
result = await db.execute(select(Ticker).order_by(Ticker.symbol))
|
||||
tickers = list(result.scalars().all())
|
||||
|
||||
benchmark_closes = await _load_activation_benchmark(db)
|
||||
using_residual = len(benchmark_closes) >= _MOM_LOOKBACK
|
||||
|
||||
values: dict[str, float] = {}
|
||||
for ticker in tickers:
|
||||
try:
|
||||
records = await query_ohlcv(db, ticker.symbol)
|
||||
except Exception:
|
||||
logger.exception("Momentum fetch failed for %s", ticker.symbol)
|
||||
continue
|
||||
closes = [float(r.close) for r in records]
|
||||
value = (
|
||||
compute_residual_12_1_momentum([r.date for r in records], closes, benchmark_closes)
|
||||
if using_residual
|
||||
else compute_12_1_momentum(closes)
|
||||
)
|
||||
if value is not None:
|
||||
values[ticker.symbol] = value
|
||||
|
||||
percentiles = _percentiles(values)
|
||||
logger.info(json.dumps({
|
||||
"event": "momentum_ranked",
|
||||
"signal": "residual_12_1" if using_residual else "raw_12_1_fallback",
|
||||
"tickers": len(percentiles),
|
||||
}))
|
||||
return percentiles
|
||||
ranks = await compute_activation_ranks(db)
|
||||
return {
|
||||
sym: float(row["momentum_percentile"])
|
||||
for sym, row in ranks.items()
|
||||
if row.get("momentum_percentile") is not None
|
||||
}
|
||||
|
||||
|
||||
def compute_realized_vol_6m(closes: list[float]) -> float | None:
|
||||
@@ -204,19 +202,10 @@ async def compute_activation_ranks(db: AsyncSession) -> dict[str, dict[str, floa
|
||||
for sym in symbols:
|
||||
momentum_pct = momentum_percentiles.get(sym)
|
||||
vol_pct = vol_percentiles.get(sym)
|
||||
strategy_rank = (
|
||||
round(
|
||||
momentum_pct * STRATEGY_RANK_MOMENTUM_WEIGHT
|
||||
+ vol_pct * STRATEGY_RANK_VOL_WEIGHT,
|
||||
2,
|
||||
)
|
||||
if momentum_pct is not None and vol_pct is not None
|
||||
else momentum_pct
|
||||
)
|
||||
ranks[sym] = {
|
||||
"momentum_percentile": momentum_pct,
|
||||
"volatility_percentile": vol_pct,
|
||||
"strategy_rank": strategy_rank,
|
||||
"strategy_rank": blend_strategy_rank(momentum_pct, vol_pct),
|
||||
}
|
||||
|
||||
logger.info(json.dumps({
|
||||
|
||||
Reference in New Issue
Block a user