feat: configurable grid params + auto walk-forward optimizer
Track 1 — Grid param sweep in vbt_runner:
- _generate_signals accepts params dict: grid_levels, spacing_bps, rebalance_every
- run_strategy passes params through to signals
- _strategy_params reflects actual runtime params
- Grid param sweep results: spacing is critical, levels don't matter
Tight spacing (1-2bps) = 1 trade, positive EV
Wide spacing (20bps+) = many trades, negative EV
Candle simulation can't model grid MM fills accurately
Track 6 — quant/optimizer.py:
- ParanOptimizer: automated IS/OOS parameter walk-forward
- add_param() to define parameter grid
- Composite score: Sharpe × sqrt(trades) for robustness
- IS optimization per window, OOS testing per window
- WFParamWindow + OptimizerReport with consistency + stable params
Grid MM walk-forward results (3 windows):
W0: IS S=-3.75 → OOS S=+2.23 (+12.7%, 1t)
W1: IS S=+2.52 → OOS S=-3.49 (-19.0%, 11t)
W2: IS S=-3.30 → OOS S=0.00 (0t)
Consistency: 33.3%, Stable params: {levels=5, spacing=1bps, rebalance=5}
Verdict: Candle-based grid MM is fundamentally unreliable.
Real fills require queue simulation with L2 data.
This commit is contained in:
+31
-17
@@ -35,12 +35,21 @@ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
|||||||
# Strategy signal generators
|
# Strategy signal generators
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _generate_signals(strategy: str, data: dict[str, pd.DataFrame]) -> tuple[pd.Series, pd.Series]:
|
def _generate_signals(strategy: str, data: dict[str, pd.DataFrame],
|
||||||
|
params: dict | None = None) -> tuple[pd.Series, pd.Series]:
|
||||||
"""Generate entry/exit signals for a strategy from candle data.
|
"""Generate entry/exit signals for a strategy from candle data.
|
||||||
|
|
||||||
Returns (entries, exits) as boolean pandas Series.
|
Returns (entries, exits) as boolean pandas Series.
|
||||||
Each strategy uses the primary coin's close prices.
|
Each strategy uses the primary coin's close prices.
|
||||||
|
|
||||||
|
Params:
|
||||||
|
grid_mm: grid_levels, spacing_bps, rebalance_every
|
||||||
|
as_mm: gamma, k, tau, min_hold, max_hold, profit_target, stop_loss
|
||||||
|
obi: lookback, entry_threshold, exit_threshold
|
||||||
|
pairs: z_entry, z_exit, lookback
|
||||||
"""
|
"""
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
main_coin = {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC",
|
main_coin = {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC",
|
||||||
"obi": "BTC", "grid_mm": "BTC", "composite_mm": "BTC",
|
"obi": "BTC", "grid_mm": "BTC", "composite_mm": "BTC",
|
||||||
"iceberg": "BTC", "funding_arb": "BTC", "momentum": "BTC",
|
"iceberg": "BTC", "funding_arb": "BTC", "momentum": "BTC",
|
||||||
@@ -142,14 +151,13 @@ def _generate_signals(strategy: str, data: dict[str, pd.DataFrame]) -> tuple[pd.
|
|||||||
|
|
||||||
elif strategy == "grid_mm":
|
elif strategy == "grid_mm":
|
||||||
# Grid MM: simulate grid fills from candle high/low ranges
|
# Grid MM: simulate grid fills from candle high/low ranges
|
||||||
grid_levels = 10
|
grid_levels = params.get("grid_levels", 10)
|
||||||
grid_spacing_pct = 0.001
|
grid_spacing_pct = params.get("spacing_bps", 10) / 10000 # bps → decimal
|
||||||
|
rebalance = params.get("rebalance_every", 20)
|
||||||
|
|
||||||
entries = pd.Series(False, index=close.index)
|
entries = pd.Series(False, index=close.index)
|
||||||
exits = pd.Series(False, index=close.index)
|
exits = pd.Series(False, index=close.index)
|
||||||
# Track grid state per bar
|
fills_accumulated = 0
|
||||||
grid_fills = 0
|
|
||||||
prev_entry = 0
|
|
||||||
|
|
||||||
for i in range(1, len(close)):
|
for i in range(1, len(close)):
|
||||||
mid = close.iloc[i]
|
mid = close.iloc[i]
|
||||||
@@ -164,10 +172,11 @@ def _generate_signals(strategy: str, data: dict[str, pd.DataFrame]) -> tuple[pd.
|
|||||||
if high >= sell_px:
|
if high >= sell_px:
|
||||||
fills_this_bar += 1
|
fills_this_bar += 1
|
||||||
if fills_this_bar > 0:
|
if fills_this_bar > 0:
|
||||||
|
fills_accumulated += fills_this_bar
|
||||||
entries.iloc[i] = True
|
entries.iloc[i] = True
|
||||||
# Exit after spread capture (next bar close)
|
# Exit after rebalance period
|
||||||
if i + 1 < len(close):
|
if i + rebalance < len(close):
|
||||||
exits.iloc[i + 1] = True
|
exits.iloc[i + rebalance] = True
|
||||||
|
|
||||||
elif strategy == "composite_mm":
|
elif strategy == "composite_mm":
|
||||||
# Composite: weighted ensemble of OBI + Hurst
|
# Composite: weighted ensemble of OBI + Hurst
|
||||||
@@ -314,6 +323,7 @@ class VBTBacktestRunner:
|
|||||||
limit: int = 5000,
|
limit: int = 5000,
|
||||||
start_ms: int | None = None,
|
start_ms: int | None = None,
|
||||||
end_ms: int | None = None,
|
end_ms: int | None = None,
|
||||||
|
params: dict | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Fetch candles, generate signals, run VBT backtest, return metrics."""
|
"""Fetch candles, generate signals, run VBT backtest, return metrics."""
|
||||||
coins = self._get_coins(strategy)
|
coins = self._get_coins(strategy)
|
||||||
@@ -332,7 +342,7 @@ class VBTBacktestRunner:
|
|||||||
logger.error("No candle data fetched for strategy: %s", strategy)
|
logger.error("No candle data fetched for strategy: %s", strategy)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
entries, exits = _generate_signals(strategy, data)
|
entries, exits = _generate_signals(strategy, data, params)
|
||||||
|
|
||||||
primary = list(data.values())[0]
|
primary = list(data.values())[0]
|
||||||
close = primary["close"]
|
close = primary["close"]
|
||||||
@@ -365,7 +375,7 @@ class VBTBacktestRunner:
|
|||||||
return self._empty_result(strategy, interval)
|
return self._empty_result(strategy, interval)
|
||||||
|
|
||||||
stats = pf.stats()
|
stats = pf.stats()
|
||||||
result = self._extract_metrics(pf, stats, strategy, interval, len(close))
|
result = self._extract_metrics(pf, stats, strategy, interval, len(close), params)
|
||||||
|
|
||||||
# Save equity curve
|
# Save equity curve
|
||||||
eq_curve = pf.value().dropna()
|
eq_curve = pf.value().dropna()
|
||||||
@@ -448,7 +458,8 @@ class VBTBacktestRunner:
|
|||||||
}
|
}
|
||||||
return coin_map.get(strategy, ["BTC"])
|
return coin_map.get(strategy, ["BTC"])
|
||||||
|
|
||||||
def _extract_metrics(self, pf, stats, strategy, interval, n_bars) -> dict:
|
def _extract_metrics(self, pf, stats, strategy, interval, n_bars,
|
||||||
|
runtime_params=None) -> dict:
|
||||||
from config.fee_tiers import compute_trade_fees, get_strategy_fee_model
|
from config.fee_tiers import compute_trade_fees, get_strategy_fee_model
|
||||||
|
|
||||||
main_coin = self._get_coins(strategy)[0]
|
main_coin = self._get_coins(strategy)[0]
|
||||||
@@ -519,7 +530,7 @@ class VBTBacktestRunner:
|
|||||||
"profit_factor": round(float(stats.get("Profit Factor", 0)), 3),
|
"profit_factor": round(float(stats.get("Profit Factor", 0)), 3),
|
||||||
"expectancy": round(float(stats.get("Expectancy", 0)), 3),
|
"expectancy": round(float(stats.get("Expectancy", 0)), 3),
|
||||||
"trades": trades,
|
"trades": trades,
|
||||||
"params": _strategy_params(strategy),
|
"params": _strategy_params(strategy, runtime_params),
|
||||||
"fee_info": fee_info,
|
"fee_info": fee_info,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,20 +554,23 @@ class VBTBacktestRunner:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _strategy_params(strategy: str) -> dict:
|
def _strategy_params(strategy: str, runtime_params: dict | None = None) -> dict:
|
||||||
"""Return the key parameters/coefficients for a strategy."""
|
"""Return the key parameters/coefficients for a strategy."""
|
||||||
params = {
|
base = {
|
||||||
"pairs": {"z_entry": 1.5, "z_exit": 0.5, "lookback": 20, "type": "Stat Arb"},
|
"pairs": {"z_entry": 1.5, "z_exit": 0.5, "lookback": 20, "type": "Stat Arb"},
|
||||||
"hurst_vpin": {"hurst_entry": 0.55, "hurst_exit": 0.45, "vpin_threshold": 0.25, "vpin_window": 50, "hurst_window": 64, "type": "Directional"},
|
"hurst_vpin": {"hurst_entry": 0.55, "hurst_exit": 0.45, "vpin_threshold": 0.25, "vpin_window": 50, "hurst_window": 64, "type": "Directional"},
|
||||||
"as_mm": {"gamma": 0.1, "sigma_dynamic": True, "inventory_skew": True, "type": "Market Making"},
|
"as_mm": {"gamma": 0.1, "sigma_dynamic": True, "inventory_skew": True, "type": "Market Making"},
|
||||||
"obi": {"obi_lookback": 20, "obi_entry": 0.35, "obi_exit": 0.10, "type": "Reversal"},
|
"obi": {"obi_lookback": 20, "obi_entry": 0.35, "obi_exit": 0.10, "type": "Reversal"},
|
||||||
"grid_mm": {"grid_levels": 10, "grid_spacing_pct": 0.1, "rebalance_every": 20, "type": "Market Making"},
|
"grid_mm": {"grid_levels": 10, "spacing_bps": 10, "rebalance_every": 20, "type": "Market Making"},
|
||||||
"composite_mm": {"obi_weight": 0.30, "as_weight": 0.40, "hurst_weight": 0.30, "entry_score": 0.50, "type": "Ensemble"},
|
"composite_mm": {"obi_weight": 0.30, "as_weight": 0.40, "hurst_weight": 0.30, "entry_score": 0.50, "type": "Ensemble"},
|
||||||
"iceberg": {"vol_mult": 1.8, "min_consec": 3, "max_hold": 8, "type": "Momentum"},
|
"iceberg": {"vol_mult": 1.8, "min_consec": 3, "max_hold": 8, "type": "Momentum"},
|
||||||
"momentum": {"bollinger_window": 20, "bollinger_std": 2.0, "type": "Momentum"},
|
"momentum": {"bollinger_window": 20, "bollinger_std": 2.0, "type": "Momentum"},
|
||||||
"mean_rev": {"vwap_window": 20, "deviation": 1.0, "type": "Reversal"},
|
"mean_rev": {"vwap_window": 20, "deviation": 1.0, "type": "Reversal"},
|
||||||
}
|
}
|
||||||
return params.get(strategy, {"type": "Unknown"})
|
result = base.get(strategy, {"type": "Unknown"})
|
||||||
|
if runtime_params:
|
||||||
|
result.update({k: v for k, v in runtime_params.items() if k in result})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _generate_signals_sweep(
|
def _generate_signals_sweep(
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
"""
|
||||||
|
Automated parameter walk-forward optimizer.
|
||||||
|
|
||||||
|
Takes a strategy and parameter grid, runs IS/OOS walk-forward across
|
||||||
|
N windows, picks the best parameter combo per window, and reports
|
||||||
|
out-of-sample performance with statistical significance.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
optimizer = ParamOptimizer(strategy='grid_mm', interval='1h')
|
||||||
|
optimizer.add_param('grid_levels', [5, 10, 20, 50])
|
||||||
|
optimizer.add_param('spacing_bps', [1, 2, 5, 10, 20, 50])
|
||||||
|
optimizer.add_param('rebalance_every', [5, 10, 20, 50])
|
||||||
|
report = optimizer.run(n_windows=4)
|
||||||
|
print(report.summary())
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParamResult:
|
||||||
|
"""Single parameter combo result."""
|
||||||
|
params: dict
|
||||||
|
sharpe: float
|
||||||
|
return_pct: float
|
||||||
|
trades: int
|
||||||
|
win_rate: float = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def score(self) -> float:
|
||||||
|
"""Composite score: Sharpe weighted by sqrt(trades) for robustness."""
|
||||||
|
return self.sharpe * (self.trades ** 0.5) if self.trades > 0 else -999.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WFParamWindow:
|
||||||
|
"""Single walk-forward window with parameter optimization."""
|
||||||
|
window_idx: int
|
||||||
|
is_start: str
|
||||||
|
is_end: str
|
||||||
|
oos_start: str
|
||||||
|
oos_end: str
|
||||||
|
best_params: dict
|
||||||
|
is_sharpe: float
|
||||||
|
oos_sharpe: float
|
||||||
|
is_return_pct: float
|
||||||
|
oos_return_pct: float
|
||||||
|
oos_trades: int
|
||||||
|
is_trials: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OptimizerReport:
|
||||||
|
"""Complete parameter optimization walk-forward report."""
|
||||||
|
strategy: str
|
||||||
|
interval: str
|
||||||
|
n_windows: int
|
||||||
|
total_trials: int
|
||||||
|
windows: list[WFParamWindow] = field(default_factory=list)
|
||||||
|
elapsed_seconds: float = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def consistency(self) -> float:
|
||||||
|
if not self.windows:
|
||||||
|
return 0.0
|
||||||
|
return sum(1 for w in self.windows if w.oos_sharpe > 0) / len(self.windows)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avg_oos_sharpe(self) -> float:
|
||||||
|
if not self.windows:
|
||||||
|
return 0.0
|
||||||
|
return sum(w.oos_sharpe for w in self.windows) / len(self.windows)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def best_stable_params(self) -> dict | None:
|
||||||
|
"""Find params that appear most frequently across windows."""
|
||||||
|
from collections import Counter
|
||||||
|
param_sigs = []
|
||||||
|
for w in self.windows:
|
||||||
|
sig = tuple(sorted(w.best_params.items()))
|
||||||
|
param_sigs.append(sig)
|
||||||
|
if not param_sigs:
|
||||||
|
return None
|
||||||
|
most_common = Counter(param_sigs).most_common(1)[0]
|
||||||
|
return dict(most_common[0])
|
||||||
|
|
||||||
|
def summary(self) -> dict:
|
||||||
|
return {
|
||||||
|
"strategy": self.strategy,
|
||||||
|
"interval": self.interval,
|
||||||
|
"n_windows": self.n_windows,
|
||||||
|
"total_trials": self.total_trials,
|
||||||
|
"consistency": round(self.consistency, 3),
|
||||||
|
"avg_oos_sharpe": round(self.avg_oos_sharpe, 3),
|
||||||
|
"stable_params": self.best_stable_params,
|
||||||
|
"elapsed_s": round(self.elapsed_seconds, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self):
|
||||||
|
for w in self.windows:
|
||||||
|
print(f' W{w.window_idx}: {w.is_start}→{w.is_end}/{w.oos_start}→{w.oos_end}')
|
||||||
|
print(f' Best params: {w.best_params}')
|
||||||
|
print(f' IS: S={w.is_sharpe:.2f} ret={w.is_return_pct:.1f}% ({w.is_trials} trials)')
|
||||||
|
print(f' OOS: S={w.oos_sharpe:.2f} ret={w.oos_return_pct:.1f}% ({w.oos_trades}t)')
|
||||||
|
print(f' Consistency: {self.consistency:.1%} Avg OOS Sharpe: {self.avg_oos_sharpe:.2f}')
|
||||||
|
if self.best_stable_params:
|
||||||
|
print(f' Stable params: {self.best_stable_params}')
|
||||||
|
|
||||||
|
|
||||||
|
class ParamOptimizer:
|
||||||
|
"""Automated strategy parameter walk-forward optimizer."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
strategy: str = "grid_mm",
|
||||||
|
interval: str = "1h",
|
||||||
|
coin: str = "BTC",
|
||||||
|
n_windows: int = 4,
|
||||||
|
fee_tier: int = 0,
|
||||||
|
staking_tier: str = "none",
|
||||||
|
):
|
||||||
|
self._strategy = strategy
|
||||||
|
self._interval = interval
|
||||||
|
self._coin = coin
|
||||||
|
self._n_windows = n_windows
|
||||||
|
self._fee_tier = fee_tier
|
||||||
|
self._staking_tier = staking_tier
|
||||||
|
self._param_grid: dict[str, list] = {}
|
||||||
|
|
||||||
|
def add_param(self, name: str, values: list):
|
||||||
|
"""Add a parameter to sweep."""
|
||||||
|
self._param_grid[name] = values
|
||||||
|
|
||||||
|
def _generate_combos(self) -> list[dict]:
|
||||||
|
"""Generate all param combinations from the grid."""
|
||||||
|
if not self._param_grid:
|
||||||
|
return [{}]
|
||||||
|
keys = list(self._param_grid.keys())
|
||||||
|
combos = []
|
||||||
|
for values in itertools.product(*self._param_grid.values()):
|
||||||
|
combos.append(dict(zip(keys, values)))
|
||||||
|
return combos
|
||||||
|
|
||||||
|
def _optimize_is(self, is_start_ms: int, is_end_ms: int) -> ParamResult:
|
||||||
|
"""Find best params on in-sample data."""
|
||||||
|
from backtests.vbt_runner import VBTBacktestRunner
|
||||||
|
|
||||||
|
best = ParamResult(params={}, sharpe=-999, return_pct=0, trades=0)
|
||||||
|
combos = self._generate_combos()
|
||||||
|
|
||||||
|
async_run_limit = 720
|
||||||
|
if self._interval == "1h":
|
||||||
|
async_run_limit = 720
|
||||||
|
elif self._interval == "4h":
|
||||||
|
async_run_limit = 180
|
||||||
|
elif self._interval == "1d":
|
||||||
|
async_run_limit = 30
|
||||||
|
|
||||||
|
for combo in combos:
|
||||||
|
try:
|
||||||
|
runner = VBTBacktestRunner(
|
||||||
|
vip_tier=self._fee_tier, staking_tier=self._staking_tier
|
||||||
|
)
|
||||||
|
result = runner.run_strategy(
|
||||||
|
strategy=self._strategy, interval=self._interval,
|
||||||
|
limit=async_run_limit, start_ms=is_start_ms, end_ms=is_end_ms,
|
||||||
|
params=combo,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
sh = result.get("sharpe", -999)
|
||||||
|
ret = result.get("total_return_pct", 0)
|
||||||
|
tr = len(result.get("trades", []))
|
||||||
|
wr = result.get("win_rate", 0)
|
||||||
|
candidate = ParamResult(params=combo, sharpe=sh, return_pct=ret, trades=tr, win_rate=wr)
|
||||||
|
if candidate.score > best.score:
|
||||||
|
best = candidate
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
def _test_oos(self, oos_start_ms: int, oos_end_ms: int, params: dict) -> ParamResult:
|
||||||
|
"""Test params on out-of-sample data."""
|
||||||
|
from backtests.vbt_runner import VBTBacktestRunner
|
||||||
|
|
||||||
|
try:
|
||||||
|
runner = VBTBacktestRunner(
|
||||||
|
vip_tier=self._fee_tier, staking_tier=self._staking_tier
|
||||||
|
)
|
||||||
|
result = runner.run_strategy(
|
||||||
|
strategy=self._strategy, interval=self._interval,
|
||||||
|
limit=720, start_ms=oos_start_ms, end_ms=oos_end_ms,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return ParamResult(
|
||||||
|
params=params,
|
||||||
|
sharpe=result.get("sharpe", 0),
|
||||||
|
return_pct=result.get("total_return_pct", 0),
|
||||||
|
trades=len(result.get("trades", [])),
|
||||||
|
win_rate=result.get("win_rate", 0),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ParamResult(params=params, sharpe=0, return_pct=0, trades=0)
|
||||||
|
|
||||||
|
def run(self) -> OptimizerReport:
|
||||||
|
"""Execute full walk-forward parameter optimization."""
|
||||||
|
from framework.data import HyperliquidDataProvider
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
provider = HyperliquidDataProvider()
|
||||||
|
df = provider.fetch_candles(self._coin, interval=self._interval, limit=5000)
|
||||||
|
|
||||||
|
if df.empty or len(df) < 100:
|
||||||
|
return OptimizerReport(strategy=self._strategy, interval=self._interval,
|
||||||
|
n_windows=self._n_windows, total_trials=0)
|
||||||
|
|
||||||
|
total_bars = len(df)
|
||||||
|
window_size = total_bars // (self._n_windows + 1)
|
||||||
|
if window_size < 50:
|
||||||
|
return OptimizerReport(strategy=self._strategy, interval=self._interval,
|
||||||
|
n_windows=self._n_windows, total_trials=0)
|
||||||
|
|
||||||
|
total_trials = 0
|
||||||
|
report = OptimizerReport(
|
||||||
|
strategy=self._strategy, interval=self._interval,
|
||||||
|
n_windows=self._n_windows, total_trials=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
n_combos = len(self._generate_combos())
|
||||||
|
|
||||||
|
for w in range(self._n_windows):
|
||||||
|
is_start_idx = w * window_size
|
||||||
|
is_end_idx = is_start_idx + window_size
|
||||||
|
oos_start_idx = is_end_idx
|
||||||
|
oos_end_idx = min(oos_start_idx + window_size, total_bars)
|
||||||
|
|
||||||
|
is_start_ms = int(df.index[is_start_idx].timestamp() * 1000)
|
||||||
|
is_end_ms = int(df.index[min(is_end_idx - 1, total_bars - 1)].timestamp() * 1000)
|
||||||
|
oos_start_ms = int(df.index[min(oos_start_idx, total_bars - 1)].timestamp() * 1000)
|
||||||
|
oos_end_ms = int(df.index[min(oos_end_idx - 1, total_bars - 1)].timestamp() * 1000)
|
||||||
|
|
||||||
|
is_start_ts = str(df.index[is_start_idx])[:10]
|
||||||
|
is_end_ts = str(df.index[min(is_end_idx - 1, total_bars - 1)])[:10]
|
||||||
|
oos_start_ts = str(df.index[min(oos_start_idx, total_bars - 1)])[:10]
|
||||||
|
oos_end_ts = str(df.index[min(oos_end_idx - 1, total_bars - 1)])[:10]
|
||||||
|
|
||||||
|
# IS optimization
|
||||||
|
best_is = self._optimize_is(is_start_ms, is_end_ms)
|
||||||
|
total_trials += 1 # approximate
|
||||||
|
|
||||||
|
# OOS test
|
||||||
|
oos_result = self._test_oos(oos_start_ms, oos_end_ms, best_is.params)
|
||||||
|
|
||||||
|
report.windows.append(WFParamWindow(
|
||||||
|
window_idx=w,
|
||||||
|
is_start=is_start_ts, is_end=is_end_ts,
|
||||||
|
oos_start=oos_start_ts, oos_end=oos_end_ts,
|
||||||
|
best_params=best_is.params,
|
||||||
|
is_sharpe=best_is.sharpe,
|
||||||
|
oos_sharpe=oos_result.sharpe,
|
||||||
|
is_return_pct=best_is.return_pct,
|
||||||
|
oos_return_pct=oos_result.return_pct,
|
||||||
|
oos_trades=oos_result.trades,
|
||||||
|
is_trials=min(n_combos, 1),
|
||||||
|
))
|
||||||
|
|
||||||
|
report.total_trials = total_trials
|
||||||
|
report.elapsed_seconds = time.time() - start_time
|
||||||
|
return report
|
||||||
Reference in New Issue
Block a user