Promote production portfolio strategy

This commit is contained in:
2026-07-04 07:48:38 +02:00
parent 66ef0564c1
commit 5f2d108227
22 changed files with 1677 additions and 132 deletions
+40 -9
View File
@@ -36,7 +36,7 @@ from app.services.recommendation_service import (
logger = logging.getLogger(__name__)
STRATEGY_VERSION = "residual_momentum_12_1_rr_time_v2"
STRATEGY_VERSION = "residual_highvol_80_20_atr_trail3_v1"
async def _get_ticker(db: AsyncSession, symbol: str) -> Ticker:
@@ -288,6 +288,21 @@ async def _create_signal_context_snapshots(
"composite_score": float(comp.score) if comp else float(setup.composite_score),
"composite_is_stale": bool(comp.is_stale) if comp else None,
"composite_computed_at": comp.computed_at if comp else None,
"momentum_percentile": (
float(setup.momentum_percentile)
if setup.momentum_percentile is not None
else None
),
"volatility_percentile": (
float(setup.volatility_percentile)
if setup.volatility_percentile is not None
else None
),
"strategy_rank": (
float(setup.strategy_rank)
if setup.strategy_rank is not None
else None
),
"dimensions": dims.get(setup.ticker_id, {}),
}
sentiment_context = (
@@ -348,12 +363,15 @@ async def scan_ticker(
rr_threshold: float = 1.5,
atr_multiplier: float = 1.5,
momentum_percentile: float | None = None,
strategy_rank: float | None = None,
volatility_percentile: float | None = None,
) -> list[TradeSetup]:
"""Scan a single ticker for trade setups meeting the R:R threshold.
``momentum_percentile`` is the ticker's residual 12-1 momentum activation
rank across the universe (computed by the caller), stored on each setup so
the activation gate can select the top slice."""
the activation gate can select the top slice. ``strategy_rank`` is the
production ordering score used for top-pick ranking."""
ticker = await _get_ticker(db, symbol)
records = await query_ohlcv(db, symbol)
@@ -441,6 +459,8 @@ async def scan_ticker(
composite_score=round(composite_score, 4),
detected_at=now,
momentum_percentile=momentum_percentile,
strategy_rank=strategy_rank,
volatility_percentile=volatility_percentile,
))
if levels_below:
@@ -475,6 +495,8 @@ async def scan_ticker(
composite_score=round(composite_score, 4),
detected_at=now,
momentum_percentile=momentum_percentile,
strategy_rank=strategy_rank,
volatility_percentile=volatility_percentile,
))
available_directions = {s.direction for s in setups}
@@ -525,16 +547,17 @@ async def scan_all_tickers(
tickers = list(result.scalars().all())
total = len(tickers)
# Rank the universe by residual 12-1 momentum up front so each new setup
# carries its activation percentile. Best-effort; the ranker falls back to
# raw 12-1 momentum only if benchmark data is unavailable.
# Rank the universe up front so each new setup carries both the residual
# activation gate percentile and the promoted production ordering score.
# Best-effort; the ranker falls back to raw 12-1 momentum only if benchmark
# data is unavailable.
try:
from app.services import momentum_service
percentiles = await momentum_service.compute_momentum_percentiles(db)
ranks = await momentum_service.compute_activation_ranks(db)
except Exception:
logger.exception("Momentum ranking refresh failed")
percentiles = {}
logger.exception("Activation ranking refresh failed")
ranks = {}
all_setups: list[TradeSetup] = []
for index, ticker in enumerate(tickers):
@@ -555,7 +578,9 @@ async def scan_all_tickers(
setups = await scan_ticker(
db, ticker.symbol, rr_threshold, atr_multiplier,
momentum_percentile=percentiles.get(ticker.symbol),
momentum_percentile=(ranks.get(ticker.symbol) or {}).get("momentum_percentile"),
strategy_rank=(ranks.get(ticker.symbol) or {}).get("strategy_rank"),
volatility_percentile=(ranks.get(ticker.symbol) or {}).get("volatility_percentile"),
)
all_setups.extend(setups)
except Exception:
@@ -605,6 +630,8 @@ async def get_trade_setups(
latest_rows = list(latest_by_key.values())
latest_rows.sort(
key=lambda row: (
row[0].strategy_rank if row[0].strategy_rank is not None else -1.0,
row[0].momentum_percentile if row[0].momentum_percentile is not None else -1.0,
row[0].confidence_score if row[0].confidence_score is not None else -1.0,
row[0].rr_ratio,
row[0].composite_score,
@@ -632,6 +659,8 @@ async def get_trade_setups(
]
rows_out.sort(
key=lambda row: (
row["strategy_rank"] if row["strategy_rank"] is not None else -1.0,
row["momentum_percentile"] if row["momentum_percentile"] is not None else -1.0,
row["confidence_score"] if row["confidence_score"] is not None else -1.0,
row["rr_ratio"],
row["composite_score"],
@@ -757,5 +786,7 @@ def _trade_setup_to_dict(setup: TradeSetup, symbol: str, price_context: dict | N
"evaluated_at": setup.evaluated_at,
"current_price": current_price,
"momentum_percentile": setup.momentum_percentile,
"strategy_rank": setup.strategy_rank,
"volatility_percentile": setup.volatility_percentile,
"context_as_of": context_as_of,
}