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:
@@ -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']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user