daeeeee Claude Sonnet 4.6 commited on
Commit
daeb695
·
1 Parent(s): 938ae19

feat(autotune): run_autotune orchestrator with heatmap output

Browse files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. autotune.py +93 -0
  2. tests/test_autotune.py +82 -0
autotune.py CHANGED
@@ -5,6 +5,7 @@ from typing import Optional
5
 
6
  import numpy as np
7
  import pandas as pd
 
8
 
9
  _TUNING_DDL = """
10
  CREATE TABLE IF NOT EXISTS tuning_results (
@@ -147,3 +148,95 @@ def backtest_core(predict_fn, fetch_fn,
147
  "sharpe": sharpe,
148
  "per_anchor": df,
149
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  import numpy as np
7
  import pandas as pd
8
+ import plotly.graph_objects as go
9
 
10
  _TUNING_DDL = """
11
  CREATE TABLE IF NOT EXISTS tuning_results (
 
148
  "sharpe": sharpe,
149
  "per_anchor": df,
150
  }
151
+
152
+
153
+ GRID_T = (0.5, 1.0, 1.5)
154
+ GRID_TOP_P = (0.7, 0.85, 0.95)
155
+
156
+ def run_autotune(*, predict_fn, fetch_fn, db_path: str,
157
+ symbol: str, interval: str,
158
+ start_date: str, end_date: str,
159
+ lookback: int, pred_len: int, stride: int,
160
+ sample_count: int, max_anchors: int):
161
+ params = dict(lookback=lookback, pred_len=pred_len, stride=stride,
162
+ sample_count=sample_count, max_anchors=max_anchors,
163
+ start_date=start_date, end_date=end_date)
164
+ results = []
165
+ for T in GRID_T:
166
+ for top_p in GRID_TOP_P:
167
+ try:
168
+ m = backtest_core(
169
+ predict_fn, fetch_fn,
170
+ symbol=symbol, interval=interval,
171
+ start_date=start_date, end_date=end_date,
172
+ lookback=lookback, pred_len=pred_len, stride=stride,
173
+ T=T, top_p=top_p,
174
+ sample_count=sample_count, max_anchors=max_anchors,
175
+ )
176
+ results.append({
177
+ "T": T, "top_p": top_p, "error": None,
178
+ "anchors": m["anchors"],
179
+ "mean_rmse": m["mean_rmse"],
180
+ "hit_rate": m["hit_rate"],
181
+ "total_return_pct": m["total_return_pct"],
182
+ "max_dd_pct": m["max_dd_pct"],
183
+ "sharpe": m["sharpe"],
184
+ })
185
+ except Exception as e:
186
+ results.append({
187
+ "T": T, "top_p": top_p, "error": str(e),
188
+ "anchors": 0, "mean_rmse": float("nan"),
189
+ "hit_rate": float("nan"),
190
+ "total_return_pct": float("nan"),
191
+ "max_dd_pct": float("nan"),
192
+ "sharpe": float("nan"),
193
+ })
194
+
195
+ valid = [r for r in results if r["error"] is None]
196
+ if not valid:
197
+ first_err = results[0]["error"]
198
+ raise RuntimeError(f"All grid cells failed: {first_err}")
199
+
200
+ best = max(valid, key=lambda r: r["total_return_pct"])
201
+ save_tuning(db_path, symbol, interval, best, results, params)
202
+
203
+ grid_df = pd.DataFrame(results)
204
+ z = np.full((len(GRID_T), len(GRID_TOP_P)), np.nan)
205
+ for i, T in enumerate(GRID_T):
206
+ for j, top_p in enumerate(GRID_TOP_P):
207
+ row = next(r for r in results if r["T"] == T and r["top_p"] == top_p)
208
+ if row["error"] is None:
209
+ z[i, j] = row["total_return_pct"]
210
+
211
+ fig = go.Figure(data=go.Heatmap(
212
+ z=z, x=[str(p) for p in GRID_TOP_P], y=[str(t) for t in GRID_T],
213
+ colorscale="RdYlGn", zmid=0.0, colorbar=dict(title="P&L %"),
214
+ ))
215
+ fig.update_layout(
216
+ title=f"{symbol.upper()} {interval} — Auto-tune heatmap (P&L %)",
217
+ xaxis_title="top_p", yaxis_title="T",
218
+ template="plotly_dark", height=420,
219
+ margin=dict(l=20, r=20, t=60, b=20),
220
+ )
221
+ fig.add_annotation(
222
+ x=str(best["top_p"]), y=str(best["T"]),
223
+ text="★", showarrow=False, font=dict(size=24, color="black"),
224
+ )
225
+
226
+ cells_errored = sum(1 for r in results if r["error"] is not None)
227
+ summary = pd.DataFrame({
228
+ "Field": ["Symbol", "Interval", "Best T", "Best top_p",
229
+ "Best P&L %", "Best hit rate %", "Best mean RMSE",
230
+ "Anchors per cell", "Total cells", "Cells errored"],
231
+ "Value": [
232
+ symbol.upper(), interval,
233
+ f"{best['T']}", f"{best['top_p']}",
234
+ f"{best['total_return_pct']:+.3f}",
235
+ f"{best['hit_rate'] * 100.0:.2f}",
236
+ f"{best['mean_rmse']:.4f}",
237
+ f"{best['anchors']}", f"{len(results)}",
238
+ f"{cells_errored}",
239
+ ],
240
+ })
241
+
242
+ return fig, summary, grid_df
tests/test_autotune.py CHANGED
@@ -134,3 +134,85 @@ def test_backtest_core_raises_when_lookback_plus_predlen_too_big():
134
  lookback=400, pred_len=200, stride=50,
135
  T=1.0, top_p=0.9,
136
  sample_count=1, max_anchors=5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  lookback=400, pred_len=200, stride=50,
135
  T=1.0, top_p=0.9,
136
  sample_count=1, max_anchors=5)
137
+
138
+
139
+ from unittest.mock import MagicMock
140
+ from autotune import run_autotune, GRID_T, GRID_TOP_P
141
+
142
+ def test_run_autotune_iterates_full_grid_and_picks_max(tmp_db, monkeypatch):
143
+ init_tuning_table(tmp_db)
144
+ calls = []
145
+
146
+ def fake_core(predict_fn, fetch_fn, *, symbol, interval, start_date, end_date,
147
+ lookback, pred_len, stride, T, top_p, sample_count, max_anchors):
148
+ calls.append((T, top_p))
149
+ # Make T=1.0, top_p=0.85 win clearly
150
+ score = 9.0 if (T == 1.0 and top_p == 0.85) else 1.0
151
+ return {
152
+ "anchors": 5, "mean_rmse": 1.0,
153
+ "hit_rate": 0.5,
154
+ "total_return_pct": score,
155
+ "max_dd_pct": -1.0, "sharpe": 0.5,
156
+ "per_anchor": pd.DataFrame(),
157
+ }
158
+
159
+ monkeypatch.setattr("autotune.backtest_core", fake_core)
160
+ fig, summary, grid = run_autotune(
161
+ predict_fn=MagicMock(), fetch_fn=MagicMock(),
162
+ db_path=tmp_db, symbol="SPY", interval="5min",
163
+ start_date="", end_date="",
164
+ lookback=200, pred_len=30, stride=30,
165
+ sample_count=1, max_anchors=5,
166
+ )
167
+ assert sorted(calls) == sorted([(t, p) for t in GRID_T for p in GRID_TOP_P])
168
+ assert len(grid) == 9
169
+ best_row = summary[summary["Field"] == "Best T"].iloc[0]["Value"]
170
+ assert float(best_row) == 1.0
171
+
172
+ persisted = load_tuning(tmp_db, "SPY", "5min")
173
+ assert persisted["best_T"] == 1.0
174
+ assert persisted["best_top_p"] == 0.85
175
+ assert persisted["best_pnl_pct"] == 9.0
176
+
177
+ def test_run_autotune_skips_failed_cells(tmp_db, monkeypatch):
178
+ init_tuning_table(tmp_db)
179
+
180
+ def flaky_core(predict_fn, fetch_fn, *, T, top_p, **kw):
181
+ if T == 0.5:
182
+ raise RuntimeError("fmp blew up")
183
+ return {"anchors": 5, "mean_rmse": 1.0, "hit_rate": 0.5,
184
+ "total_return_pct": 2.0 + top_p,
185
+ "max_dd_pct": -1.0, "sharpe": 0.5,
186
+ "per_anchor": pd.DataFrame()}
187
+
188
+ monkeypatch.setattr("autotune.backtest_core", flaky_core)
189
+ fig, summary, grid = run_autotune(
190
+ predict_fn=MagicMock(), fetch_fn=MagicMock(),
191
+ db_path=tmp_db, symbol="X", interval="5min",
192
+ start_date="", end_date="",
193
+ lookback=200, pred_len=30, stride=30,
194
+ sample_count=1, max_anchors=5,
195
+ )
196
+ errored = grid[grid["error"].notna()]
197
+ assert len(errored) == 3 # all T=0.5 rows
198
+ valid = grid[grid["error"].isna()]
199
+ assert len(valid) == 6
200
+ persisted = load_tuning(tmp_db, "X", "5min")
201
+ assert persisted["best_T"] != 0.5 # never the failing T
202
+
203
+ def test_run_autotune_raises_when_all_cells_fail(tmp_db, monkeypatch):
204
+ init_tuning_table(tmp_db)
205
+ def always_fail(*a, **kw):
206
+ raise RuntimeError("everything is broken")
207
+ monkeypatch.setattr("autotune.backtest_core", always_fail)
208
+
209
+ import pytest
210
+ with pytest.raises(RuntimeError, match="All grid cells failed"):
211
+ run_autotune(
212
+ predict_fn=MagicMock(), fetch_fn=MagicMock(),
213
+ db_path=tmp_db, symbol="X", interval="5min",
214
+ start_date="", end_date="",
215
+ lookback=200, pred_len=30, stride=30,
216
+ sample_count=1, max_anchors=5,
217
+ )
218
+ assert load_tuning(tmp_db, "X", "5min") is None