kronos-dashboard / tests /test_autotune.py
daeeeee's picture
feat(autotune): apply_tuning helper for Forecast tab pre-fill
fb03e07
Raw
History Blame Contribute Delete
10.3 kB
import sqlite3
import json
from autotune import init_tuning_table, save_tuning, load_tuning
def _params(**overrides):
base = dict(
lookback=200, pred_len=30, stride=30,
sample_count=1, max_anchors=20,
start_date="", end_date="",
)
base.update(overrides)
return base
def test_init_tuning_table_creates_schema(tmp_db):
init_tuning_table(tmp_db)
with sqlite3.connect(tmp_db) as c:
cols = [r[1] for r in c.execute("PRAGMA table_info(tuning_results)").fetchall()]
assert "symbol" in cols
assert "interval" in cols
assert "best_T" in cols
assert "best_top_p" in cols
assert "best_pnl_pct" in cols
assert "grid_json" in cols
def test_init_tuning_table_unique_constraint(tmp_db):
init_tuning_table(tmp_db)
with sqlite3.connect(tmp_db) as c:
c.execute("INSERT INTO tuning_results (ts, symbol, interval, best_T, best_top_p, best_pnl_pct, best_hit_rate, best_rmse, grid_json) "
"VALUES ('t1','SPY','5min',1.0,0.9,5.0,0.6,1.2,'[]')")
c.execute("INSERT INTO tuning_results (ts, symbol, interval, best_T, best_top_p, best_pnl_pct, best_hit_rate, best_rmse, grid_json) "
"VALUES ('t2','SPY','5min',1.5,0.85,7.0,0.7,1.0,'[]')")
rows = c.execute("SELECT best_T, best_top_p FROM tuning_results WHERE symbol='SPY' AND interval='5min'").fetchall()
assert rows == [(1.5, 0.85)]
def test_save_and_load_roundtrip(tmp_db):
init_tuning_table(tmp_db)
best = {"T": 1.0, "top_p": 0.85, "total_return_pct": 4.2,
"hit_rate": 0.6, "mean_rmse": 1.1}
results = [{"T": 1.0, "top_p": 0.85, "total_return_pct": 4.2}]
save_tuning(tmp_db, "SPY", "5min", best, results, _params())
got = load_tuning(tmp_db, "SPY", "5min")
assert got is not None
assert got["best_T"] == 1.0
assert got["best_top_p"] == 0.85
assert got["best_pnl_pct"] == 4.2
assert json.loads(got["grid_json"]) == results
def test_load_tuning_missing_returns_none(tmp_db):
init_tuning_table(tmp_db)
assert load_tuning(tmp_db, "ZZZ", "1min") is None
def test_save_tuning_replaces_on_conflict(tmp_db):
init_tuning_table(tmp_db)
save_tuning(tmp_db, "SPY", "5min",
{"T": 1.0, "top_p": 0.85, "total_return_pct": 1.0,
"hit_rate": 0.5, "mean_rmse": 2.0},
[], _params())
save_tuning(tmp_db, "SPY", "5min",
{"T": 1.5, "top_p": 0.7, "total_return_pct": 9.0,
"hit_rate": 0.8, "mean_rmse": 0.5},
[], _params())
got = load_tuning(tmp_db, "SPY", "5min")
assert got["best_T"] == 1.5
assert got["best_pnl_pct"] == 9.0
with sqlite3.connect(tmp_db) as c:
n = c.execute("SELECT COUNT(*) FROM tuning_results WHERE symbol='SPY' AND interval='5min'").fetchone()[0]
assert n == 1
import numpy as np
import pandas as pd
from autotune import backtest_core
def _make_full(rows):
ts = pd.date_range("2026-01-01", periods=rows, freq="5min")
closes = np.linspace(100.0, 110.0, rows)
df = pd.DataFrame({
"timestamps": ts,
"open": closes, "high": closes + 0.1, "low": closes - 0.1,
"close": closes, "volume": 1000.0, "amount": closes * 1000.0,
})
return df
def _stub_predict(rising):
"""Returns a predict fn that always forecasts a monotone path."""
def _f(df, x_timestamp, y_timestamp, pred_len, T, top_p, sample_count, verbose):
last = float(df["close"].iloc[-1])
delta = 1.0 if rising else -1.0
path = np.linspace(last + delta, last + delta * pred_len, pred_len)
return pd.DataFrame({
"open": path, "high": path + 0.1, "low": path - 0.1,
"close": path, "volume": 1000.0, "amount": path * 1000.0,
}, index=y_timestamp)
return _f
def test_backtest_core_perfect_long_when_price_rises_and_forecast_rises():
full = _make_full(rows=400) # rising
fetch_fn = lambda symbol, interval, n_bars: full.copy()
predict_fn = _stub_predict(rising=True)
out = backtest_core(predict_fn, fetch_fn,
symbol="X", interval="5min",
start_date="", end_date="",
lookback=100, pred_len=10, stride=10,
T=1.0, top_p=0.9,
sample_count=1, max_anchors=5)
assert out["anchors"] == 5
assert out["hit_rate"] == 1.0 # forecast up, realized up — every anchor hits
assert out["total_return_pct"] > 0
assert isinstance(out["per_anchor"], pd.DataFrame)
assert len(out["per_anchor"]) == 5
def test_backtest_core_short_when_forecast_down_but_price_up():
full = _make_full(rows=400)
fetch_fn = lambda *a, **k: full.copy()
predict_fn = _stub_predict(rising=False) # forecast says down
out = backtest_core(predict_fn, fetch_fn,
symbol="X", interval="5min",
start_date="", end_date="",
lookback=100, pred_len=10, stride=10,
T=1.0, top_p=0.9,
sample_count=1, max_anchors=5)
assert out["hit_rate"] == 0.0 # always wrong
assert out["total_return_pct"] < 0 # short while market rises = loss
def test_backtest_core_raises_when_lookback_plus_predlen_too_big():
full = _make_full(rows=400)
fetch_fn = lambda *a, **k: full.copy()
predict_fn = _stub_predict(rising=True)
import pytest
with pytest.raises(ValueError, match="≤ 512"):
backtest_core(predict_fn, fetch_fn,
symbol="X", interval="5min",
start_date="", end_date="",
lookback=400, pred_len=200, stride=50,
T=1.0, top_p=0.9,
sample_count=1, max_anchors=5)
from unittest.mock import MagicMock
from autotune import run_autotune, GRID_T, GRID_TOP_P
def test_run_autotune_iterates_full_grid_and_picks_max(tmp_db, monkeypatch):
init_tuning_table(tmp_db)
calls = []
def fake_core(predict_fn, fetch_fn, *, symbol, interval, start_date, end_date,
lookback, pred_len, stride, T, top_p, sample_count, max_anchors):
calls.append((T, top_p))
# Make T=1.0, top_p=0.85 win clearly
score = 9.0 if (T == 1.0 and top_p == 0.85) else 1.0
return {
"anchors": 5, "mean_rmse": 1.0,
"hit_rate": 0.5,
"total_return_pct": score,
"max_dd_pct": -1.0, "sharpe": 0.5,
"per_anchor": pd.DataFrame(),
}
monkeypatch.setattr("autotune.backtest_core", fake_core)
fig, summary, grid = run_autotune(
predict_fn=MagicMock(), fetch_fn=MagicMock(),
db_path=tmp_db, symbol="SPY", interval="5min",
start_date="", end_date="",
lookback=200, pred_len=30, stride=30,
sample_count=1, max_anchors=5,
)
assert sorted(calls) == sorted([(t, p) for t in GRID_T for p in GRID_TOP_P])
assert len(grid) == 9
best_row = summary[summary["Field"] == "Best T"].iloc[0]["Value"]
assert float(best_row) == 1.0
persisted = load_tuning(tmp_db, "SPY", "5min")
assert persisted["best_T"] == 1.0
assert persisted["best_top_p"] == 0.85
assert persisted["best_pnl_pct"] == 9.0
def test_run_autotune_skips_failed_cells(tmp_db, monkeypatch):
init_tuning_table(tmp_db)
def flaky_core(predict_fn, fetch_fn, *, T, top_p, **kw):
if T == 0.5:
raise RuntimeError("fmp blew up")
return {"anchors": 5, "mean_rmse": 1.0, "hit_rate": 0.5,
"total_return_pct": 2.0 + top_p,
"max_dd_pct": -1.0, "sharpe": 0.5,
"per_anchor": pd.DataFrame()}
monkeypatch.setattr("autotune.backtest_core", flaky_core)
fig, summary, grid = run_autotune(
predict_fn=MagicMock(), fetch_fn=MagicMock(),
db_path=tmp_db, symbol="X", interval="5min",
start_date="", end_date="",
lookback=200, pred_len=30, stride=30,
sample_count=1, max_anchors=5,
)
errored = grid[grid["error"].notna()]
assert len(errored) == 3 # all T=0.5 rows
valid = grid[grid["error"].isna()]
assert len(valid) == 6
persisted = load_tuning(tmp_db, "X", "5min")
assert persisted["best_T"] != 0.5 # never the failing T
def test_run_autotune_raises_when_all_cells_fail(tmp_db, monkeypatch):
init_tuning_table(tmp_db)
def always_fail(*a, **kw):
raise RuntimeError("everything is broken")
monkeypatch.setattr("autotune.backtest_core", always_fail)
import pytest
with pytest.raises(RuntimeError, match="All grid cells failed"):
run_autotune(
predict_fn=MagicMock(), fetch_fn=MagicMock(),
db_path=tmp_db, symbol="X", interval="5min",
start_date="", end_date="",
lookback=200, pred_len=30, stride=30,
sample_count=1, max_anchors=5,
)
assert load_tuning(tmp_db, "X", "5min") is None
def test_apply_tuning_returns_values_when_cached_and_enabled(tmp_db):
from autotune import apply_tuning
init_tuning_table(tmp_db)
save_tuning(tmp_db, "SPY", "5min",
{"T": 1.5, "top_p": 0.7, "total_return_pct": 7.0,
"hit_rate": 0.6, "mean_rmse": 0.9},
[], {"lookback": 200, "pred_len": 30, "stride": 30,
"sample_count": 1, "max_anchors": 20,
"start_date": "", "end_date": ""})
t_val, top_p_val = apply_tuning(tmp_db, "SPY", "5min", True)
assert t_val == 1.5
assert top_p_val == 0.7
def test_apply_tuning_returns_none_when_disabled(tmp_db):
from autotune import apply_tuning
init_tuning_table(tmp_db)
save_tuning(tmp_db, "SPY", "5min",
{"T": 1.5, "top_p": 0.7, "total_return_pct": 7.0,
"hit_rate": 0.6, "mean_rmse": 0.9},
[], {"lookback": 200, "pred_len": 30, "stride": 30,
"sample_count": 1, "max_anchors": 20,
"start_date": "", "end_date": ""})
assert apply_tuning(tmp_db, "SPY", "5min", False) == (None, None)
def test_apply_tuning_returns_none_when_no_cache(tmp_db):
from autotune import apply_tuning
init_tuning_table(tmp_db)
assert apply_tuning(tmp_db, "ZZZ", "1min", True) == (None, None)