Promote production portfolio strategy
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user