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:
+34
-14
@@ -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)
|
||||
async def dashboard_config_page(request: Request) -> HTMLResponse:
|
||||
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")
|
||||
async def dashboard_api_pairings_sync(request: Request) -> JSONResponse:
|
||||
"""Trigger a re-sync of pairings from Kraken."""
|
||||
kraken_client = request.app.state.kraken_client
|
||||
@router.post("/dashboard/api/pairings/sync", response_class=HTMLResponse)
|
||||
async def dashboard_api_pairings_sync(request: Request) -> HTMLResponse:
|
||||
"""Sync pairings from Kraken and return refreshed table."""
|
||||
from arbitrade.config.pairing_sync import sync_pairings_from_kraken
|
||||
|
||||
store = request.app.state.store
|
||||
summary = await sync_pairings_from_kraken(kraken_client, store)
|
||||
|
||||
await _record_audit(
|
||||
request,
|
||||
actor="dashboard_user",
|
||||
event_type="dashboard.pairings.sync",
|
||||
decision="approved",
|
||||
payload=summary, # type: ignore
|
||||
kraken = getattr(request.app.state, "kraken_client", None)
|
||||
if kraken is not None:
|
||||
await sync_pairings_from_kraken(kraken, store)
|
||||
repo = _pairing_repo(request)
|
||||
pairings = await repo.list_pairings()
|
||||
pairings.sort(key=lambda p: (p.base_asset, p.quote_asset))
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="partials/pairings_table.html",
|
||||
context={"request": request, "pairings": pairings},
|
||||
)
|
||||
|
||||
return JSONResponse(summary)
|
||||
|
||||
@@ -86,7 +86,8 @@ class OrderBook:
|
||||
BookLevel(price=price, volume=self._bids[price])
|
||||
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
|
||||
|
||||
def compute_checksum(self, depth: int = 10) -> int:
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
</div>
|
||||
{% set nav_links = [ {"url": "/dashboard", "label": "Dashboard", "class":
|
||||
"secondary"}, {"url": "/dashboard/config", "label": "Config", "class":
|
||||
"secondary"}, {"url": "/dashboard/backtesting", "label": "Backtesting",
|
||||
"class": "secondary"}, {"url": "/dashboard/health", "label": "Health",
|
||||
"class": "secondary"}, ] %}
|
||||
"secondary"}, {"url": "/dashboard/config/pairings", "label": "Pairings",
|
||||
"class": "secondary"}, {"url": "/dashboard/backtesting", "label":
|
||||
"Backtesting", "class": "secondary"}, {"url": "/dashboard/health", "label":
|
||||
"Health", "class": "secondary"}, ] %}
|
||||
<div class="toolbar">
|
||||
{% for link in nav_links %}
|
||||
<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>
|
||||
@@ -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-target="#config-panel"
|
||||
hx-swap="outerHTML"
|
||||
style="
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
"
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
{% include "config/runtime.html" %}
|
||||
{% include "config/alerts.html" %}
|
||||
{% include "config/kraken.html" %}
|
||||
{% include "config/risk.html" %}
|
||||
<div style="grid-column: 1 / -1">
|
||||
<button type="submit" class="button">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user