Setup views: primary-target column, floor-target prune, liveness cutoff
Three follow-ups to the gate probability floor (8f41143):
- Signals table shows the starred primary target (shared primaryTarget
helper) instead of an independently computed max-probability best,
so Overview, Signals and ticker details agree by construction.
- Targets pinned at the 3% probability clamp floor collapse to the
nearest one (enhance_trade_setup + backtest candidates in parity):
floor-pinned levels are indistinguishable to the model, so farther
ones were duplicate 3% rows inviting lottery headlines.
- get_trade_setups only returns setups re-emitted within
LIVE_SETUP_MAX_AGE_DAYS (3): an older latest row means the daily
scan no longer confirms the setup, and such rows otherwise surface
forever on Overview/Signals/ticker/alerts. History endpoints keep
full history.
Backtest on the Jul-3 snapshot is metric-identical to the gate-floor
run on all qualified stats (1089 qualified, Sharpe 2.02, CAGR +49.6%,
DD -15.8%): the prune only removes noise the gate already rejected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -39,6 +39,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
STRATEGY_VERSION = "residual_highvol_80_20_atr_trail3_v1"
|
||||
|
||||
# A setup counts as live only while the daily scan keeps re-emitting it. The
|
||||
# scan runs every day (07:00 UTC cron), so anything older than this was NOT
|
||||
# re-confirmed — typically because no level clears the R:R threshold from the
|
||||
# current price anymore. Without this cutoff such rows stay "latest" forever
|
||||
# (the scanner never writes a replacement) and keep surfacing on the live
|
||||
# views. 3 days buffers a missed pipeline run or two; history endpoints are
|
||||
# unaffected.
|
||||
LIVE_SETUP_MAX_AGE_DAYS = 3
|
||||
|
||||
|
||||
async def _get_ticker(db: AsyncSession, symbol: str) -> Ticker:
|
||||
normalised = symbol.strip().upper()
|
||||
@@ -602,10 +611,17 @@ async def get_trade_setups(
|
||||
live_recommendation: bool = False,
|
||||
exclude_open_trade_tickers: bool = False,
|
||||
) -> list[dict]:
|
||||
"""Get latest stored trade setups, optionally filtered."""
|
||||
"""Get latest stored trade setups, optionally filtered.
|
||||
|
||||
Only setups the daily scan re-emitted within ``LIVE_SETUP_MAX_AGE_DAYS``
|
||||
are returned — an older "latest" row means the scanner no longer finds a
|
||||
valid setup for that ticker, so it must not surface as current.
|
||||
"""
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=LIVE_SETUP_MAX_AGE_DAYS)
|
||||
stmt = (
|
||||
select(TradeSetup, Ticker.symbol)
|
||||
.join(Ticker, TradeSetup.ticker_id == Ticker.id)
|
||||
.where(TradeSetup.detected_at >= cutoff)
|
||||
)
|
||||
if direction is not None:
|
||||
stmt = stmt.where(TradeSetup.direction == direction.lower())
|
||||
|
||||
Reference in New Issue
Block a user