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,45 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class SignalContextSnapshot(Base):
|
||||
"""Point-in-time context captured when a trade setup is generated.
|
||||
|
||||
This stores the discretionary overlay inputs (scores, sentiment,
|
||||
fundamentals) as they looked at detection time, so future analysis can test
|
||||
whether human filtering improved or hurt the qualified-list strategy.
|
||||
"""
|
||||
|
||||
__tablename__ = "signal_context_snapshots"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
trade_setup_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("trade_setups.id", ondelete="CASCADE"), nullable=False, unique=True
|
||||
)
|
||||
ticker_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("tickers.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
detected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
strategy_version: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
|
||||
direction: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
entry_price: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
stop_loss: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
target: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
rr_ratio: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
confidence_score: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
recommended_action: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
risk_level: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
||||
momentum_percentile: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
score_context_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
|
||||
sentiment_context_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
|
||||
fundamental_context_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
|
||||
|
||||
trade_setup = relationship("TradeSetup")
|
||||
ticker = relationship("Ticker")
|
||||
Reference in New Issue
Block a user