Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ historical reproduction.
- Walk-forward and train/test optimization designed to avoid leaking OOS data
into parameter selection, plus full-sample robust calibration for final
production parameter discovery.
- Domain-agnostic Optuna optimization adapters for prepared signal, intrabar,
portfolio, and generic endpoint workflows.

## Performance Philosophy

Expand Down Expand Up @@ -141,6 +143,23 @@ about 23.3x faster than the readable Python oracle on the committed benchmark
while preserving the oracle semantics through targeted parity tests and audit
second-pass checks.

Latest Phase 32C optimization overhead benchmark:

| Measurement | Result |
|---|---:|
| Optimizer overhead | 0.0174s for 24 trials |
| Optimizer overhead / trial | 0.000723s |
| Prepared signal evaluator | 2.03x faster than normal endpoint replay |
| Intrabar first vs warm run | 3.70x first/warm ratio |
| Parity | pass, final equity diff 0.0 |

Phase 32C consolidates safe walk-forward optimization primitives with the new
domain-agnostic optimizer core while keeping WFO fold isolation and robust
selection semantics inside `walkforward.py`. Read
[`docs/optimization.md`](docs/optimization.md) and
`benchmarks/results/optimization_overhead.md` for signal, intrabar, portfolio,
arbitrage/grid/options fallback examples and benchmark details.

Ecosystem positioning:

| Tool | Core strength | Runtime model | QuantBT role beside it |
Expand Down Expand Up @@ -241,6 +260,9 @@ service creates a run.
- Full-sample robust calibration selectors: `full_robust`,
`full_plateau_robust`, `full_temporal_robust`, and `full_best`.
- Optional trade-count penalty to avoid overfit low-trade Sharpe traps.
- Shared domain-agnostic optimizer primitives for search-space parsing,
duplicate detection, early stopping, objective helpers, constraints, and
candidate selection.

### Nautilus Validation Reports

Expand Down
64 changes: 64 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,51 @@
validate_walkforward_strategy_output,
walkforward_support_matrix,
)
from .optimization import (
CONSTRAINTS_USER_ATTR,
ArbitrageGenericEvaluator,
ArbitrageTrialOutput,
CandidateSelector,
GenericEndpointEvaluator,
GridDCAGenericEvaluator,
GridDCATrialOutput,
JsonlOptimizationLogger,
MissingOptimizationMetricError,
ObjectiveResult,
OptionPackageGenericEvaluator,
OptionTrialOutput,
OptimizationConfig,
OptimizationResult,
OptimizationTrialRecord,
OptunaOptimizer,
PreparedIntrabarEvaluator,
PreparedPortfolioEvaluator,
PreparedSignalEvaluator,
ReportMetricObjective,
SamplerConfig,
SearchSpaceInfo,
SelectedCandidate,
SharpeObjective,
SingleObjectiveEarlyStopping,
TrialEvaluator,
build_grid_search_space,
build_sampler,
constraints_feasible,
constraints_from_trial,
max_drawdown_constraint,
max_margin_utilization_constraint,
max_rejection_rate_constraint,
max_turnover_constraint,
metric_from_result,
metrics_from_result,
min_trades_constraint,
result_full_report,
search_space_info,
set_trial_constraints,
stable_params_key,
suggest_parameter,
suggest_params,
)
from .engines import BacktestEngineV2, EventDrivenBacktestEngine, OptionBacktestEngine, PortfolioBacktestEngine
from .backends import (
NativeEventBackend,
Expand Down Expand Up @@ -537,6 +582,25 @@
"WalkForwardCompatibilityEntry",
"EarlyStoppingCallback",
"DuplicatePruner",
"CONSTRAINTS_USER_ATTR",
"JsonlOptimizationLogger",
"ObjectiveResult",
"OptimizationConfig",
"OptimizationResult",
"OptimizationTrialRecord",
"OptunaOptimizer",
"SamplerConfig",
"SearchSpaceInfo",
"SingleObjectiveEarlyStopping",
"TrialEvaluator",
"build_grid_search_space",
"build_sampler",
"constraints_from_trial",
"search_space_info",
"set_trial_constraints",
"stable_params_key",
"suggest_parameter",
"suggest_params",
"benchmark_walkforward_kernels",
"logging_callback",
"score_strategy_output",
Expand Down
16 changes: 16 additions & 0 deletions benchmarks/results/optimization_overhead.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"intrabar_compile_to_warm_ratio": 3.6954904749510424,
"intrabar_final_equity_diff": 0.0,
"intrabar_first_run_seconds": 0.01777169480919838,
"intrabar_warm_run_seconds": 0.004809021949768066,
"loops": 24,
"normal_signal_replay_seconds": 0.1651457599364221,
"optimizer_overhead_per_trial_seconds": 0.0007232134230434895,
"optimizer_overhead_seconds": 0.017357122153043747,
"prepared_signal_replay_seconds": 0.08149230107665062,
"prepared_signal_speedup": 2.026519778611824,
"rows": 360,
"signal_final_equity_diff": 0.0,
"status": "pass",
"trials": 24
}
21 changes: 21 additions & 0 deletions benchmarks/results/optimization_overhead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Phase 32C Optimization Overhead Benchmark

Status: **pass**

| Measurement | Value |
|---|---:|
| Optimizer overhead | `0.017357s` |
| Optimizer overhead / trial | `0.000723s` |
| Normal signal replays | `0.165146s` |
| Prepared signal replays | `0.081492s` |
| Prepared signal speedup | `2.027x` |
| Intrabar first run | `0.017772s` |
| Intrabar warm run | `0.004809s` |
| Intrabar first/warm ratio | `3.695x` |

Parity checks:

- Signal final equity diff: `0.0`
- Intrabar final equity diff: `0.0`

This benchmark measures facade/optimizer overhead, not strategy quality.
195 changes: 195 additions & 0 deletions benchmarks/run_optimization_overhead.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""Phase 32C optimization overhead and prepared-evaluator benchmark."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
import sys
import time

import numpy as np
import pandas as pd

PACKAGE_DIR = Path(__file__).resolve().parents[1]
PROJECT_DIR = PACKAGE_DIR.parent
if str(PROJECT_DIR) not in sys.path:
sys.path.insert(0, str(PROJECT_DIR))

from quantbt import ( # noqa: E402
GenericEndpointEvaluator,
IntrabarIntentTape,
ObjectiveResult,
OptimizationConfig,
OptunaOptimizer,
PreparedSignalEvaluator,
QuantBTEndpoint,
SamplerConfig,
)


def run_benchmark(rows: int = 360, trials: int = 24, loops: int = 24) -> dict:
df = _frame(rows)
optimizer_seconds = _optimizer_overhead(trials)
normal_seconds, prepared_seconds, signal_diff = _signal_replay_benchmark(df, loops)
first_intrabar, warm_intrabar, intrabar_diff = _intrabar_compile_benchmark(df)
status = "pass" if signal_diff <= 1e-9 and intrabar_diff <= 1e-9 else "fail"
return {
"status": status,
"rows": int(rows),
"trials": int(trials),
"loops": int(loops),
"optimizer_overhead_seconds": float(optimizer_seconds),
"optimizer_overhead_per_trial_seconds": float(optimizer_seconds / max(1, trials)),
"normal_signal_replay_seconds": float(normal_seconds),
"prepared_signal_replay_seconds": float(prepared_seconds),
"prepared_signal_speedup": float(normal_seconds / prepared_seconds) if prepared_seconds > 0 else 0.0,
"signal_final_equity_diff": float(signal_diff),
"intrabar_first_run_seconds": float(first_intrabar),
"intrabar_warm_run_seconds": float(warm_intrabar),
"intrabar_compile_to_warm_ratio": float(first_intrabar / warm_intrabar) if warm_intrabar > 0 else 0.0,
"intrabar_final_equity_diff": float(intrabar_diff),
}


def make_markdown(report: dict) -> str:
return "\n".join(
[
"# Phase 32C Optimization Overhead Benchmark",
"",
f"Status: **{report['status']}**",
"",
"| Measurement | Value |",
"|---|---:|",
f"| Optimizer overhead | `{report['optimizer_overhead_seconds']:.6f}s` |",
f"| Optimizer overhead / trial | `{report['optimizer_overhead_per_trial_seconds']:.6f}s` |",
f"| Normal signal replays | `{report['normal_signal_replay_seconds']:.6f}s` |",
f"| Prepared signal replays | `{report['prepared_signal_replay_seconds']:.6f}s` |",
f"| Prepared signal speedup | `{report['prepared_signal_speedup']:.3f}x` |",
f"| Intrabar first run | `{report['intrabar_first_run_seconds']:.6f}s` |",
f"| Intrabar warm run | `{report['intrabar_warm_run_seconds']:.6f}s` |",
f"| Intrabar first/warm ratio | `{report['intrabar_compile_to_warm_ratio']:.3f}x` |",
"",
"Parity checks:",
"",
f"- Signal final equity diff: `{report['signal_final_equity_diff']}`",
f"- Intrabar final equity diff: `{report['intrabar_final_equity_diff']}`",
"",
"This benchmark measures facade/optimizer overhead, not strategy quality.",
]
) + "\n"


def _optimizer_overhead(trials: int) -> float:
evaluator = GenericEndpointEvaluator(
build_run_inputs=lambda params: {"value": float(params["x"])},
run_func=lambda value: value,
objective_builder=lambda result, params: ObjectiveResult.scalar(float(result), metrics={"score": float(result)}),
)
optimizer = OptunaOptimizer(
evaluator=evaluator,
config=OptimizationConfig(
study_name=f"phase32c_overhead_{time.time_ns()}",
n_trials=int(trials),
seed=42,
show_progress_bar=False,
duplicate_policy="allow",
),
sampler_config=SamplerConfig(name="random"),
)
start = time.perf_counter()
optimizer.optimize(param_ranges={"x": (0.0, 1.0)})
return time.perf_counter() - start


def _signal_replay_benchmark(df: pd.DataFrame, loops: int):
endpoint = QuantBTEndpoint.signal_notional(
backend="native_vectorized",
initial_capital=20_000.0,
leverage=5.0,
alloc_per_trade=1_000.0,
fee_rate=0.0,
use_funding=False,
)
signal = pd.Series(np.where(df["close"].diff().fillna(0.0) > 0.0, 1.0, 0.0), index=df.index)
normal = endpoint.backtest(data=df, signal=signal, symbols=["BTC"])
prepared = endpoint.prepare_service_context(data=df, symbols=["BTC"])
prepared_result = prepared.backtest(signal=signal)
diff = abs(float(normal.equity.iloc[-1]) - float(prepared_result.equity.iloc[-1]))

start = time.perf_counter()
for _ in range(int(loops)):
endpoint.backtest(data=df, signal=signal, symbols=["BTC"])
normal_seconds = time.perf_counter() - start

evaluator = PreparedSignalEvaluator(
prepared_context=prepared,
strategy_func=lambda params: signal,
objective_builder=lambda result, params: ObjectiveResult.scalar(float(result.equity.iloc[-1])),
)
start = time.perf_counter()
for _ in range(int(loops)):
evaluator.evaluate({})
prepared_seconds = time.perf_counter() - start
return normal_seconds, prepared_seconds, diff


def _intrabar_compile_benchmark(df: pd.DataFrame):
endpoint = QuantBTEndpoint.intrabar_bracket(
initial_capital=20_000.0,
leverage=5.0,
fee_rate=0.0,
slippage_bps=0.0,
use_funding=False,
report_level="minimal",
)
runner = endpoint.prepare_intrabar(data=df, symbols=["BTC"])
entry = np.zeros(len(df))
entry[0] = 1.0
intent = IntrabarIntentTape.from_arrays(entry_side=entry, entry_size=np.abs(entry))

start = time.perf_counter()
first = runner.run(intent, report_level="minimal")
first_seconds = time.perf_counter() - start
start = time.perf_counter()
warm = runner.run(intent, report_level="minimal")
warm_seconds = time.perf_counter() - start
diff = abs(float(first.equity.iloc[-1]) - float(warm.equity.iloc[-1]))
return first_seconds, warm_seconds, diff


def _frame(rows: int) -> pd.DataFrame:
idx = pd.date_range("2024-01-01", periods=int(rows), freq="1h", tz="UTC")
x = np.linspace(0.0, 16.0, len(idx))
close = 100.0 + np.sin(x) * 2.0 + np.arange(len(idx)) * 0.01
return pd.DataFrame(
{
"open": close,
"high": close * 1.01,
"low": close * 0.99,
"close": close,
"volume": 1_000.0,
},
index=idx,
)


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--rows", type=int, default=360)
parser.add_argument("--trials", type=int, default=24)
parser.add_argument("--loops", type=int, default=24)
parser.add_argument("--json", type=Path, default=PACKAGE_DIR / "benchmarks" / "results" / "optimization_overhead.json")
parser.add_argument("--markdown", type=Path, default=PACKAGE_DIR / "benchmarks" / "results" / "optimization_overhead.md")
args = parser.parse_args()
report = run_benchmark(rows=args.rows, trials=args.trials, loops=args.loops)
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
args.markdown.write_text(make_markdown(report))
print(json.dumps(report, indent=2, sort_keys=True))
return 0 if report["status"] == "pass" else 1


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading