Compare commits

..

2 Commits

Author SHA1 Message Date
zwitschi 0d1f6961d6 style: Clean up code formatting for improved readability
CI / lint-test-build (push) Failing after 14s
2026-06-01 15:18:01 +02:00
zwitschi d742577484 feat: Add dashboard controls for tradable pairs and strategy configuration 2026-06-01 15:13:05 +02:00
10 changed files with 146 additions and 34 deletions
+2
View File
@@ -18,6 +18,7 @@
- Added synthetic latency profiler scenarios and CLI scripts for baseline generation and regression checks.
- Added latency baseline/threshold artifacts and CI latency guardrail enforcement.
- Added deterministic replay backtesting engine, CLI script, and unit coverage for JSONL event replay.
- Added dashboard controls for tradable pair universe selection and strategy mode/parameter configuration.
### Changed
@@ -27,6 +28,7 @@
- Added explicit Kraken API key permission configuration (`KRAKEN_API_KEY_PERMISSIONS`) and docs for least-privilege key usage.
- Optimized dashboard metrics aggregation to use DuckDB SQL aggregates/quantiles instead of Python row scans.
- Added backtesting usage and replay format documentation to README.
- Dashboard controls now surface tradable pairs and strategy config snapshot values.
### Removed
+4 -8
View File
@@ -30,8 +30,7 @@ def _parse_balances(raw: str) -> Mapping[str, float]:
def main() -> int:
parser = argparse.ArgumentParser(
description="Run a deterministic replay backtest.")
parser = argparse.ArgumentParser(description="Run a deterministic replay backtest.")
parser.add_argument("--events", type=Path, required=True)
parser.add_argument("--starting-balances", type=str, default="USD=1000.0")
parser.add_argument("--trade-capital", type=float, default=100.0)
@@ -56,18 +55,15 @@ def main() -> int:
started_at=events[0].occurred_at if events else datetime.now(UTC),
)
report = asyncio.run(
engine.run(events, starting_balances=_parse_balances(
args.starting_balances))
engine.run(events, starting_balances=_parse_balances(args.starting_balances))
)
print("Backtest report:")
print(f"- processed_events: {report.processed_events}")
print(f"- opportunities_seen: {report.opportunities_seen}")
print(f"- trades_executed: {report.trades_executed}")
print(
f"- win_rate: {report.win_rate if report.win_rate is not None else 'n/a'}")
print(
f"- fill_rate: {report.fill_rate if report.fill_rate is not None else 'n/a'}")
print(f"- win_rate: {report.win_rate if report.win_rate is not None else 'n/a'}")
print(f"- fill_rate: {report.fill_rate if report.fill_rate is not None else 'n/a'}")
print(f"- realized_pnl_usd: {report.realized_pnl_usd:.4f}")
print(f"- max_drawdown_usd: {report.max_drawdown_usd:.4f}")
print(f"- miss_reasons: {dict(report.miss_reasons)}")
+3 -6
View File
@@ -63,16 +63,13 @@ def scan_worktree() -> list[str]:
for rule_name, pattern in PATTERNS:
if pattern.search(content):
findings.append(
f"worktree:{path.relative_to(WORKSPACE)}:{rule_name}")
findings.append(f"worktree:{path.relative_to(WORKSPACE)}:{rule_name}")
return findings
def scan_git_history() -> list[str]:
cmd = ["git", "-C", str(WORKSPACE), "log", "--all",
"-p", "--pretty=format:%H"]
completed = subprocess.run(
cmd, check=False, capture_output=True, text=True)
cmd = ["git", "-C", str(WORKSPACE), "log", "--all", "-p", "--pretty=format:%H"]
completed = subprocess.run(cmd, check=False, capture_output=True, text=True)
if completed.returncode != 0:
return ["history_scan_failed"]
+4
View File
@@ -10,6 +10,10 @@ from arbitrade.risk.kill_switch import KillSwitch
class DashboardControlState:
is_running: bool = True
kill_switch: KillSwitch = field(default_factory=KillSwitch)
tradable_pairs: list[str] = field(default_factory=list)
strategy_mode: str = "incremental"
strategy_profit_threshold: float = 0.0005
strategy_max_depth_levels: int = 10
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
def mark_updated(self) -> None:
+35
View File
@@ -271,6 +271,9 @@ def _dashboard_controls(request: Request) -> dict[str, object]:
dedup_seconds_raw = alert_status.get("dedup_seconds", 0.0)
dedup_seconds = float(dedup_seconds_raw) if isinstance(dedup_seconds_raw, int | float) else 0.0
tradable_pairs_display = (
", ".join(controls.tradable_pairs) if controls.tradable_pairs else "All"
)
return {
"execution_status": "running" if controls.is_running else "stopped",
@@ -307,6 +310,11 @@ def _dashboard_controls(request: Request) -> dict[str, object]:
"alerts_last_channel_results": [
str(item) for item in cast(list[object], alert_status.get("last_channel_results", []))
],
"tradable_pairs_display": tradable_pairs_display,
"tradable_pairs_value": ", ".join(controls.tradable_pairs),
"strategy_mode": controls.strategy_mode,
"strategy_profit_threshold": f"{controls.strategy_profit_threshold:.6f}",
"strategy_max_depth_levels": str(controls.strategy_max_depth_levels),
"updated_at": controls.updated_at.isoformat(),
"start_endpoint": "/dashboard/control/start",
"stop_endpoint": "/dashboard/control/stop",
@@ -327,6 +335,18 @@ def _form_bool(value: str | None) -> bool:
return value.lower() in {"1", "true", "yes", "on"}
def _parse_comma_separated_list(value: str | None) -> list[str]:
if value is None:
return []
items: list[str] = []
for raw_item in value.split(","):
item = raw_item.strip().upper()
if item and item not in items:
items.append(item)
return items
async def _dashboard_response(
request: Request, template_name: str = "dashboard.html"
) -> HTMLResponse:
@@ -514,6 +534,17 @@ async def dashboard_control_config(request: Request) -> HTMLResponse:
max_concurrent_value = form["max_concurrent_trades"].strip()
settings.max_concurrent_trades = int(max_concurrent_value) if max_concurrent_value else None
controls.tradable_pairs = _parse_comma_separated_list(form.get("tradable_pairs"))
if "strategy_mode" in form and form["strategy_mode"].strip():
strategy_mode = form["strategy_mode"].strip().lower()
if strategy_mode not in {"incremental", "paper", "live"}:
raise ValueError("strategy_mode must be one of: incremental, paper, live")
controls.strategy_mode = strategy_mode
if "strategy_profit_threshold" in form and form["strategy_profit_threshold"].strip():
controls.strategy_profit_threshold = float(form["strategy_profit_threshold"])
if "strategy_max_depth_levels" in form and form["strategy_max_depth_levels"].strip():
controls.strategy_max_depth_levels = int(form["strategy_max_depth_levels"])
settings.paper_trading_mode = _form_bool(form.get("paper_trading_mode"))
controls.mark_updated()
@@ -549,6 +580,10 @@ async def dashboard_control_config(request: Request) -> HTMLResponse:
"max_trade_capital_usd": settings.max_trade_capital_usd,
"max_concurrent_trades": settings.max_concurrent_trades,
"paper_trading_mode": settings.paper_trading_mode,
"tradable_pairs": controls.tradable_pairs,
"strategy_mode": controls.strategy_mode,
"strategy_profit_threshold": controls.strategy_profit_threshold,
"strategy_max_depth_levels": controls.strategy_max_depth_levels,
},
)
+6 -12
View File
@@ -153,8 +153,7 @@ def _parse_book_levels(raw_levels: Any) -> tuple[BookLevel, ...]:
or not isinstance(raw_level[1], int | float)
):
raise ValueError("Each level must be [price, volume]")
levels.append(BookLevel(price=float(
raw_level[0]), volume=float(raw_level[1])))
levels.append(BookLevel(price=float(raw_level[0]), volume=float(raw_level[1])))
return tuple(levels)
@@ -173,8 +172,7 @@ def load_replay_events(path: Path) -> list[ReplayBookEvent]:
if not isinstance(timestamp_raw, str) or not isinstance(symbol_raw, str):
raise ValueError("Each event must include timestamp and symbol")
occurred_at = datetime.fromisoformat(
timestamp_raw.replace("Z", "+00:00")).astimezone(UTC)
occurred_at = datetime.fromisoformat(timestamp_raw.replace("Z", "+00:00")).astimezone(UTC)
events.append(
ReplayBookEvent(
occurred_at=occurred_at,
@@ -208,8 +206,7 @@ class BacktestReplayEngine:
min_order_size_by_pair=config.min_order_size_by_pair,
)
self._pre_trade = PreTradeValidator()
self._trade_limits = TradeLimitsGuard(
max_concurrent_trades=config.max_concurrent_trades)
self._trade_limits = TradeLimitsGuard(max_concurrent_trades=config.max_concurrent_trades)
self._simulated_rest = _SimulatedRestClient(
self._clock,
slippage_bps=config.slippage_bps,
@@ -244,8 +241,7 @@ class BacktestReplayEngine:
trades_executed = 0
realized_pnl = 0.0
equity = float(starting_balances.get(
self._config.quote_asset.upper(), 0.0))
equity = float(starting_balances.get(self._config.quote_asset.upper(), 0.0))
peak_equity = equity
max_drawdown = 0.0
@@ -288,8 +284,7 @@ class BacktestReplayEngine:
result = await self._sequencer.execute(opportunity)
self._trade_limits.close_trade(exposure)
execution_latencies.append(
self._simulated_rest.last_trade_latency_ms)
execution_latencies.append(self._simulated_rest.last_trade_latency_ms)
fill_samples.append(self._simulated_rest.last_fill_ratio)
if not result.success:
@@ -312,8 +307,7 @@ class BacktestReplayEngine:
wins = sum(1 for pnl in realized_samples if pnl > 0.0)
win_rate = (wins / len(realized_samples)) if realized_samples else None
fill_rate = (sum(fill_samples) / len(fill_samples)
) if fill_samples else None
fill_rate = (sum(fill_samples) / len(fill_samples)) if fill_samples else None
return BacktestReport(
started_at=events[0].occurred_at if events else self._clock.now,
+2 -4
View File
@@ -140,8 +140,7 @@ async def restore_runtime_state(app: FastAPI) -> RuntimeRecoveryReport:
snapshot_at = latest.snapshot_at.isoformat()
controls.is_running = latest.is_running
if latest.kill_switch_active:
controls.kill_switch.activate(
reason=latest.kill_switch_reason or "recovered")
controls.kill_switch.activate(reason=latest.kill_switch_reason or "recovered")
else:
controls.kill_switch.deactivate()
controls.mark_updated()
@@ -151,8 +150,7 @@ async def restore_runtime_state(app: FastAPI) -> RuntimeRecoveryReport:
if open_trades > 0:
controls.is_running = False
if not controls.kill_switch.is_active:
controls.kill_switch.activate(
reason="recovery_open_trades_detected")
controls.kill_switch.activate(reason="recovery_open_trades_detected")
controls.mark_updated()
restart_guard_active = True
+14
View File
@@ -202,6 +202,8 @@ async def test_dashboard_page_and_fragment_and_sse(tmp_path) -> None:
assert "Last result" in controls.text
assert "Paper trading mode" in controls.text
assert "Trade capital USD" in controls.text
assert "Tradable pairs" in controls.text
assert "Strategy mode" in controls.text
assert charts.status_code == 200
assert "Opportunity Trend" in charts.text
@@ -229,6 +231,10 @@ async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> N
"trade_capital_usd": "250.50",
"max_trade_capital_usd": "300.00",
"max_concurrent_trades": "4",
"tradable_pairs": "BTC/USD, ETH/BTC, BTC/USD",
"strategy_mode": "paper",
"strategy_profit_threshold": "0.0025",
"strategy_max_depth_levels": "7",
"paper_trading_mode": "on",
},
)
@@ -247,10 +253,18 @@ async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> N
assert "250.50 USD" in config_response.text
assert "300.00 USD" in config_response.text
assert "4" in config_response.text
assert "BTC/USD, ETH/BTC" in config_response.text
assert "paper" in config_response.text
assert "0.002500" in config_response.text
assert "7" in config_response.text
assert app.state.settings.trade_capital_usd == 250.5
assert app.state.settings.max_trade_capital_usd == 300.0
assert app.state.settings.max_concurrent_trades == 4
assert app.state.settings.paper_trading_mode is True
assert app.state.dashboard_controls.tradable_pairs == ["BTC/USD", "ETH/BTC"]
assert app.state.dashboard_controls.strategy_mode == "paper"
assert app.state.dashboard_controls.strategy_profit_threshold == 0.0025
assert app.state.dashboard_controls.strategy_max_depth_levels == 7
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
+2 -4
View File
@@ -67,13 +67,11 @@ def test_backtest_replay_engine_runs_deterministically() -> None:
engine = BacktestReplayEngine(
cycles_by_pair=cycles_by_pair,
available_pairs=available_pairs,
config=BacktestConfig(trade_capital=100.0,
slippage_bps=5.0, execution_latency_ms=10.0),
config=BacktestConfig(trade_capital=100.0, slippage_bps=5.0, execution_latency_ms=10.0),
started_at=started_at,
)
report = asyncio.run(engine.run(
replay_events, starting_balances={"USD": 1000.0}))
report = asyncio.run(engine.run(replay_events, starting_balances={"USD": 1000.0}))
assert report.processed_events == 3
assert report.opportunities_seen >= 0
+74
View File
@@ -16,6 +16,10 @@
<div class="meta">Trade capital: {{ trade_capital_usd }}</div>
<div class="meta">Max trade capital: {{ max_trade_capital_usd }}</div>
<div class="meta">Max concurrent trades: {{ max_concurrent_trades }}</div>
<div class="meta">Tradable pairs: {{ tradable_pairs_display }}</div>
<div class="meta">Strategy mode: {{ strategy_mode }}</div>
<div class="meta">Profit threshold: {{ strategy_profit_threshold }}</div>
<div class="meta">Max depth levels: {{ strategy_max_depth_levels }}</div>
</article>
<article class="card">
<div class="label">Alerting</div>
@@ -109,6 +113,76 @@
value="{{ max_concurrent_trades_value }}"
/>
</label>
<label class="field">
<span>Tradable pairs</span>
<input
name="tradable_pairs"
type="text"
placeholder="BTC/USD, ETH/BTC"
value="{{ tradable_pairs_value }}"
/>
</label>
<label class="field">
<span>Strategy mode</span>
<select name="strategy_mode">
<option
value="incremental"
{%
if
strategy_mode=""
="incremental"
%}selected{%
endif
%}
>
incremental
</option>
<option
value="paper"
{%
if
strategy_mode=""
="paper"
%}selected{%
endif
%}
>
paper
</option>
<option
value="live"
{%
if
strategy_mode=""
="live"
%}selected{%
endif
%}
>
live
</option>
</select>
</label>
<label class="field">
<span>Strategy profit threshold</span>
<input
name="strategy_profit_threshold"
type="number"
min="0"
step="0.0001"
value="{{ strategy_profit_threshold }}"
/>
</label>
<label class="field">
<span>Max depth levels</span>
<input
name="strategy_max_depth_levels"
type="number"
min="1"
step="1"
value="{{ strategy_max_depth_levels }}"
/>
</label>
<label class="field checkbox">
{% set check = "checked" if paper_trading_mode == "enabled" else "" %}
<input name="paper_trading_mode" type="checkbox" {{ check }} />