Add walk-forward backtest optimization to mitigate signal overfitting (item m)
Rolling train/test folds over 3 years of data auto-optimize the three cheap-to-tune trading parameters (STRONG/BUY score thresholds, max hold time) via grid search on each fold's train window, then evaluate purely on the held-out test window. Stitching all out-of-sample results gives an honest performance estimate uninflated by tuning against the same data used to score it. Split signal_scoring.py's expensive 13-algorithm scoring from its cheap final threshold classification so grid search can replay many parameter combinations without recomputing indicators each time. Moved the backtest engine (fetch/precompute/simulate) out of the API layer into app/services/backtest_engine.py so both /backtest/run and the new walk-forward optimizer share one implementation instead of drifting copies — same rationale as the earlier signal_service.py split (item h). Also merges two long-diverged Alembic migration heads discovered while adding the walk_forward_results table, so `alembic upgrade head` has a single target again. New: POST/GET/DELETE /walk-forward/* endpoints, a Walk-Forward tab on the Backtest page (fold table, out-of-sample equity curve, run history). 19 new backend tests (153 total, all passing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""Add walk_forward_results table
|
||||
|
||||
Stores walk-forward backtest runs: rolling train/test folds with
|
||||
per-fold optimized parameters and in-sample vs out-of-sample metrics,
|
||||
plus the aggregated out-of-sample summary. `result_json` holds the
|
||||
full fold-by-fold detail; the flat columns are for fast history listing.
|
||||
|
||||
Revision ID: add_walk_forward_results
|
||||
Revises: merge_heads_1
|
||||
Create Date: 2026-07-04
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "add_walk_forward_results"
|
||||
down_revision: Union[str, None] = "merge_heads_1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"walk_forward_results",
|
||||
sa.Column("id", sa.UUID(), nullable=False),
|
||||
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||
sa.Column("symbol", sa.String(length=50), nullable=False),
|
||||
sa.Column("exchange", sa.String(length=20), nullable=False),
|
||||
sa.Column("timeframe", sa.String(length=10), nullable=False),
|
||||
sa.Column("total_days", sa.Integer(), nullable=False),
|
||||
sa.Column("train_days", sa.Integer(), nullable=False),
|
||||
sa.Column("test_days", sa.Integer(), nullable=False),
|
||||
sa.Column("folds_count", sa.Integer(), nullable=False),
|
||||
sa.Column("oos_trades", sa.Integer(), nullable=False),
|
||||
sa.Column("oos_win_rate", sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column("oos_total_pnl", sa.Numeric(precision=20, scale=8), nullable=True),
|
||||
sa.Column("oos_profit_factor", sa.Numeric(precision=10, scale=4), nullable=True),
|
||||
sa.Column("oos_max_drawdown_pct", sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column("result_json", sa.Text(), nullable=False, comment="Full fold-by-fold detail + stitched OOS equity curve"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_walk_forward_results_user_id", "walk_forward_results", ["user_id"], unique=False)
|
||||
op.create_index("ix_walk_forward_results_created_at", "walk_forward_results", ["created_at"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_walk_forward_results_created_at", table_name="walk_forward_results")
|
||||
op.drop_index("ix_walk_forward_results_user_id", table_name="walk_forward_results")
|
||||
op.drop_table("walk_forward_results")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""merge divergent heads (1b3f1630986f, 4_add_sl_tp_columns)
|
||||
|
||||
Both branched off add_candle_partitions independently, leaving two
|
||||
unmerged heads. This is a no-op merge so `alembic upgrade head` has a
|
||||
single target again.
|
||||
|
||||
Revision ID: merge_heads_1
|
||||
Revises: 1b3f1630986f, 4_add_sl_tp_columns
|
||||
Create Date: 2026-07-04
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
revision: str = "merge_heads_1"
|
||||
down_revision: Union[str, Sequence[str], None] = ("1b3f1630986f", "4_add_sl_tp_columns")
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
Reference in New Issue
Block a user