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:
2026-07-18 13:03:22 +02:00
parent e07da0f8f0
commit b0e33e1606
25 changed files with 429 additions and 221 deletions
+150
View File
@@ -26,7 +26,11 @@ from app.services.backtest_service import (
from app.services.momentum_service import (
STRATEGY_RANK_MOMENTUM_WEIGHT,
STRATEGY_RANK_VOL_WEIGHT,
blend_strategy_rank,
)
from app.services.qualification import MIN_TARGET_PROBABILITY
from app.services.recommendation_service import PRIMARY_TARGET_MIN_RR
from app.services import rr_scanner_service
def _production_monitor_row() -> dict:
@@ -103,3 +107,149 @@ def test_live_gate_equals_the_production_variant_gate() -> None:
assert _momentum_qualifies(cand, cutoff) == _qualifies_strategy_variant(
cand, entry_cfg
), cand
def test_activation_defaults_match_promoted_production_gate() -> None:
"""Greenfield Admin must ship the researched gate, not the old trough defaults."""
assert float(ACTIVATION_DEFAULTS["min_rr"]) == 2.0
assert float(ACTIVATION_DEFAULTS["min_confidence"]) == 0.0
assert float(ACTIVATION_DEFAULTS["min_momentum_percentile"]) == 80.0
assert ACTIVATION_DEFAULTS["exclude_neutral"] is True
def test_primary_target_rr_floor_is_single_sourced() -> None:
assert rr_scanner_service.PRIMARY_TARGET_MIN_RR == PRIMARY_TARGET_MIN_RR
assert PRIMARY_TARGET_MIN_RR == 1.5
assert MIN_TARGET_PROBABILITY == 20.0
def test_strategy_rank_falls_back_to_momentum_when_vol_missing() -> None:
"""Live and backtest must not bury a name solely because vol history is short."""
assert blend_strategy_rank(80.0, 60.0) == 76.0
assert blend_strategy_rank(80.0, None) == 80.0
assert blend_strategy_rank(None, 60.0) is None
assert blend_strategy_rank(None, None) is None
from app.services import backtest_service as bt
cands = [
{bt.PRODUCTION_PERCENTILE_KEY: 80.0, bt.VOL_PERCENTILE_KEY: None},
{bt.PRODUCTION_PERCENTILE_KEY: 70.0, bt.VOL_PERCENTILE_KEY: 50.0},
]
bt._assign_residual_high_vol_blend(cands)
assert cands[0][bt.RESIDUAL_HIGH_VOL_BLEND_80_20_KEY] == 80.0
assert cands[1][bt.RESIDUAL_HIGH_VOL_BLEND_80_20_KEY] == 66.0
@pytest.mark.asyncio
async def test_live_scan_and_backtest_window_share_gtl_primary() -> None:
"""Same OHLCV + dims: live scan_ticker primary ≡ backtest _window_setups.
No gate_levels_override — both paths build the production GTL from bars.
Dimension scores are seeded to the values the backtest window computes so
probability ranking cannot diverge for that reason alone.
"""
from datetime import date, datetime, timedelta, timezone
from app.models.ohlcv import OHLCVRecord
from app.models.score import DimensionScore
from app.models.ticker import Ticker
from app.services import backtest_service as bt
from app.services.recommendation_service import DEFAULT_RECOMMENDATION_CONFIG
from app.services.rr_scanner_service import scan_ticker
from app.services.scoring_service import (
compute_momentum_from_closes,
compute_technical_from_arrays,
)
from tests.conftest import _test_session_factory
n = 120
base = date(2024, 1, 1)
# Oscillating range so GTL finds traffic-backed proposals above/below spot.
closes: list[float] = []
highs: list[float] = []
lows: list[float] = []
volumes: list[int] = []
price = 100.0
for i in range(n):
phase = i % 30
if phase < 12:
price = price + (94.0 - price) * 0.2
elif phase < 24:
price = price + (108.0 - price) * 0.2
else:
price = 100.0 + (i % 5) * 0.3
high = price + 1.2
low = price - 1.2
close = price
closes.append(close)
highs.append(high)
lows.append(low)
volumes.append(100_000 + i * 10)
tech = (compute_technical_from_arrays(highs, lows, closes, volumes)[0]) or 50.0
mom = (compute_momentum_from_closes(closes)[0]) or 50.0
async with _test_session_factory() as session:
ticker = Ticker(symbol="GTLPAR")
session.add(ticker)
await session.flush()
bars = [
OHLCVRecord(
ticker_id=ticker.id,
date=base + timedelta(days=i),
open=closes[i] - 0.2,
high=highs[i],
low=lows[i],
close=closes[i],
volume=volumes[i],
)
for i in range(n)
]
session.add_all(bars)
now = datetime.now(timezone.utc)
session.add_all([
DimensionScore(
ticker_id=ticker.id, dimension="technical", score=float(tech),
is_stale=False, computed_at=now,
),
DimensionScore(
ticker_id=ticker.id, dimension="momentum", score=float(mom),
is_stale=False, computed_at=now,
),
])
await session.commit()
live = await scan_ticker(session, "GTLPAR", rr_threshold=1.5, atr_multiplier=1.5)
# Re-load bars as plain ORM list for the pure backtest window path.
from sqlalchemy import select
records = list(
(
await session.execute(
select(OHLCVRecord)
.where(OHLCVRecord.ticker_id == ticker.id)
.order_by(OHLCVRecord.date.asc())
)
).scalars().all()
)
config = dict(DEFAULT_RECOMMENDATION_CONFIG)
activation = dict(ACTIVATION_DEFAULTS)
sim = bt._window_setups(records, config, activation)
live_by_dir = {s.direction: s for s in live}
sim_by_dir = {s["direction"]: s for s in sim}
assert set(live_by_dir) == set(sim_by_dir), (
f"direction mismatch live={set(live_by_dir)} sim={set(sim_by_dir)}"
)
assert live_by_dir, "expected at least one directional setup from GTL"
for direction, live_setup in live_by_dir.items():
sim_setup = sim_by_dir[direction]
assert live_setup.target == pytest.approx(float(sim_setup["target"]), abs=0.05), (
f"{direction}: live target {live_setup.target} != sim {sim_setup['target']}"
)
assert live_setup.rr_ratio == pytest.approx(float(sim_setup["rr"]), abs=0.05), (
f"{direction}: live rr {live_setup.rr_ratio} != sim {sim_setup['rr']}"
)