80b4113280
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>
111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
"""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"
|