feat: add pair fees configuration panel and related functionality

- Introduced a new HTML template for configuring pair fees, allowing users to add, edit, and delete fees for trading pairs.
- Implemented a responsive fee table displaying existing fees with options for editing and deleting.
- Added a form for adding new fees, including fields for base asset, quote asset, market type, and fee rates.
- Removed outdated templates related to backtesting, dashboard, health, and various partials to streamline the codebase.
- Ensured the new fee configuration panel integrates seamlessly with existing endpoints and uses htmx for dynamic updates.
This commit is contained in:
2026-06-03 18:43:36 +02:00
parent ccca9ef62a
commit 87dd655f08
14 changed files with 866 additions and 851 deletions
+37
View File
@@ -0,0 +1,37 @@
{% extends "base.html" %} {% block title %}{{ title }}{% endblock %} {% block
main_class %}shell{% endblock %} {% block content %}
<section class="hero">
<div>
<h1 class="title">Configuration</h1>
<p class="subtitle">
Runtime settings, alerts, exchange, risk, and strategy.
</p>
</div>
<div class="toolbar">
<a class="button secondary" href="/dashboard">Dashboard</a>
<a class="button secondary" href="/dashboard/backtesting">Backtesting</a>
<a class="button secondary" href="/health">Health</a>
</div>
</section>
<section
id="fees-shell"
hx-get="{{ config_fees_fragment }}"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
{% include "partials/config_fees.html" %}
</section>
<section
id="config-shell"
hx-get="/dashboard/fragment/config"
hx-target="this"
hx-trigger="load, every 30s"
hx-swap="outerHTML"
>
{% include "partials/config.html" %}
</section>
{% endblock %}
@@ -18,6 +18,7 @@ head_scripts %}
>Refresh metrics</a
>
<a class="button secondary" href="/health">Health</a>
<a class="button secondary" href="/dashboard/config">Config</a>
<a class="button secondary" href="/dashboard/backtesting">Backtesting</a>
</div>
</section>
@@ -0,0 +1,614 @@
<div id="config-panel" class="panel" style="margin-top: 16px">
<form
class="form-grid"
hx-post="{{ config_endpoint }}"
hx-target="#config-panel"
hx-swap="outerHTML"
style="
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
"
>
<!-- Runtime -->
<div class="card">
<div class="label">Runtime</div>
<label class="field">
<span>App env</span>
<input type="text" value="{{ app_env }}" disabled />
</label>
<label class="field">
<span>App host</span>
<input name="app_host" type="text" value="{{ app_host }}" />
</label>
<label class="field">
<span>App port</span>
<input
name="app_port"
type="number"
min="1"
max="65535"
value="{{ app_port }}"
/>
</label>
<label class="field">
<span>Log level</span>
<select name="log_level">
{% for lvl in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] %} {%
set sel = "selected" if log_level == lvl else "" %}
<option value="{{ lvl }}" {{ sel }}>{{ lvl }}</option>
{% endfor %}
</select>
</label>
<label class="field checkbox">
<input name="log_json" type="checkbox" {{ log_json }} />
<span>JSON logs</span>
</label>
<label class="field checkbox">
<input
name="paper_trading_mode"
type="checkbox"
{{
paper_trading_mode
}}
/>
<span>Paper trading mode</span>
</label>
<label class="field">
<span>Trade capital USD</span>
<input
name="trade_capital_usd"
type="number"
min="0"
step="0.01"
value="{{ trade_capital_usd_value }}"
/>
</label>
<label class="field">
<span>Max trade capital USD</span>
<input
name="max_trade_capital_usd"
type="number"
min="0"
step="0.01"
value="{{ max_trade_capital_usd_value }}"
/>
</label>
<label class="field">
<span>Max concurrent trades</span>
<input
name="max_concurrent_trades"
type="number"
min="1"
step="1"
value="{{ max_concurrent_trades_value }}"
/>
</label>
<label class="field">
<span>Max exposure per asset USD</span>
<input
name="max_exposure_per_asset_usd"
type="number"
min="0"
step="0.01"
value="{{ max_exposure_per_asset_value }}"
/>
</label>
<label class="field">
<span>Quote balance asset</span>
<input
name="quote_balance_asset"
type="text"
value="{{ quote_balance_asset }}"
/>
</label>
<label class="field">
<span>Min order size USD</span>
<input
name="min_order_size_usd"
type="number"
min="0"
step="0.01"
value="{{ min_order_size_usd_value }}"
/>
</label>
<label class="field">
<span>Tradable pairs (comma-separated)</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">
{% set sel = "selected" if strategy_mode == "incremental" else "" %}
<option value="incremental" {{ sel }}>incremental</option>
{% set sel = "selected" if strategy_mode == "paper" else "" %}
<option value="paper" {{ sel }}>paper</option>
{% set sel = "selected" if strategy_mode == "live" else "" %}
<option value="live" {{ sel }}>live</option>
{% if strategy_stat_arb_enabled %} {% set sel = "selected" if
strategy_mode == "stat_arb_experiment" else "" %}
<option value="stat_arb_experiment" {{ sel }}>
stat_arb_experiment
</option>
{% endif %}
</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>
</div>
<!-- Alerts -->
<div class="card">
<div class="label">Alerting</div>
<label class="field checkbox">
<input name="alerts_enabled" type="checkbox" {{ alerts_enabled }} />
<span>Alerts enabled</span>
</label>
<label class="field">
<span>Min severity</span>
<select name="alert_min_severity">
{% for sev in ["info", "warning", "error", "critical"] %} {% set sel =
"selected" if alert_min_severity == sev else "" %}
<option value="{{ sev }}" {{ sel }}>{{ sev }}</option>
{% endfor %}
</select>
</label>
<label class="field">
<span>Dedup seconds</span>
<input
name="alert_dedup_seconds"
type="number"
min="0"
step="1"
value="{{ alert_dedup_seconds }}"
/>
</label>
<label class="field checkbox">
<input
name="alert_on_trade_events"
type="checkbox"
{{
alert_on_trade_events
}}
/>
<span>Trade events</span>
</label>
<label class="field checkbox">
<input
name="alert_on_error_events"
type="checkbox"
{{
alert_on_error_events
}}
/>
<span>Error events</span>
</label>
<label class="field checkbox">
<input
name="alert_on_threshold_events"
type="checkbox"
{{
alert_on_threshold_events
}}
/>
<span>Threshold events</span>
</label>
<label class="field checkbox">
<input
name="alert_on_system_events"
type="checkbox"
{{
alert_on_system_events
}}
/>
<span>System events</span>
</label>
<hr
style="
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin: 12px 0;
"
/>
<label class="field checkbox">
<input
name="telegram_alerts_enabled"
type="checkbox"
{{
telegram_alerts_enabled
}}
/>
<span>Telegram</span>
</label>
<label class="field">
<span>Telegram bot token</span>
<input
name="telegram_bot_token"
type="password"
value="{{ telegram_bot_token }}"
placeholder="Bot token"
/>
</label>
<label class="field">
<span>Telegram chat ID</span>
<input
name="telegram_chat_id"
type="text"
value="{{ telegram_chat_id }}"
placeholder="Chat ID"
/>
</label>
<hr
style="
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin: 12px 0;
"
/>
<label class="field checkbox">
<input
name="discord_alerts_enabled"
type="checkbox"
{{
discord_alerts_enabled
}}
/>
<span>Discord</span>
</label>
<label class="field">
<span>Discord webhook URL</span>
<input
name="discord_webhook_url"
type="password"
value="{{ discord_webhook_url }}"
placeholder="Webhook URL"
/>
</label>
<hr
style="
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin: 12px 0;
"
/>
<label class="field checkbox">
<input
name="email_alerts_enabled"
type="checkbox"
{{
email_alerts_enabled
}}
/>
<span>Email</span>
</label>
<label class="field">
<span>SMTP host</span>
<input
name="email_smtp_host"
type="text"
value="{{ email_smtp_host }}"
placeholder="smtp.example.com"
/>
</label>
<label class="field">
<span>SMTP port</span>
<input
name="email_smtp_port"
type="number"
min="1"
max="65535"
value="{{ email_smtp_port }}"
/>
</label>
<label class="field">
<span>SMTP username</span>
<input
name="email_smtp_username"
type="text"
value="{{ email_smtp_username }}"
/>
</label>
<label class="field">
<span>SMTP password</span>
<input
name="email_smtp_password"
type="password"
value="{{ email_smtp_password }}"
placeholder="Leave blank to keep existing"
/>
</label>
<label class="field">
<span>From address</span>
<input
name="email_alert_from"
type="text"
value="{{ email_alert_from }}"
/>
</label>
<label class="field">
<span>To address</span>
<input name="email_alert_to" type="text" value="{{ email_alert_to }}" />
</label>
<label class="field checkbox">
<input
name="email_smtp_use_tls"
type="checkbox"
{{
email_smtp_use_tls
}}
/>
<span>Use TLS</span>
</label>
</div>
<!-- Kraken -->
<div class="card">
<div class="label">Kraken Exchange</div>
<label class="field">
<span>REST URL</span>
<input
name="kraken_rest_url"
type="text"
value="{{ kraken_rest_url }}"
/>
</label>
<label class="field">
<span>WebSocket URL</span>
<input name="kraken_ws_url" type="text" value="{{ kraken_ws_url }}" />
</label>
<label class="field">
<span>Private rate limit (s)</span>
<input
name="kraken_private_rate_limit_seconds"
type="number"
min="0"
step="0.01"
value="{{ kraken_private_rate_limit_seconds }}"
/>
</label>
<label class="field">
<span>HTTP timeout (s)</span>
<input
name="kraken_http_timeout_seconds"
type="number"
min="1"
step="0.5"
value="{{ kraken_http_timeout_seconds }}"
/>
</label>
<label class="field">
<span>Retry attempts</span>
<input
name="kraken_retry_attempts"
type="number"
min="0"
step="1"
value="{{ kraken_retry_attempts }}"
/>
</label>
<label class="field">
<span>Retry base delay (s)</span>
<input
name="kraken_retry_base_delay_seconds"
type="number"
min="0"
step="0.01"
value="{{ kraken_retry_base_delay_seconds }}"
/>
</label>
<label class="field">
<span>API key</span>
<input
name="kraken_api_key"
type="text"
value="{{ kraken_api_key }}"
placeholder="API key"
/>
</label>
<label class="field">
<span>API secret</span>
<input
name="kraken_api_secret"
type="password"
value="{{ kraken_api_secret }}"
placeholder="API secret"
/>
</label>
<label class="field">
<span>Key permissions</span>
<input
name="kraken_api_key_permissions"
type="text"
value="{{ kraken_api_key_permissions }}"
placeholder="query,trade"
/>
</label>
<label class="field">
<span>Heartbeat timeout (s)</span>
<input
name="ws_heartbeat_timeout_seconds"
type="number"
min="1"
step="1"
value="{{ ws_heartbeat_timeout_seconds }}"
/>
</label>
<label class="field">
<span>Max staleness (s)</span>
<input
name="ws_max_staleness_seconds"
type="number"
min="0"
step="0.5"
value="{{ ws_max_staleness_seconds }}"
/>
</label>
</div>
<!-- Risk -->
<div class="card">
<div class="label">Risk Limits</div>
<label class="field">
<span>Daily loss limit USD</span>
<input
name="daily_loss_limit_usd"
type="number"
min="0"
step="0.01"
value="{{ daily_loss_limit_value }}"
placeholder="None"
/>
</label>
<label class="field">
<span>Cumulative loss limit USD</span>
<input
name="cumulative_loss_limit_usd"
type="number"
min="0"
step="0.01"
value="{{ cumulative_loss_limit_value }}"
placeholder="None"
/>
</label>
<label class="field">
<span>Max source latency (ms)</span>
<input
name="max_source_latency_ms"
type="number"
min="0"
step="1"
value="{{ max_source_latency_value }}"
placeholder="None"
/>
</label>
<label class="field">
<span>Max apply latency (ms)</span>
<input
name="max_apply_latency_ms"
type="number"
min="0"
step="1"
value="{{ max_apply_latency_value }}"
placeholder="None"
/>
</label>
<label class="field">
<span>Max consecutive failures</span>
<input
name="max_consecutive_failures"
type="number"
min="1"
step="1"
value="{{ max_consecutive_failures_value }}"
placeholder="None"
/>
</label>
<label class="field checkbox">
<input
name="kill_switch_active"
type="checkbox"
{{
kill_switch_active
}}
/>
<span>Kill switch active</span>
</label>
</div>
<!-- Strategy Stat-Arb -->
<div class="card">
<div class="label">Stat-Arb Strategy</div>
<label class="field checkbox">
<input
name="strategy_enable_stat_arb_experiment"
type="checkbox"
{%
if
strategy_stat_arb_enabled
%}checked{%
endif
%}
/>
<span>Enable stat-arb experiment</span>
</label>
{% if strategy_stat_arb_enabled %}
<label class="field">
<span>Lookback window</span>
<input
name="strategy_stat_arb_lookback_window"
type="number"
min="2"
step="1"
value="{{ strategy_stat_arb_lookback_window }}"
/>
</label>
<label class="field">
<span>Entry z-score</span>
<input
name="strategy_stat_arb_entry_zscore"
type="number"
min="0"
step="0.1"
value="{{ strategy_stat_arb_entry_zscore }}"
/>
</label>
<label class="field">
<span>Exit z-score</span>
<input
name="strategy_stat_arb_exit_zscore"
type="number"
min="0"
step="0.1"
value="{{ strategy_stat_arb_exit_zscore }}"
/>
</label>
<label class="field">
<span>Max holding seconds</span>
<input
name="strategy_stat_arb_max_holding_seconds"
type="number"
min="1"
step="1"
value="{{ strategy_stat_arb_max_holding_seconds }}"
/>
</label>
{% endif %}
</div>
<!-- Submit -->
<div
class="card"
style="display: flex; align-items: center; justify-content: center"
>
<button
type="submit"
class="button"
style="padding: 14px 32px; font-size: 1.1rem"
>
Save configuration
</button>
</div>
</form>
</div>
@@ -0,0 +1,214 @@
<div id="config-fees-panel" class="panel" style="margin-top: 16px">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
"
>
<h2 style="margin: 0; font-size: 1.3rem">Pair Fees</h2>
<button
type="button"
class="button"
onclick="
document.getElementById('fee-form-section').style.display =
document.getElementById('fee-form-section').style.display === 'none'
? 'block'
: 'none'
"
>
Add / Edit Fee
</button>
</div>
{% if error %}
<div
style="
background: rgba(186, 61, 79, 0.2);
border: 1px solid #ba3d4f;
border-radius: 10px;
padding: 12px;
margin-bottom: 16px;
"
>
{{ error }}
</div>
{% endif %}
<!-- Fee Table -->
{% if pair_fees %}
<div style="overflow-x: auto">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem">
<thead>
<tr style="border-bottom: 1px solid rgba(255, 255, 255, 0.14)">
<th style="text-align: left; padding: 10px; color: #9fb2d0">Pair</th>
<th style="text-align: left; padding: 10px; color: #9fb2d0">
Market Type
</th>
<th style="text-align: left; padding: 10px; color: #9fb2d0">
Maker Rate
</th>
<th style="text-align: left; padding: 10px; color: #9fb2d0">
Taker Rate
</th>
<th style="text-align: left; padding: 10px; color: #9fb2d0">
Updated
</th>
<th style="text-align: left; padding: 10px; color: #9fb2d0">
Actions
</th>
</tr>
</thead>
<tbody>
{% for fee in pair_fees %}
<tr style="border-bottom: 1px solid rgba(255, 255, 255, 0.06)">
<td style="padding: 10px">{{ fee.pair_display }}</td>
<td style="padding: 10px">{{ fee.market_type }}</td>
<td style="padding: 10px">{{ "%.4f"|format(fee.maker_fee_rate) }}</td>
<td style="padding: 10px">{{ "%.4f"|format(fee.taker_fee_rate) }}</td>
<td style="padding: 10px; color: #7f95b7">
{{ fee.updated_at or "—" }}
</td>
<td style="padding: 10px">
<form
hx-post="{{ config_fees_endpoint }}"
hx-target="#config-fees-panel"
hx-swap="outerHTML"
style="display: inline"
>
<input type="hidden" name="action" value="update" />
<input
type="hidden"
name="base_asset"
value="{{ fee.base_asset }}"
/>
<input
type="hidden"
name="quote_asset"
value="{{ fee.quote_asset }}"
/>
<input
type="hidden"
name="market_type"
value="{{ fee.market_type }}"
/>
<input
type="hidden"
name="maker_fee_rate"
value="{{ fee.maker_fee_rate }}"
/>
<input
type="hidden"
name="taker_fee_rate"
value="{{ fee.taker_fee_rate }}"
/>
<button
type="submit"
class="button secondary"
style="padding: 4px 10px; font-size: 0.8rem"
>
Edit
</button>
</form>
<form
hx-post="{{ config_fees_endpoint }}"
hx-target="#config-fees-panel"
hx-swap="outerHTML"
style="display: inline"
>
<input type="hidden" name="action" value="delete" />
<input
type="hidden"
name="base_asset"
value="{{ fee.base_asset }}"
/>
<input
type="hidden"
name="quote_asset"
value="{{ fee.quote_asset }}"
/>
<input
type="hidden"
name="market_type"
value="{{ fee.market_type }}"
/>
<button
type="submit"
class="button danger"
style="padding: 4px 10px; font-size: 0.8rem"
onclick="return confirm('Delete this fee?');"
>
Delete
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="color: #7f95b7; padding: 20px; text-align: center">
No pair fees configured. Click "Add / Edit Fee" to create one.
</div>
{% endif %}
<!-- Add/Edit Form -->
<div id="fee-form-section" style="display: none; margin-top: 20px">
<div class="card">
<div class="label">Add / Edit Fee</div>
<form
class="form-grid"
hx-post="{{ config_fees_endpoint }}"
hx-target="#config-fees-panel"
hx-swap="outerHTML"
style="
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
"
>
<input type="hidden" name="action" value="add" />
<label class="field">
<span>Base Asset</span>
<input name="base_asset" type="text" placeholder="BTC" required />
</label>
<label class="field">
<span>Quote Asset</span>
<input name="quote_asset" type="text" placeholder="USD" required />
</label>
<label class="field">
<span>Market Type</span>
<select name="market_type">
<option value="crypto_crypto">crypto_crypto</option>
<option value="crypto_fiat">crypto_fiat</option>
</select>
</label>
<label class="field">
<span>Maker Fee Rate</span>
<input
name="maker_fee_rate"
type="number"
min="0"
step="0.0001"
placeholder="0.0016"
/>
</label>
<label class="field">
<span>Taker Fee Rate</span>
<input
name="taker_fee_rate"
type="number"
min="0"
step="0.0001"
placeholder="0.0026"
/>
</label>
<label class="field">
<span>&nbsp;</span>
<button type="submit" class="button">Save Fee</button>
</label>
</form>
</div>
</div>
</div>
-24
View File
@@ -1,24 +0,0 @@
{% extends "base.html" %} {% block title %}{{ title }}{% endblock %} {% block
content %}
<section class="hero">
<div>
<h1 class="title">Backtesting</h1>
<p class="subtitle">
Replay controls, run status, and recent summary reports.
</p>
</div>
<div class="toolbar">
<a class="button secondary" href="{{ dashboard_endpoint }}">Dashboard</a>
</div>
</section>
<section
id="backtesting-shell"
hx-get="{{ panel_endpoint }}"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
{% include "partials/backtesting_panel.html" %}
</section>
{% endblock %}
-148
View File
@@ -1,148 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}{{ title or "Arbitrade" }}{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
{% block head_scripts %}{% endblock %}
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background: #0b1220;
color: #e5eefb;
}
.shell {
max-width: 1120px;
margin: 0 auto;
padding: 32px 20px 48px;
}
.hero {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: end;
margin-bottom: 24px;
}
.title {
font-size: 2rem;
margin: 0 0 8px;
}
.subtitle {
margin: 0;
color: #9fb2d0;
}
.panel {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 20px;
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.card {
background: rgba(255, 255, 255, 0.04);
border-radius: 14px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.label {
color: #9fb2d0;
font-size: 0.85rem;
margin-bottom: 8px;
}
.value {
font-size: 1.4rem;
font-weight: 700;
}
.meta {
margin-top: 18px;
color: #7f95b7;
font-size: 0.85rem;
}
.toolbar {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.toolbar form {
margin: 0;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
cursor: pointer;
padding: 10px 14px;
border-radius: 999px;
background: #2d6cdf;
color: white;
text-decoration: none;
font: inherit;
}
.button.secondary {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.14);
}
.button.danger {
background: #ba3d4f;
}
.form-grid {
display: grid;
gap: 12px;
}
.field {
display: grid;
gap: 6px;
color: #9fb2d0;
font-size: 0.9rem;
}
.field input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
color: #e5eefb;
font: inherit;
}
.field.checkbox {
display: flex;
align-items: center;
gap: 8px;
}
.field.checkbox input {
width: auto;
}
.control-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.chart-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.chart-canvas {
width: 100%;
min-height: 260px;
}
</style>
{% block extra_style %}{% endblock %}
</head>
<body>
<main class="{% block main_class %}shell{% endblock %}">
{% block content %}{% endblock %}
</main>
{% block scripts %}{% endblock %}
</body>
</html>
-180
View File
@@ -1,180 +0,0 @@
{% extends "base.html" %} {% block title %}{{ title }}{% endblock %} {% block
head_scripts %}
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
{% endblock %} {% block main_class %}shell{% endblock %} {% block content %}
<section class="hero">
<div>
<h1 class="title">Arbitrade Dashboard</h1>
<p class="subtitle">Live execution, P&amp;L, and system state.</p>
</div>
<div class="toolbar">
<a
class="button"
href="{{ metrics_endpoint }}"
hx-get="{{ metrics_endpoint }}"
hx-target="#metrics-panel"
hx-swap="outerHTML"
>Refresh metrics</a
>
<a class="button secondary" href="/health">Health</a>
<a class="button secondary" href="/dashboard/backtesting">Backtesting</a>
</div>
</section>
<section
id="metrics-shell"
hx-get="{{ metrics_endpoint }}"
hx-target="this"
hx-trigger="load, every 15s"
hx-swap="outerHTML"
>
{% include "partials/metrics.html" %}
</section>
<section
id="overview-shell"
hx-get="{{ overview_endpoint }}"
hx-target="this"
hx-trigger="load, every 10s"
hx-swap="outerHTML"
>
{% include "partials/overview.html" %}
</section>
<section
id="controls-shell"
hx-get="{{ controls_endpoint }}"
hx-target="this"
hx-trigger="load, every 20s"
hx-swap="outerHTML"
>
{% include "partials/controls.html" %}
</section>
<section
id="charts-shell"
hx-get="{{ charts_endpoint }}"
hx-target="this"
hx-trigger="load, every 30s"
hx-swap="outerHTML"
>
{% include "partials/charts.html" %}
</section>
<section
id="audit-shell"
hx-get="{{ audit_endpoint }}"
hx-target="this"
hx-trigger="load, every 20s"
hx-swap="outerHTML"
>
{% include "partials/audit.html" %}
</section>
{% endblock %} {% block scripts %}
<script>
window.arbitradeRenderCharts = (payload) => {
const chartHost = document.getElementById("opportunity-chart");
if (!chartHost || typeof Chart === "undefined") {
return;
}
const existing = Chart.getChart(chartHost);
if (existing) {
existing.destroy();
}
const data = JSON.parse(payload);
if (!data.has_chart_data) {
return;
}
new Chart(chartHost, {
type: "line",
data: {
labels: data.labels,
datasets: [
{
label: "Net %",
data: data.net_pct_values,
borderColor: "#2d6cdf",
backgroundColor: "rgba(45, 108, 223, 0.18)",
tension: 0.3,
fill: true,
yAxisID: "y",
},
{
label: "Est profit USD",
data: data.est_profit_values,
borderColor: "#52c41a",
backgroundColor: "rgba(82, 196, 26, 0.12)",
tension: 0.3,
fill: false,
yAxisID: "y1",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false,
},
plugins: {
legend: {
labels: {
color: "#e5eefb",
},
},
},
scales: {
x: {
ticks: {
color: "#9fb2d0",
maxRotation: 0,
autoSkip: true,
},
grid: {
color: "rgba(255, 255, 255, 0.06)",
},
},
y: {
position: "left",
ticks: {
color: "#9fb2d0",
},
grid: {
color: "rgba(255, 255, 255, 0.06)",
},
},
y1: {
position: "right",
ticks: {
color: "#9fb2d0",
},
grid: {
drawOnChartArea: false,
},
},
},
},
});
};
const stream = new EventSource("{{ stream_endpoint }}");
stream.addEventListener("metrics", (event) => {
const panel = document.getElementById("metrics-panel");
if (panel) {
panel.outerHTML = JSON.parse(event.data);
}
});
const overviewStream = new EventSource("{{ overview_stream_endpoint }}");
overviewStream.addEventListener("overview", (event) => {
const panel = document.getElementById("overview-panel");
if (panel) {
panel.outerHTML = JSON.parse(event.data);
}
});
</script>
{% endblock %}
-14
View File
@@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block content %}
<section class="card">
<h1>Arbitrade Bootstrap Complete</h1>
<p><span class="badge">Status: {{ status }}</span></p>
<p>UTC: {{ time }}</p>
<p>
Health JSON:
<a href="/health" hx-get="/health" hx-target="#health-json" hx-swap="innerHTML">refresh</a>
</p>
<pre id="health-json">{"status":"ok","service":"arbitrade"}</pre>
</section>
{% endblock %}
-37
View File
@@ -1,37 +0,0 @@
<div id="audit-panel" class="panel" style="margin-top: 16px">
<div class="label">Audit Trail</div>
<div class="meta">Generated {{ generated_at }}</div>
<div style="overflow-x: auto; margin-top: 12px">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem">
<thead>
<tr>
<th style="text-align: left; padding: 8px">Time</th>
<th style="text-align: left; padding: 8px">Actor</th>
<th style="text-align: left; padding: 8px">Event</th>
<th style="text-align: left; padding: 8px">Decision</th>
<th style="text-align: left; padding: 8px">Payload</th>
<th style="text-align: left; padding: 8px">Correlation</th>
</tr>
</thead>
<tbody>
{% if entries %}
{% for entry in entries %}
<tr>
<td style="padding: 8px; color: #9fb2d0">{{ entry.occurred_at }}</td>
<td style="padding: 8px">{{ entry.actor }}</td>
<td style="padding: 8px">{{ entry.event_type }}</td>
<td style="padding: 8px">{{ entry.decision }}</td>
<td style="padding: 8px; color: #9fb2d0">{{ entry.payload }}</td>
<td style="padding: 8px; color: #9fb2d0">{{ entry.correlation_id }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" style="padding: 8px; color: #9fb2d0">No audit entries yet.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
@@ -1,142 +0,0 @@
<div id="backtesting-shell" class="panel">
<div
class="grid"
style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))"
>
<article class="card">
<div class="label">Run Status</div>
<div class="value">{{ status }}</div>
<div class="meta">{{ message }}</div>
</article>
<article class="card">
<div class="label">Latest Report</div>
{% if latest_report %}
<div class="meta">Run at {{ latest_report.run_at }}</div>
<div class="meta">Events: {{ latest_report.events_path }}</div>
<div class="meta">
Processed: {{ latest_report.report.processed_events }}
</div>
<div class="meta">
Opportunities: {{ latest_report.report.opportunities_seen }}
</div>
<div class="meta">Trades: {{ latest_report.report.trades_executed }}</div>
<div class="meta">
Realized P&amp;L: {{
'%.4f'|format(latest_report.report.realized_pnl_usd) }} USD
</div>
<div class="meta">
Max drawdown: {{ '%.4f'|format(latest_report.report.max_drawdown_usd) }}
USD
</div>
{% else %}
<div class="meta">No runs yet.</div>
{% endif %}
</article>
</div>
<article class="card" style="margin-top: 16px">
<div class="label">Run Backtest</div>
<form
class="form-grid"
hx-post="{{ run_endpoint }}"
hx-target="#backtesting-shell"
hx-swap="outerHTML"
>
<label class="field">
<span>Replay events path (JSONL)</span>
<input
name="events_path"
type="text"
value="{{ events_path }}"
placeholder="data/replay.jsonl"
/>
</label>
<label class="field">
<span>Starting balances</span>
<input
name="starting_balances"
type="text"
value="{{ starting_balances }}"
placeholder="USD=1000.0,BTC=0.0"
/>
</label>
<label class="field">
<span>Trade capital</span>
<input
name="trade_capital"
type="number"
min="0"
step="0.01"
value="{{ trade_capital }}"
/>
</label>
<label class="field">
<span>Min profit threshold</span>
<input
name="min_profit_threshold"
type="number"
min="0"
step="0.0001"
value="{{ min_profit_threshold }}"
/>
</label>
<label class="field">
<span>Fee profile</span>
<select name="fee_profile">
{% set sel = "selected" if fee_profile == "standard" else "" %}
<option value="standard" {{ sel }}>standard</option>
{% set sel = "selected" if fee_profile == "maker_heavy" else "" %}
<option value="maker_heavy" {{ sel }}>maker_heavy</option>
{% set sel = "selected" if fee_profile == "taker_heavy" else "" %}
<option value="taker_heavy" {{ sel }}>taker_heavy</option>
{% set sel = "selected" if fee_profile == "custom" else "" %}
<option value="custom" {{ sel }}>custom</option>
</select>
</label>
<label class="field">
<span>Custom fee rate (if fee profile = custom)</span>
<input
name="custom_fee_rate"
type="number"
min="0"
step="0.0001"
value="{{ custom_fee_rate }}"
/>
</label>
<label class="field">
<span>Slippage (bps)</span>
<input
name="slippage_bps"
type="number"
min="0"
step="0.1"
value="{{ slippage_bps }}"
/>
</label>
<label class="field">
<span>Execution latency (ms)</span>
<input
name="execution_latency_ms"
type="number"
min="0"
step="0.1"
value="{{ execution_latency_ms }}"
/>
</label>
<button type="submit" class="button">Run backtest</button>
</form>
</article>
<article class="card" style="margin-top: 16px">
<div class="label">Recent Runs</div>
{% if recent_reports %} {% for item in recent_reports %}
<div class="meta">
{{ item.run_at }} | {{ item.events_path }} | trades={{
item.report.trades_executed }} | pnl={{
'%.4f'|format(item.report.realized_pnl_usd) }} USD
</div>
{% endfor %} {% else %}
<div class="meta">No recent reports yet.</div>
{% endif %}
</article>
</div>
-37
View File
@@ -1,37 +0,0 @@
<div
id="charts-panel"
class="panel"
style="margin-top: 16px"
x-data="{ expanded: true }"
>
<div class="chart-head">
<div>
<div class="label">Opportunity Trend</div>
<div class="meta">Recent opportunities from DuckDB. Updated {{ generated_at }}</div>
</div>
<button type="button" class="button secondary" x-on:click="expanded = !expanded">
<span x-text="expanded ? 'Hide chart' : 'Show chart'"></span>
</button>
</div>
<div x-show="expanded" x-transition style="margin-top: 16px">
<div class="card" style="padding: 12px">
{% if has_chart_data %}
<canvas id="opportunity-chart" class="chart-canvas"></canvas>
<script>
window.arbitradeRenderCharts(
{{ {
"has_chart_data": has_chart_data,
"labels": labels,
"net_pct_values": net_pct_values,
"est_profit_values": est_profit_values,
"cycles": cycles,
} | tojson }}
);
</script>
{% else %}
<div class="meta">No opportunity data yet.</div>
{% endif %}
</div>
</div>
</div>
-171
View File
@@ -1,171 +0,0 @@
<div id="controls-panel" class="panel" style="margin-top: 16px">
<div class="grid">
<article class="card">
<div class="label">Runtime Status</div>
<div class="value">{{ execution_status }}</div>
<div class="meta">Updated {{ updated_at }}</div>
</article>
<article class="card">
<div class="label">Kill Switch</div>
<div class="value">{{ kill_switch_status }}</div>
<div class="meta">Reason {{ kill_switch_reason }}</div>
</article>
<article class="card">
<div class="label">Config Snapshot</div>
<div class="meta">Paper trading: {{ paper_trading_mode }}</div>
<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>
<div class="meta">Status: {{ alerts_enabled }}</div>
<div class="meta">Channels: {{ alerts_channels }}</div>
<div class="meta">Min severity: {{ alerts_min_severity }}</div>
<div class="meta">Dedup window: {{ alerts_dedup_seconds }}s</div>
<div class="meta">Last result: {{ alerts_last_result }}</div>
<div class="meta">Last attempted: {{ alerts_last_attempted_at }}</div>
<div class="meta">Last success: {{ alerts_last_success_at }}</div>
<div class="meta">Last event: {{ alerts_last_event_title }}</div>
<div class="meta">Last error: {{ alerts_last_error }}</div>
{% if alerts_last_channel_results %} {% for item in
alerts_last_channel_results %}
<div class="meta">{{ item }}</div>
{% endfor %} {% endif %}
</article>
</div>
<div
class="grid"
style="
margin-top: 16px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
"
>
<article class="card">
<div class="label">Execution Controls</div>
<div class="control-actions">
<form
hx-post="{{ start_endpoint }}"
hx-target="#controls-panel"
hx-swap="outerHTML"
>
<button type="submit" class="button">Start</button>
</form>
<form
hx-post="{{ stop_endpoint }}"
hx-target="#controls-panel"
hx-swap="outerHTML"
>
<button type="submit" class="button secondary">Stop</button>
</form>
<form
hx-post="{{ kill_switch_endpoint }}"
hx-target="#controls-panel"
hx-swap="outerHTML"
>
<input type="hidden" name="reason" value="manual" />
<button type="submit" class="button danger">
Trigger Kill Switch
</button>
</form>
</div>
</article>
<article class="card">
<div class="label">Edit Config</div>
<form
class="form-grid"
hx-post="{{ config_endpoint }}"
hx-target="#controls-panel"
hx-swap="outerHTML"
>
<label class="field">
<span>Trade capital USD</span>
<input
name="trade_capital_usd"
type="number"
min="0"
step="0.01"
value="{{ trade_capital_usd_value }}"
/>
</label>
<label class="field">
<span>Max trade capital USD</span>
<input
name="max_trade_capital_usd"
type="number"
min="0"
step="0.01"
value="{{ max_trade_capital_usd_value }}"
/>
</label>
<label class="field">
<span>Max concurrent trades</span>
<input
name="max_concurrent_trades"
type="number"
min="1"
step="1"
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">
{% set sel = "selected" if strategy_mode == "incremental" else "" %}
<option value="incremental" {{ sel }}>incremental</option>
{% set sel = "selected" if strategy_mode == "paper" else "" %}
<option value="paper" {{ sel }}>paper</option>
{% set sel = "selected" if strategy_mode == "live" else "" %}
<option value="live" {{ sel }}>live</option>
{% if strategy_stat_arb_enabled %} {% set sel = "selected" if
strategy_mode == "stat_arb_experiment" else "" %}
<option value="stat_arb_experiment" {{ sel }}>
stat_arb_experiment
</option>
{% endif %}
</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 }} />
<span>Paper trading mode</span>
</label>
<button type="submit" class="button">Save config</button>
</form>
</article>
</div>
</div>
-31
View File
@@ -1,31 +0,0 @@
<div id="metrics-panel" class="panel">
<div class="grid">
<article class="card">
<div class="label">Realized P&amp;L</div>
<div class="value">{{ realized_pnl }}</div>
</article>
<article class="card">
<div class="label">Win Rate</div>
<div class="value">{{ win_rate }}</div>
</article>
<article class="card">
<div class="label">Avg Trade Duration</div>
<div class="value">{{ avg_trade_duration }}</div>
</article>
<article class="card">
<div class="label">Opportunities / Min</div>
<div class="value">{{ opportunities_per_minute }}</div>
</article>
<article class="card">
<div class="label">Fill Rate</div>
<div class="value">{{ fill_rate }}</div>
</article>
<article class="card">
<div class="label">Latency p50 / p95 / p99</div>
<div class="value">
{{ latency_p50 }} | {{ latency_p95 }} | {{ latency_p99 }}
</div>
</article>
</div>
<div class="meta">Updated {{ generated_at }}</div>
</div>
-67
View File
@@ -1,67 +0,0 @@
<div id="overview-panel" class="panel" style="margin-top: 16px">
<div class="grid">
<article class="card">
<div class="label">Status</div>
<div class="value">{{ status }}</div>
</article>
<article class="card">
<div class="label">Balances</div>
<div class="value">{{ balances }}</div>
</article>
<article class="card">
<div class="label">Open Trades</div>
<div class="value">{{ open_trade_count }}</div>
</article>
<article class="card">
<div class="label">Realized P&amp;L</div>
<div class="value">{{ realized_pnl_total }}</div>
</article>
</div>
<div
class="grid"
style="
margin-top: 16px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
"
>
<article class="card">
<div class="label">Open Trades</div>
<ul>
{% for trade in open_trades %}
<li>
{{ trade.trade_ref }} - {{ trade.status }} - {{ trade.cycle }} - {{
trade.started_at }}
</li>
{% else %}
<li>No open trades.</li>
{% endfor %}
</ul>
</article>
<article class="card">
<div class="label">Balances Snapshot</div>
<div
class="value"
style="font-size: 1rem; font-weight: 500; word-break: break-word"
>
{{ balances }}
</div>
<div class="meta">Total value {{ total_value }}</div>
</article>
<article class="card">
<div class="label">Opportunity Feed</div>
<ul>
{% for opp in opportunities %}
<li>
{{ opp.cycle }} - {{ opp.net_pct }} - {{ opp.est_profit }} - {{
opp.detected_at }}
</li>
{% else %}
<li>No opportunities.</li>
{% endfor %}
</ul>
</article>
</div>
<div class="meta">Updated {{ generated_at }}</div>
</div>