""" Parameter tuning for Kalman Pairs Trader. Grid search over transition_covariance (and optionally observation_covariance) to find optimal settings that maximize out-of-sample Sharpe while controlling turnover. Design: - Train/validation split (chronological, no look-ahead) - Grid search over log-spaced transition_covariance values - Objective: maximize Sharpe_validation - λ * max_drawdown_penalty - Reports top-N parameter sets with full metrics """ from __future__ import annotations import numpy as np from typing import Optional from .kalman_filter import KalmanPairsTrader from .backtest import backtest_kalman_pairs def grid_search_transition_cov( X_train: np.ndarray, Y_train: np.ndarray, X_val: np.ndarray, Y_val: np.ndarray, transition_cov_range: tuple[float, float, int] = (1e-6, 1e-1, 20), observation_covariance: float = 1e-2, z_entry: float = 2.0, z_exit: float = 0.5, max_drawdown_penalty: float = 0.5, trade_size_usd: float = 100.0, transaction_cost_bps: float = 2.5, ) -> list[dict]: """ Grid search optimal transition_covariance. Strategy: 1. Split data chronologically (train → validation). 2. For each Q value, run Kalman backtest on validation set (with no pre-training — Kalman adapts online). 3. Score = Sharpe − λ * max_drawdown. 4. Return sorted results. Args: X_train, Y_train: Training price series (used for initialization only). X_val, Y_val: Validation price series (out-of-sample test). transition_cov_range: (min, max, num_steps) in log space. max_drawdown_penalty: Weight for drawdown penalty in scoring. Returns: List of dicts sorted by score (descending), each with: transition_cov, sharpe, sortino, max_drawdown, win_rate, total_trades, score. """ q_min, q_max, n_steps = transition_cov_range q_values = np.logspace(np.log10(q_min), np.log10(q_max), n_steps) results = [] for q in q_values: trader = KalmanPairsTrader( transition_covariance=float(q), observation_covariance=observation_covariance, z_entry=z_entry, z_exit=z_exit, ) # Pre-warm on training data (online filtering, no position taking) for x, y in zip(X_train, Y_train): trader.kf.update(float(x), float(y)) # Backtest on validation bt = backtest_kalman_pairs( X_val, Y_val, trader, trade_size_usd=trade_size_usd, transaction_cost_bps=transaction_cost_bps, ) score = bt["sharpe"] - max_drawdown_penalty * bt["max_drawdown"] results.append({ "transition_cov": float(q), "sharpe": bt["sharpe"], "sortino": bt["sortino"], "max_drawdown": bt["max_drawdown"], "win_rate": bt["win_rate"], "total_trades": bt["total_trades"], "pnl_pct": bt["pnl_pct"], "score": round(score, 4), }) results.sort(key=lambda r: r["score"], reverse=True) return results def find_optimal_params( X: np.ndarray, Y: np.ndarray, train_frac: float = 0.6, **grid_kwargs, ) -> dict: """ One-shot: split data, run grid search, return best params. Returns: dict with: best_params, all_results, train_size, val_size. """ n = len(X) split = int(n * train_frac) X_train, X_val = X[:split], X[split:] Y_train, Y_val = Y[:split], Y[split:] grid = grid_search_transition_cov(X_train, Y_train, X_val, Y_val, **grid_kwargs) return { "best_params": { "transition_covariance": grid[0]["transition_cov"] if grid else 1e-4, }, "best_score": grid[0]["score"] if grid else 0.0, "all_results": grid, "train_size": len(X_train), "val_size": len(X_val), }