feat: enhance fee management with API integration and audit trail support

This commit is contained in:
2026-06-03 19:27:32 +02:00
parent 587c9afc3b
commit ff71fc5feb
16 changed files with 206 additions and 144 deletions
+30
View File
@@ -49,6 +49,36 @@ Key features include:
- Backtesting parameter configuration - Backtesting parameter configuration
- Fee configuration by pairing and market type - Fee configuration by pairing and market type
## Templates
Full page templates (`src/arbitrade/web/templates/`):
| Template | Route | Purpose |
| ------------------ | ------------------------ | ------------------------------------------------------- |
| `base.html` | — (root layout) | Dark theme, `.shell` container, HTMX, CSS variables |
| `dashboard.html` | `/`, `/dashboard` | Main dashboard: metrics, overview, controls, charts |
| `config.html` | `/dashboard/config` | Full configuration: fees, runtime, alerts, Kraken, risk |
| `audit.html` | `/dashboard/audit` | Audit trail with auto-refresh via HTMX |
| `backtesting.html` | `/dashboard/backtesting` | Backtesting panel with replay/sweep forms |
| `health.html` | `/health` | System health check |
Dashboard partials (`src/arbitrade/web/templates/partials/`):
| Partial | In page | Content |
| ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------- |
| `metrics.html` | Dashboard | 6 KPI cards: P&L, win rate, avg duration, trade count, success %, profit factor |
| `overview.html` | Dashboard | Status, balances, fee tier, open trades list, opportunity feed |
| `controls.html` | Dashboard | Runtime status, kill switch, config snapshot, alerting status, execution controls (Start/Stop/Kill) |
| `charts.html` | Dashboard | Opportunity trend chart (Chart.js, Alpine toggle) |
| `config.html` | Config page | Config form: Runtime, Alerts, Kraken, Risk, Strategy sections |
| `config_fees.html` | Config page | Pair fee table + add/edit form |
| `backtesting_panel.html` | Backtesting page | Run status, replay/sweep forms, recent runs |
| `audit.html` | Audit page | Audit trail table: time, actor, event, decision, payload |
Legacy templates (`src/arbitrade/web/templates/dashboard/`):
- `config_settings.html`, `config_pairs.html`, `config_fees.html` — superseded by config page; retained for reference
## Prerequisites ## Prerequisites
- Python 3.12+ - Python 3.12+
+34 -6
View File
@@ -6,10 +6,31 @@ from collections.abc import Mapping
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
import duckdb
from arbitrade.backtesting.replay import BacktestConfig, BacktestReplayEngine, load_replay_events from arbitrade.backtesting.replay import BacktestConfig, BacktestReplayEngine, load_replay_events
from arbitrade.detection.graph import CurrencyGraph, TriangularCycle from arbitrade.detection.graph import CurrencyGraph, TriangularCycle
def _resolve_fee_rate(fee_rate: float | None, db_path: str | None = None) -> float:
"""Resolve fee rate from arg or DB snapshot. Falls back to 0.0026."""
if fee_rate is not None:
return fee_rate
if db_path is not None:
try:
conn = duckdb.connect(db_path)
row = conn.execute("""
SELECT maker_fee FROM kraken_account_snapshots
ORDER BY snapshot_at DESC LIMIT 1
""").fetchone()
conn.close()
if row is not None and row[0] is not None:
return float(row[0])
except Exception:
pass
return 0.0026 # ultimate fallback
def _build_graph() -> tuple[dict[str, list[TriangularCycle]], list[str]]: def _build_graph() -> tuple[dict[str, list[TriangularCycle]], list[str]]:
graph = CurrencyGraph() graph = CurrencyGraph()
graph.add_pair("USD", "BTC", "BTC/USD") graph.add_pair("USD", "BTC", "BTC/USD")
@@ -30,19 +51,23 @@ def _parse_balances(raw: str) -> Mapping[str, float]:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description="Run a deterministic replay backtest.") parser = argparse.ArgumentParser(
description="Run a deterministic replay backtest.")
parser.add_argument("--events", type=Path, required=True) parser.add_argument("--events", type=Path, required=True)
parser.add_argument("--starting-balances", type=str, default="USD=1000.0") parser.add_argument("--starting-balances", type=str, default="USD=1000.0")
parser.add_argument("--trade-capital", type=float, default=100.0) parser.add_argument("--trade-capital", type=float, default=100.0)
parser.add_argument("--fee-rate", type=float, default=0.0026) parser.add_argument("--fee-rate", type=float, default=None)
parser.add_argument("--slippage-bps", type=float, default=4.0) parser.add_argument("--slippage-bps", type=float, default=4.0)
parser.add_argument("--execution-latency-ms", type=float, default=20.0) parser.add_argument("--execution-latency-ms", type=float, default=20.0)
parser.add_argument("--db-path", type=str, default=None,
help="DuckDB path for fee lookup")
args = parser.parse_args() args = parser.parse_args()
cycles_by_pair, available_pairs = _build_graph() cycles_by_pair, available_pairs = _build_graph()
events = load_replay_events(args.events) events = load_replay_events(args.events)
fee_rate = _resolve_fee_rate(args.fee_rate, args.db_path)
config = BacktestConfig( config = BacktestConfig(
fee_rate=args.fee_rate, fee_rate=fee_rate,
trade_capital=args.trade_capital, trade_capital=args.trade_capital,
slippage_bps=args.slippage_bps, slippage_bps=args.slippage_bps,
execution_latency_ms=args.execution_latency_ms, execution_latency_ms=args.execution_latency_ms,
@@ -55,15 +80,18 @@ def main() -> int:
started_at=events[0].occurred_at if events else datetime.now(UTC), started_at=events[0].occurred_at if events else datetime.now(UTC),
) )
report = asyncio.run( report = asyncio.run(
engine.run(events, starting_balances=_parse_balances(args.starting_balances)) engine.run(events, starting_balances=_parse_balances(
args.starting_balances))
) )
print("Backtest report:") print("Backtest report:")
print(f"- processed_events: {report.processed_events}") print(f"- processed_events: {report.processed_events}")
print(f"- opportunities_seen: {report.opportunities_seen}") print(f"- opportunities_seen: {report.opportunities_seen}")
print(f"- trades_executed: {report.trades_executed}") print(f"- trades_executed: {report.trades_executed}")
print(f"- win_rate: {report.win_rate if report.win_rate is not None else 'n/a'}") print(
print(f"- fill_rate: {report.fill_rate if report.fill_rate is not None else 'n/a'}") f"- win_rate: {report.win_rate if report.win_rate is not None else 'n/a'}")
print(
f"- fill_rate: {report.fill_rate if report.fill_rate is not None else 'n/a'}")
print(f"- realized_pnl_usd: {report.realized_pnl_usd:.4f}") print(f"- realized_pnl_usd: {report.realized_pnl_usd:.4f}")
print(f"- max_drawdown_usd: {report.max_drawdown_usd:.4f}") print(f"- max_drawdown_usd: {report.max_drawdown_usd:.4f}")
print(f"- miss_reasons: {dict(report.miss_reasons)}") print(f"- miss_reasons: {dict(report.miss_reasons)}")
+63 -15
View File
@@ -19,7 +19,7 @@ from arbitrade.api.auth import require_dashboard_auth
from arbitrade.api.control_state import DashboardControlState from arbitrade.api.control_state import DashboardControlState
from arbitrade.backtesting.replay import BacktestConfig, BacktestReplayEngine, load_replay_events from arbitrade.backtesting.replay import BacktestConfig, BacktestReplayEngine, load_replay_events
from arbitrade.detection.graph import CurrencyGraph, TriangularCycle from arbitrade.detection.graph import CurrencyGraph, TriangularCycle
from arbitrade.storage.repositories import AuditRecord, AuditRepository from arbitrade.storage.repositories import AuditRecord, AuditRepository, KrakenAccountSnapshotRepository
router = APIRouter(dependencies=[Depends(require_dashboard_auth)]) router = APIRouter(dependencies=[Depends(require_dashboard_auth)])
public_router = APIRouter() public_router = APIRouter()
@@ -185,6 +185,7 @@ def _dashboard_overview(request: Request) -> dict[str, object]:
"maker_fee": maker_fee, "maker_fee": maker_fee,
"taker_fee": taker_fee, "taker_fee": taker_fee,
"thirty_day_volume": thirty_day_volume, "thirty_day_volume": thirty_day_volume,
"fee_source": "API" if fee_tier != "" else "",
} }
@@ -569,23 +570,49 @@ def _normalize_fee_profile(profile: str) -> str:
return profile.strip().lower().replace("-", "_") return profile.strip().lower().replace("-", "_")
def _fee_rate_for_profile(profile: str, custom_fee_rate: float | None) -> float: def _fee_rate_for_profile(
profile: str,
custom_fee_rate: float | None,
request: Request | None = None,
) -> float:
"""Resolve fee rate from profile name.
- 'api': fetches latest maker_fee from kraken_account_snapshots (requires request)
- 'custom': uses custom_fee_rate
- legacy 'standard'/'maker_heavy'/'taker_heavy': still supported via hardcoded
fallback, logged at warning level
"""
normalized = _normalize_fee_profile(profile) normalized = _normalize_fee_profile(profile)
profile_map = {
"standard": 0.0026, if normalized == "api":
"maker_heavy": 0.0016, if request is None:
"taker_heavy": 0.0035, raise ValueError("api fee profile requires request context")
} store = request.app.state.store
repo = KrakenAccountSnapshotRepository(store)
latest = repo.latest_snapshot()
if latest is not None and latest.maker_fee is not None:
return latest.maker_fee
# Fallback to standard if no snapshot yet
return 0.0026
if normalized == "custom": if normalized == "custom":
if custom_fee_rate is None: if custom_fee_rate is None:
raise ValueError("custom fee profile requires custom_fee_rate") raise ValueError("custom fee profile requires custom_fee_rate")
if custom_fee_rate < 0.0: if custom_fee_rate < 0.0:
raise ValueError("custom_fee_rate must be >= 0") raise ValueError("custom_fee_rate must be >= 0")
return custom_fee_rate return custom_fee_rate
if normalized not in profile_map:
valid = ", ".join(sorted(list(profile_map.keys()) + ["custom"])) # Legacy hardcoded profiles (kept for backward compat, but soft-deprecated)
raise ValueError(f"fee_profile must be one of: {valid}") profile_map = {
return profile_map[normalized] "standard": 0.0026,
"maker_heavy": 0.0016,
"taker_heavy": 0.0035,
}
if normalized in profile_map:
return profile_map[normalized]
valid = ", ".join(sorted(list(profile_map.keys()) + ["api", "custom"]))
raise ValueError(f"fee_profile must be one of: {valid}")
def _parse_balances(raw: str) -> dict[str, float]: def _parse_balances(raw: str) -> dict[str, float]:
@@ -652,7 +679,7 @@ def _backtesting_panel_context(
"starting_balances": "USD=1000.0", "starting_balances": "USD=1000.0",
"trade_capital": "100.0", "trade_capital": "100.0",
"min_profit_threshold": "0.0005", "min_profit_threshold": "0.0005",
"fee_profile": "standard", "fee_profile": "api",
"custom_fee_rate": "", "custom_fee_rate": "",
"slippage_bps": "4.0", "slippage_bps": "4.0",
"execution_latency_ms": "20.0", "execution_latency_ms": "20.0",
@@ -687,7 +714,6 @@ async def _dashboard_response(
"overview_endpoint": "/dashboard/fragment/overview", "overview_endpoint": "/dashboard/fragment/overview",
"controls_endpoint": "/dashboard/fragment/controls", "controls_endpoint": "/dashboard/fragment/controls",
"charts_endpoint": "/dashboard/fragment/charts", "charts_endpoint": "/dashboard/fragment/charts",
"audit_endpoint": "/dashboard/fragment/audit",
"stream_endpoint": "/dashboard/stream/metrics", "stream_endpoint": "/dashboard/stream/metrics",
"overview_stream_endpoint": "/dashboard/stream/overview", "overview_stream_endpoint": "/dashboard/stream/overview",
}, },
@@ -763,6 +789,28 @@ async def dashboard_charts(request: Request) -> HTMLResponse:
) )
@router.get("/dashboard/audit", response_class=HTMLResponse)
async def dashboard_audit_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="audit.html",
context={
"title": "Arbitrade Audit Trail",
"request": request,
**_dashboard_audit(request),
},
)
@router.get("/dashboard/audit/fragment", response_class=HTMLResponse)
async def dashboard_audit_fragment(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="partials/audit.html",
context={"request": request, **_dashboard_audit(request)},
)
@router.get("/dashboard/fragment/audit", response_class=HTMLResponse) @router.get("/dashboard/fragment/audit", response_class=HTMLResponse)
async def dashboard_audit(request: Request) -> HTMLResponse: async def dashboard_audit(request: Request) -> HTMLResponse:
return templates.TemplateResponse( return templates.TemplateResponse(
@@ -903,7 +951,7 @@ async def dashboard_backtesting_run(request: Request) -> HTMLResponse:
"starting_balances": form.get("starting_balances", "USD=1000.0"), "starting_balances": form.get("starting_balances", "USD=1000.0"),
"trade_capital": form.get("trade_capital", "100.0"), "trade_capital": form.get("trade_capital", "100.0"),
"min_profit_threshold": form.get("min_profit_threshold", "0.0005"), "min_profit_threshold": form.get("min_profit_threshold", "0.0005"),
"fee_profile": _normalize_fee_profile(form.get("fee_profile", "standard")), "fee_profile": _normalize_fee_profile(form.get("fee_profile", "api")),
"custom_fee_rate": form.get("custom_fee_rate", ""), "custom_fee_rate": form.get("custom_fee_rate", ""),
"slippage_bps": form.get("slippage_bps", "4.0"), "slippage_bps": form.get("slippage_bps", "4.0"),
"execution_latency_ms": form.get("execution_latency_ms", "20.0"), "execution_latency_ms": form.get("execution_latency_ms", "20.0"),
@@ -924,7 +972,7 @@ async def dashboard_backtesting_run(request: Request) -> HTMLResponse:
) if defaults["custom_fee_rate"].strip() else None ) if defaults["custom_fee_rate"].strip() else None
) )
fee_rate = _fee_rate_for_profile( fee_rate = _fee_rate_for_profile(
defaults["fee_profile"], custom_fee_rate) defaults["fee_profile"], custom_fee_rate, request=request)
starting_balances = _parse_balances(defaults["starting_balances"]) starting_balances = _parse_balances(defaults["starting_balances"])
trade_capital = float(defaults["trade_capital"]) trade_capital = float(defaults["trade_capital"])
+1 -1
View File
@@ -56,7 +56,7 @@ class ReplayBookEvent:
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class BacktestConfig: class BacktestConfig:
fee_rate: float = 0.0026 fee_rate: float = 0.0 # 0.0 means "use API-sourced fee from kraken_account_snapshots"
min_profit_threshold: float = 0.0005 min_profit_threshold: float = 0.0005
trade_capital: float = 100.0 trade_capital: float = 100.0
quote_asset: str = "USD" quote_asset: str = "USD"
+3
View File
@@ -44,6 +44,7 @@ class ConfigPairFee(BaseModel):
market_type: str # 'crypto_crypto' or 'crypto_fiat' market_type: str # 'crypto_crypto' or 'crypto_fiat'
maker_fee_rate: float maker_fee_rate: float
taker_fee_rate: float taker_fee_rate: float
source: str = "manual" # 'manual' or 'kraken_api'
updated_at: datetime | None = None updated_at: datetime | None = None
@@ -247,6 +248,7 @@ class ConfigurationService:
"market_type": fee.market_type, "market_type": fee.market_type,
"maker_fee_rate": fee.maker_fee_rate, "maker_fee_rate": fee.maker_fee_rate,
"taker_fee_rate": fee.taker_fee_rate, "taker_fee_rate": fee.taker_fee_rate,
"source": fee.source,
"updated_at": fee.updated_at.isoformat() if fee.updated_at else None, "updated_at": fee.updated_at.isoformat() if fee.updated_at else None,
}) })
return result return result
@@ -282,6 +284,7 @@ class ConfigurationService:
"market_type": updated.market_type, "market_type": updated.market_type,
"maker_fee_rate": updated.maker_fee_rate, "maker_fee_rate": updated.maker_fee_rate,
"taker_fee_rate": updated.taker_fee_rate, "taker_fee_rate": updated.taker_fee_rate,
"source": updated.source,
"updated_at": updated.updated_at.isoformat() if updated.updated_at else None, "updated_at": updated.updated_at.isoformat() if updated.updated_at else None,
} }
+3 -2
View File
@@ -47,7 +47,7 @@ def _build_detector_and_books() -> tuple[IncrementalCycleDetector, dict[str, Ord
detector = IncrementalCycleDetector( detector = IncrementalCycleDetector(
index, index,
fee_rate=0.001, fee_rate=0.001, # synthetic benchmark: uses fixed rate, not API-sourced
min_profit_threshold=0.001, min_profit_threshold=0.001,
max_depth_levels=5, max_depth_levels=5,
max_book_age_seconds=10.0, max_book_age_seconds=10.0,
@@ -92,7 +92,8 @@ def run_incremental_detection_benchmark(
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Benchmark incremental detection latency") parser = argparse.ArgumentParser(
description="Benchmark incremental detection latency")
parser.add_argument("--iterations", type=int, default=50_000) parser.add_argument("--iterations", type=int, default=50_000)
parser.add_argument("--target-ms", type=float, default=1.0) parser.add_argument("--target-ms", type=float, default=1.0)
args = parser.parse_args() args = parser.parse_args()
+11 -2
View File
@@ -61,7 +61,8 @@ CREATE TABLE IF NOT EXISTS config_backtesting_defaults (
trade_capital DOUBLE, trade_capital DOUBLE,
min_profit_threshold DOUBLE, min_profit_threshold DOUBLE,
slippage_bps INTEGER, slippage_bps INTEGER,
execution_latency_ms INTEGER execution_latency_ms INTEGER,
fee_source VARCHAR DEFAULT 'api'
); );
CREATE TABLE IF NOT EXISTS opportunities ( CREATE TABLE IF NOT EXISTS opportunities (
@@ -159,7 +160,7 @@ CREATE TABLE IF NOT EXISTS kraken_account_snapshots (
class DuckDBStore: class DuckDBStore:
SCHEMA_VERSION = 3 SCHEMA_VERSION = 4
def __init__(self, settings: Settings) -> None: def __init__(self, settings: Settings) -> None:
self._db_path = Path(settings.duckdb_path) self._db_path = Path(settings.duckdb_path)
@@ -274,6 +275,14 @@ class DuckDBStore:
"INSERT OR IGNORE INTO schema_migrations (version) VALUES (3)") "INSERT OR IGNORE INTO schema_migrations (version) VALUES (3)")
_LOG.info("migration_applied", version=3) _LOG.info("migration_applied", version=3)
if current_version < 4:
# Migration v4: Add fee_source to backtesting defaults
conn.execute(
"ALTER TABLE config_backtesting_defaults ADD COLUMN IF NOT EXISTS fee_source VARCHAR DEFAULT 'api'")
conn.execute(
"INSERT OR IGNORE INTO schema_migrations (version) VALUES (4)")
_LOG.info("migration_applied", version=4)
# Update version to current # Update version to current
conn.execute( conn.execute(
f"INSERT OR REPLACE INTO schema_migrations (version, applied_at) " f"INSERT OR REPLACE INTO schema_migrations (version, applied_at) "
+17 -11
View File
@@ -738,15 +738,16 @@ class ConfigPairFeeRepository:
with self._store.connect() as conn: with self._store.connect() as conn:
cursor = conn.execute( cursor = conn.execute(
""" """
INSERT INTO config_pair_fees (pairing_id, market_type, maker_fee_rate, taker_fee_rate) INSERT INTO config_pair_fees (pairing_id, market_type, maker_fee_rate, taker_fee_rate, source)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
RETURNING pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at RETURNING pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, source
""", """,
( (
pair_fee.pairing_id, pair_fee.pairing_id,
pair_fee.market_type, pair_fee.market_type,
pair_fee.maker_fee_rate, pair_fee.maker_fee_rate,
pair_fee.taker_fee_rate, pair_fee.taker_fee_rate,
pair_fee.source,
), ),
) )
row = cursor.fetchone() row = cursor.fetchone()
@@ -756,7 +757,8 @@ class ConfigPairFeeRepository:
market_type=row[1], market_type=row[1],
maker_fee_rate=row[2], maker_fee_rate=row[2],
taker_fee_rate=row[3], taker_fee_rate=row[3],
updated_at=row[4] updated_at=row[4],
source=row[5],
) )
raise ValueError("Failed to create pair fee") raise ValueError("Failed to create pair fee")
@@ -765,7 +767,7 @@ class ConfigPairFeeRepository:
with self._store.connect() as conn: with self._store.connect() as conn:
cursor = conn.execute( cursor = conn.execute(
""" """
SELECT pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at SELECT pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, COALESCE(source, 'manual') AS source
FROM config_pair_fees FROM config_pair_fees
WHERE pairing_id = ? AND market_type = ? WHERE pairing_id = ? AND market_type = ?
""", """,
@@ -778,7 +780,8 @@ class ConfigPairFeeRepository:
market_type=row[1], market_type=row[1],
maker_fee_rate=row[2], maker_fee_rate=row[2],
taker_fee_rate=row[3], taker_fee_rate=row[3],
updated_at=row[4] updated_at=row[4],
source=row[5],
) )
return None return None
@@ -788,13 +791,14 @@ class ConfigPairFeeRepository:
cursor = conn.execute( cursor = conn.execute(
""" """
UPDATE config_pair_fees UPDATE config_pair_fees
SET maker_fee_rate = ?, taker_fee_rate = ? SET maker_fee_rate = ?, taker_fee_rate = ?, source = ?
WHERE pairing_id = ? AND market_type = ? WHERE pairing_id = ? AND market_type = ?
RETURNING pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at RETURNING pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, source
""", """,
( (
pair_fee.maker_fee_rate, pair_fee.maker_fee_rate,
pair_fee.taker_fee_rate, pair_fee.taker_fee_rate,
pair_fee.source,
pairing_id, pairing_id,
market_type, market_type,
), ),
@@ -806,7 +810,8 @@ class ConfigPairFeeRepository:
market_type=row[1], market_type=row[1],
maker_fee_rate=row[2], maker_fee_rate=row[2],
taker_fee_rate=row[3], taker_fee_rate=row[3],
updated_at=row[4] updated_at=row[4],
source=row[5],
) )
raise ValueError("Failed to update pair fee") raise ValueError("Failed to update pair fee")
@@ -827,7 +832,7 @@ class ConfigPairFeeRepository:
with self._store.connect() as conn: with self._store.connect() as conn:
cursor = conn.execute( cursor = conn.execute(
""" """
SELECT pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at SELECT pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, COALESCE(source, 'manual') AS source
FROM config_pair_fees FROM config_pair_fees
WHERE pairing_id = ? WHERE pairing_id = ?
ORDER BY market_type ORDER BY market_type
@@ -840,7 +845,8 @@ class ConfigPairFeeRepository:
market_type=row[1], market_type=row[1],
maker_fee_rate=row[2], maker_fee_rate=row[2],
taker_fee_rate=row[3], taker_fee_rate=row[3],
updated_at=row[4] updated_at=row[4],
source=row[5],
) )
for row in cursor.fetchall() for row in cursor.fetchall()
] ]
+27
View File
@@ -0,0 +1,27 @@
{% extends "base.html" %} {% block title %}Audit Trail{% endblock %} {% block
main_class %}shell{% endblock %} {% block content %}
<section class="hero">
<div>
<h1 class="title">Audit Trail</h1>
<p class="subtitle">
System activity, configuration changes, and execution decisions.
</p>
</div>
<div class="toolbar">
<a class="button secondary" href="/dashboard">Dashboard</a>
<a class="button secondary" href="/dashboard/config">Config</a>
<a class="button secondary" href="/dashboard/backtesting">Backtesting</a>
<a class="button secondary" href="/health">Health</a>
</div>
</section>
<section
id="audit-shell"
hx-get="/dashboard/audit/fragment"
hx-target="this"
hx-trigger="load, every 20s"
hx-swap="outerHTML"
>
{% include "partials/audit.html" %}
</section>
{% endblock %}
+2 -3
View File
@@ -14,9 +14,8 @@
color: #e5eefb; color: #e5eefb;
} }
.shell { .shell {
max-width: 1120px; max-width: none;
margin: 0 auto; padding: 24px 32px 48px;
padding: 32px 20px 48px;
} }
.hero { .hero {
display: flex; display: flex;
+1
View File
@@ -10,6 +10,7 @@ main_class %}shell{% endblock %} {% block content %}
<div class="toolbar"> <div class="toolbar">
<a class="button secondary" href="/dashboard">Dashboard</a> <a class="button secondary" href="/dashboard">Dashboard</a>
<a class="button secondary" href="/dashboard/backtesting">Backtesting</a> <a class="button secondary" href="/dashboard/backtesting">Backtesting</a>
<a class="button secondary" href="/dashboard/audit">Audit</a>
<a class="button secondary" href="/health">Health</a> <a class="button secondary" href="/health">Health</a>
</div> </div>
</section> </section>
+1 -9
View File
@@ -20,6 +20,7 @@ head_scripts %}
<a class="button secondary" href="/health">Health</a> <a class="button secondary" href="/health">Health</a>
<a class="button secondary" href="/dashboard/config">Config</a> <a class="button secondary" href="/dashboard/config">Config</a>
<a class="button secondary" href="/dashboard/backtesting">Backtesting</a> <a class="button secondary" href="/dashboard/backtesting">Backtesting</a>
<a class="button secondary" href="/dashboard/audit">Audit</a>
</div> </div>
</section> </section>
@@ -63,15 +64,6 @@ head_scripts %}
{% include "partials/charts.html" %} {% include "partials/charts.html" %}
</section> </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 %} {% endblock %} {% block scripts %}
<script> <script>
window.arbitradeRenderCharts = (payload) => { window.arbitradeRenderCharts = (payload) => {
@@ -83,6 +83,8 @@
<label class="field"> <label class="field">
<span>Fee profile</span> <span>Fee profile</span>
<select name="fee_profile"> <select name="fee_profile">
{% set sel = "selected" if fee_profile == "api" else "" %}
<option value="api" {{ sel }}>api (from Kraken)</option>
{% set sel = "selected" if fee_profile == "standard" else "" %} {% set sel = "selected" if fee_profile == "standard" else "" %}
<option value="standard" {{ sel }}>standard</option> <option value="standard" {{ sel }}>standard</option>
{% set sel = "selected" if fee_profile == "maker_heavy" else "" %} {% set sel = "selected" if fee_profile == "maker_heavy" else "" %}
@@ -58,6 +58,9 @@
<th style="text-align: left; padding: 10px; color: #9fb2d0"> <th style="text-align: left; padding: 10px; color: #9fb2d0">
Actions Actions
</th> </th>
<th style="text-align: left; padding: 10px; color: #9fb2d0">
Source
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -143,6 +146,9 @@
</button> </button>
</form> </form>
</td> </td>
<td style="padding: 10px; color: #7f95b7">
{{ fee.source if fee.source else "manual" }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -191,7 +197,7 @@
type="number" type="number"
min="0" min="0"
step="0.0001" step="0.0001"
placeholder="0.0016" placeholder="API default"
/> />
</label> </label>
<label class="field"> <label class="field">
@@ -201,7 +207,7 @@
type="number" type="number"
min="0" min="0"
step="0.0001" step="0.0001"
placeholder="0.0026" placeholder="API default"
/> />
</label> </label>
<label class="field"> <label class="field">
@@ -75,97 +75,5 @@
</form> </form>
</div> </div>
</article> </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>
</div> </div>
@@ -19,7 +19,9 @@
<article class="card"> <article class="card">
<div class="label">Fee Tier</div> <div class="label">Fee Tier</div>
<div class="value">{{ fee_tier }}</div> <div class="value">{{ fee_tier }}</div>
<div class="meta">Maker {{ maker_fee }} / Taker {{ taker_fee }}</div> <div class="meta">
Maker {{ maker_fee }} / Taker {{ taker_fee }} &middot; {{ fee_source }}
</div>
</article> </article>
</div> </div>