feat: add pairings management page and integrate with Kraken API for syncing

feat: create configuration templates for alerts, Kraken settings, risk limits, and runtime settings
refactor: streamline config form by including separate template files for better organization
This commit is contained in:
2026-06-07 17:44:26 +02:00
parent c1dda187af
commit db2e02c316
9 changed files with 581 additions and 668 deletions
+34 -14
View File
@@ -899,6 +899,25 @@ async def dashboard_audit(request: Request) -> HTMLResponse:
) )
@router.get("/dashboard/config/pairings", response_class=HTMLResponse)
async def dashboard_config_pairings_page(
request: Request,
search: str | None = None,
enabled: str | None = None,
) -> HTMLResponse:
"""Standalone pairings management page."""
return templates.TemplateResponse(
request=request,
name="pairings.html",
context={
"title": "Currency Pairings",
"request": request,
"search": search or "",
"enabled": enabled or "all",
},
)
@router.get("/dashboard/config", response_class=HTMLResponse) @router.get("/dashboard/config", response_class=HTMLResponse)
async def dashboard_config_page(request: Request) -> HTMLResponse: async def dashboard_config_page(request: Request) -> HTMLResponse:
d_context = await _dashboard_config_context(request) d_context = await _dashboard_config_context(request)
@@ -1447,19 +1466,20 @@ async def dashboard_api_pairings_toggle(request: Request) -> HTMLResponse:
) )
@router.post("/dashboard/api/pairings/sync") @router.post("/dashboard/api/pairings/sync", response_class=HTMLResponse)
async def dashboard_api_pairings_sync(request: Request) -> JSONResponse: async def dashboard_api_pairings_sync(request: Request) -> HTMLResponse:
"""Trigger a re-sync of pairings from Kraken.""" """Sync pairings from Kraken and return refreshed table."""
kraken_client = request.app.state.kraken_client from arbitrade.config.pairing_sync import sync_pairings_from_kraken
store = request.app.state.store store = request.app.state.store
summary = await sync_pairings_from_kraken(kraken_client, store) kraken = getattr(request.app.state, "kraken_client", None)
if kraken is not None:
await _record_audit( await sync_pairings_from_kraken(kraken, store)
request, repo = _pairing_repo(request)
actor="dashboard_user", pairings = await repo.list_pairings()
event_type="dashboard.pairings.sync", pairings.sort(key=lambda p: (p.base_asset, p.quote_asset))
decision="approved", return templates.TemplateResponse(
payload=summary, # type: ignore request=request,
name="partials/pairings_table.html",
context={"request": request, "pairings": pairings},
) )
return JSONResponse(summary)
+2 -1
View File
@@ -86,7 +86,8 @@ class OrderBook:
BookLevel(price=price, volume=self._bids[price]) BookLevel(price=price, volume=self._bids[price])
for price in reversed(bid_keys[-depth:]) for price in reversed(bid_keys[-depth:])
] ]
asks = [BookLevel(price=price, volume=self._asks[price]) for price in ask_keys[:depth]] asks = [BookLevel(price=price, volume=self._asks[price])
for price in ask_keys[:depth]]
return bids, asks return bids, asks
def compute_checksum(self, depth: int = 10) -> int: def compute_checksum(self, depth: int = 10) -> int:
+4 -3
View File
@@ -5,9 +5,10 @@
</div> </div>
{% set nav_links = [ {"url": "/dashboard", "label": "Dashboard", "class": {% set nav_links = [ {"url": "/dashboard", "label": "Dashboard", "class":
"secondary"}, {"url": "/dashboard/config", "label": "Config", "class": "secondary"}, {"url": "/dashboard/config", "label": "Config", "class":
"secondary"}, {"url": "/dashboard/backtesting", "label": "Backtesting", "secondary"}, {"url": "/dashboard/config/pairings", "label": "Pairings",
"class": "secondary"}, {"url": "/dashboard/health", "label": "Health", "class": "secondary"}, {"url": "/dashboard/backtesting", "label":
"class": "secondary"}, ] %} "Backtesting", "class": "secondary"}, {"url": "/dashboard/health", "label":
"Health", "class": "secondary"}, ] %}
<div class="toolbar"> <div class="toolbar">
{% for link in nav_links %} {% for link in nav_links %}
<a <a
@@ -0,0 +1,192 @@
<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=""
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>
@@ -0,0 +1,93 @@
<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 }}" />
</label>
<label class="field">
<span>API secret</span>
<input
name="kraken_api_secret"
type="password"
value=""
placeholder="Leave blank to keep existing"
/>
</label>
<label class="field">
<span>API key permissions</span>
<input
name="kraken_api_key_permissions"
type="text"
value="{{ kraken_api_key_permissions }}"
disabled
/>
</label>
<label class="field">
<span>WS 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>WS max staleness (s)</span>
<input
name="ws_max_staleness_seconds"
type="number"
min="1"
step="1"
value="{{ ws_max_staleness_seconds }}"
/>
</label>
</div>
@@ -0,0 +1,57 @@
<div class="card">
<div class="label">Risk & Guardrails</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 }}"
/>
</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 }}"
/>
</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 }}"
/>
</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 }}"
/>
</label>
<label class="field">
<span>Max consecutive failures</span>
<input
name="max_consecutive_failures"
type="number"
min="0"
step="1"
value="{{ max_consecutive_failures_value }}"
/>
</label>
<label class="field checkbox">
<input name="kill_switch_active" type="checkbox" {{ kill_switch_active }} />
<span>Kill switch active</span>
</label>
</div>
@@ -0,0 +1,140 @@
<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>
+52
View File
@@ -0,0 +1,52 @@
{% extends "_base.html" %} {% block title %}{{ title }}{% endblock %} {% block
main_class %}shell{% endblock %} {% block header %} {% with
page_title="Currency Pairings",
page_subtitle="Enable/disable pairings, search, and sync from Kraken." %}
{% include "_header.html" %} {% endwith %} {% endblock %} {% block content %}
<div class="toolbar" style="margin-bottom: 16px; display: flex; gap: 8px">
<input
id="pairing-search"
type="text"
placeholder="Search pairings…"
value="{{ search or '' }}"
style="flex: 1; max-width: 300px"
hx-get="/dashboard/fragment/pairings"
hx-target="#pairings-table-container"
hx-trigger="keyup changed delay:300ms"
hx-include="#pairing-enabled-filter"
name="search"
/>
<select
id="pairing-enabled-filter"
name="enabled"
hx-get="/dashboard/fragment/pairings"
hx-target="#pairings-table-container"
hx-trigger="change"
hx-include="#pairing-search"
>
<option value="all">All</option>
<option value="true" {{ 'selected' if enabled == 'true' else '' }}>
Enabled
</option>
<option value="false" {{ 'selected' if enabled == 'false' else '' }}>
Disabled
</option>
</select>
<button
class="button"
hx-post="/dashboard/api/pairings/sync"
hx-target="#pairings-table-container"
hx-swap="innerHTML"
>
Sync from Kraken
</button>
</div>
<div id="pairings-table-container">
{% include "partials/pairings_table.html" %}
</div>
{% endblock %}
@@ -4,657 +4,14 @@
hx-post="{{ config_endpoint }}" hx-post="{{ config_endpoint }}"
hx-target="#config-panel" hx-target="#config-panel"
hx-swap="outerHTML" hx-swap="outerHTML"
style=" style="grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px"
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
"
> >
<!-- Runtime --> {% include "config/runtime.html" %}
<div class="card"> {% include "config/alerts.html" %}
<div class="label">Runtime</div> {% include "config/kraken.html" %}
<label class="field"> {% include "config/risk.html" %}
<span>App env</span> <div style="grid-column: 1 / -1">
<input type="text" value="{{ app_env }}" disabled /> <button type="submit" class="button">Save Settings</button>
</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>
<!-- Pairings -->
<div class="card" id="pairings-card">
<div class="label">Currency Pairings</div>
<div style="display: flex; gap: 8px; margin-bottom: 12px">
<input
id="pairing-search"
type="text"
placeholder="Search pairings..."
hx-get="/dashboard/fragment/pairings"
hx-target="#pairings-table-container"
hx-trigger="keyup changed delay:300ms"
hx-swap="innerHTML"
name="search"
style="
flex: 1;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.3);
color: inherit;
"
/>
<button
type="button"
class="button"
id="pairing-sync-btn"
hx-post="/dashboard/api/pairings/sync"
hx-target="#pairings-table-container"
hx-swap="innerHTML"
hx-trigger="click"
style="white-space: nowrap"
>
Sync from Kraken
</button>
</div>
<div
id="pairings-table-container"
hx-get="/dashboard/fragment/pairings"
hx-trigger="load"
>
<div style="text-align: center; padding: 20px; opacity: 0.5">
Loading pairings...
</div>
</div>
</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> </div>
</form> </form>
</div> </div>