feat: add strategy variant lab and signal context snapshots
Backtest report now includes research-only hold-to-horizon portfolio variants comparing raw vs residual 12-1 momentum, cutoff 80 vs 90, max 10 vs 15 positions, and SPY-200 risk scaling. A dynamic research recommendation panel flags residual momentum, cutoff 90, or regime scaling only when transparent promotion rules pass. Adds signal_context_snapshots with migration 016 and captures one point-in-time context row per newly generated TradeSetup: setup fields, composite/dimensions, latest sentiment, latest fundamentals, and strategy_version=momentum_12_1_rr_time_v1. This is forward-only; no historical sentiment/fundamental backfill is attempted. No live gate, paper-trade exit, or production ranking behavior changes. Verification: 458 backend tests pass, ruff check app/ clean, frontend npm run build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Tests for point-in-time signal context snapshots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.fundamental import FundamentalData
|
||||
from app.models.score import CompositeScore, DimensionScore
|
||||
from app.models.sentiment import SentimentScore
|
||||
from app.models.signal_context_snapshot import SignalContextSnapshot
|
||||
from app.models.ticker import Ticker
|
||||
from app.models.trade_setup import TradeSetup
|
||||
from app.services import rr_scanner_service as rr
|
||||
from tests.conftest import _test_session_factory # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def session():
|
||||
async with _test_session_factory() as s:
|
||||
yield s
|
||||
|
||||
|
||||
async def test_create_signal_context_snapshot_captures_latest_context(session):
|
||||
now = datetime(2026, 7, 2, 12, tzinfo=timezone.utc)
|
||||
ticker = Ticker(symbol="CTX")
|
||||
session.add(ticker)
|
||||
await session.flush()
|
||||
|
||||
session.add_all([
|
||||
DimensionScore(
|
||||
ticker_id=ticker.id,
|
||||
dimension="technical",
|
||||
score=71.0,
|
||||
is_stale=False,
|
||||
computed_at=now,
|
||||
),
|
||||
DimensionScore(
|
||||
ticker_id=ticker.id,
|
||||
dimension="momentum",
|
||||
score=82.0,
|
||||
is_stale=False,
|
||||
computed_at=now,
|
||||
),
|
||||
CompositeScore(
|
||||
ticker_id=ticker.id,
|
||||
score=76.5,
|
||||
is_stale=False,
|
||||
weights_json='{"technical": 0.25}',
|
||||
computed_at=now,
|
||||
),
|
||||
SentimentScore(
|
||||
ticker_id=ticker.id,
|
||||
classification="BULLISH",
|
||||
confidence=78,
|
||||
source="test",
|
||||
timestamp=now,
|
||||
reasoning="",
|
||||
citations_json="[]",
|
||||
recommendation="BUY",
|
||||
),
|
||||
FundamentalData(
|
||||
ticker_id=ticker.id,
|
||||
pe_ratio=25.0,
|
||||
revenue_growth=0.18,
|
||||
earnings_surprise=0.05,
|
||||
market_cap=1_000_000_000.0,
|
||||
next_earnings_date=date(2026, 8, 1),
|
||||
fetched_at=now,
|
||||
unavailable_fields_json="{}",
|
||||
),
|
||||
])
|
||||
setup = TradeSetup(
|
||||
ticker_id=ticker.id,
|
||||
direction="long",
|
||||
entry_price=100.0,
|
||||
stop_loss=95.0,
|
||||
target=120.0,
|
||||
rr_ratio=4.0,
|
||||
composite_score=76.5,
|
||||
detected_at=now,
|
||||
confidence_score=64.0,
|
||||
momentum_percentile=88.0,
|
||||
recommended_action="LONG_HIGH",
|
||||
risk_level="Low",
|
||||
)
|
||||
session.add(setup)
|
||||
await session.flush()
|
||||
|
||||
await rr._create_signal_context_snapshots(session, [setup])
|
||||
await session.commit()
|
||||
|
||||
row = (await session.get(SignalContextSnapshot, 1))
|
||||
assert row is not None
|
||||
assert row.trade_setup_id == setup.id
|
||||
assert row.strategy_version == rr.STRATEGY_VERSION
|
||||
assert row.momentum_percentile == 88.0
|
||||
|
||||
score = json.loads(row.score_context_json)
|
||||
sentiment = json.loads(row.sentiment_context_json)
|
||||
fundamental = json.loads(row.fundamental_context_json)
|
||||
|
||||
assert score["composite_score"] == 76.5
|
||||
assert score["dimensions"]["technical"]["score"] == 71.0
|
||||
assert sentiment["classification"] == "BULLISH"
|
||||
assert sentiment["confidence"] == 78
|
||||
assert fundamental["pe_ratio"] == 25.0
|
||||
assert fundamental["next_earnings_date"] == "2026-08-01"
|
||||
Reference in New Issue
Block a user