c8e3daeb57
CI / lint-test-build (push) Failing after 12s
- Consolidated multiline string formatting into single-line for SQL queries in multiple files. - Adjusted argument formatting in function calls for better alignment and readability. - Removed unnecessary line breaks and improved spacing in various sections of the codebase. - Updated test cases to maintain consistency in formatting and improve clarity.
147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections.abc import Mapping, Sequence
|
|
from pathlib import Path
|
|
|
|
from arbitrade.backtesting import load_replay_events
|
|
from arbitrade.backtesting.sweep import (
|
|
PromotionCriteria,
|
|
SweepResult,
|
|
build_parameter_grid,
|
|
persist_sweep_results,
|
|
run_parameter_search,
|
|
)
|
|
from arbitrade.detection.graph import CurrencyGraph, TriangularCycle
|
|
|
|
|
|
def _parse_balances(raw: str) -> Mapping[str, float]:
|
|
balances: dict[str, float] = {}
|
|
for entry in raw.split(","):
|
|
stripped = entry.strip()
|
|
if not stripped:
|
|
continue
|
|
asset, value = stripped.split("=", 1)
|
|
balances[asset.strip().upper()] = float(value)
|
|
return balances
|
|
|
|
|
|
def _parse_float_list(raw: str) -> list[float]:
|
|
values = [item.strip() for item in raw.split(",") if item.strip()]
|
|
if not values:
|
|
raise ValueError("expected at least one numeric value")
|
|
return [float(value) for value in values]
|
|
|
|
|
|
def _parse_pair_universes(raw: str) -> list[tuple[str, ...]]:
|
|
universes: list[tuple[str, ...]] = []
|
|
for chunk in raw.split(";"):
|
|
symbols = tuple(item.strip().upper() for item in chunk.split("|") if item.strip())
|
|
if symbols:
|
|
universes.append(symbols)
|
|
if not universes:
|
|
raise ValueError("at least one pair universe must be provided")
|
|
return universes
|
|
|
|
|
|
def _build_graph_from_symbols(symbols: Sequence[str]) -> dict[str, list[TriangularCycle]]:
|
|
graph = CurrencyGraph()
|
|
for symbol in symbols:
|
|
normalized = symbol.upper()
|
|
if "/" not in normalized:
|
|
continue
|
|
base, quote = normalized.split("/", 1)
|
|
graph.add_pair(base, quote, normalized)
|
|
|
|
cycles = graph.triangular_cycles()
|
|
return graph.index_cycles_by_pair(cycles)
|
|
|
|
|
|
def _print_top_results(results: Sequence[SweepResult], *, limit: int = 5) -> None:
|
|
print(f"Top {min(limit, len(results))} result(s) by out-of-sample score:")
|
|
for index, result in enumerate(results[:limit], start=1):
|
|
print(
|
|
"- "
|
|
f"#{index} "
|
|
f"theta={result.parameters.min_profit_threshold:.6f}, "
|
|
f"capital={result.parameters.trade_capital:.2f}, "
|
|
f"pairs={','.join(result.parameters.pair_universe)}, "
|
|
f"staleness={result.parameters.staleness_threshold_seconds:.2f}s, "
|
|
f"test_score={result.test_score:.4f}, "
|
|
f"promotion_ready={result.promotion_ready}"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run backtesting parameter sweep with train/test split."
|
|
)
|
|
parser.add_argument("--events", type=Path, required=True)
|
|
parser.add_argument("--starting-balances", type=str, default="USD=1000.0")
|
|
parser.add_argument("--theta-values", type=str, default="0.0003,0.0005,0.0008")
|
|
parser.add_argument("--trade-capital-values", type=str, default="50,100,150")
|
|
parser.add_argument(
|
|
"--pair-universes",
|
|
type=str,
|
|
default="BTC/USD|ETH/BTC|ETH/USD",
|
|
help="Semicolon-separated universes, each with | delimited pairs",
|
|
)
|
|
parser.add_argument("--staleness-threshold-values", type=str, default="3,5,8")
|
|
parser.add_argument("--train-ratio", type=float, default=0.7)
|
|
parser.add_argument(
|
|
"--output", type=Path, default=Path("ops/backtesting/parameter_sweep_results.json")
|
|
)
|
|
|
|
parser.add_argument("--min-test-realized-pnl-usd", type=float, default=0.0)
|
|
parser.add_argument("--min-test-win-rate", type=float, default=0.5)
|
|
parser.add_argument("--min-test-fill-rate", type=float, default=0.9)
|
|
parser.add_argument("--max-test-drawdown-usd", type=float, default=25.0)
|
|
parser.add_argument("--max-generalization-gap-ratio", type=float, default=0.5)
|
|
|
|
args = parser.parse_args()
|
|
|
|
events = load_replay_events(args.events)
|
|
symbols = sorted({event.symbol.upper() for event in events})
|
|
cycles_by_pair = _build_graph_from_symbols(symbols)
|
|
if not cycles_by_pair:
|
|
raise SystemExit("No triangular cycles found in supplied replay events")
|
|
|
|
grid = build_parameter_grid(
|
|
theta_values=_parse_float_list(args.theta_values),
|
|
trade_capital_values=_parse_float_list(args.trade_capital_values),
|
|
pair_universes=_parse_pair_universes(args.pair_universes),
|
|
staleness_threshold_values=_parse_float_list(args.staleness_threshold_values),
|
|
)
|
|
|
|
artifacts = run_parameter_search(
|
|
events=events,
|
|
cycles_by_pair=cycles_by_pair,
|
|
parameter_grid=grid,
|
|
starting_balances=_parse_balances(args.starting_balances),
|
|
train_ratio=args.train_ratio,
|
|
promotion_criteria=PromotionCriteria(
|
|
min_test_realized_pnl_usd=args.min_test_realized_pnl_usd,
|
|
min_test_win_rate=args.min_test_win_rate,
|
|
min_test_fill_rate=args.min_test_fill_rate,
|
|
max_test_drawdown_usd=args.max_test_drawdown_usd,
|
|
max_generalization_gap_ratio=args.max_generalization_gap_ratio,
|
|
),
|
|
)
|
|
|
|
persist_sweep_results(args.output, artifacts)
|
|
|
|
print(f"Completed sweep combinations: {len(artifacts.results)}")
|
|
print(f"Promotion-ready combinations: {len(artifacts.promoted)}")
|
|
print(f"Results written: {args.output}")
|
|
|
|
_print_top_results(artifacts.results)
|
|
if artifacts.promoted:
|
|
print("Promotion candidates (paper-trading canary):")
|
|
_print_top_results(artifacts.promoted)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|