mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-16 19:48:06 +00:00
Add web dashboard, historical data loader, and backtest reporting
- Add Flask dashboard with status, config editor, logs, kill switch, and backtest results pages with monthly P&L breakdowns - Add historical_loader.py for paginated OANDA candle fetching (1yr+) - Update backtester to $100k starting equity, monthly P&L computation, and JSON summary output for dashboard display - Update config to EUR_USD only on M5/M15 with SMA 21/50 strategy - Add backtest.html template with performance metrics and bar charts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ef950f25dd
commit
212f581d01
@@ -0,0 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}fx-quant — Backtest Results{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4">Backtest Results</h4>
|
||||
|
||||
{% if not summaries %}
|
||||
<div class="alert alert-info">No backtest results found. Run the backtester first.</div>
|
||||
{% endif %}
|
||||
|
||||
{% for s in summaries %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<strong>{{ s.instrument }} / {{ s.granularity }}</strong>
|
||||
<span class="text-muted float-end">Run: {{ s.run_time[:19] }} | {{ s.data_range.bars | default(0) }} bars ({{ s.data_range.start[:10] }} to {{ s.data_range.end[:10] }})</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Summary Metrics -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 text-center">
|
||||
<div class="text-muted small">Starting Equity</div>
|
||||
<div class="fs-5">${{ "{:,.2f}".format(s.metrics.starting_equity) }}</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<div class="text-muted small">Final Equity</div>
|
||||
<div class="fs-5 {% if s.metrics.final_equity >= s.metrics.starting_equity %}text-success{% else %}text-danger{% endif %}">
|
||||
${{ "{:,.2f}".format(s.metrics.final_equity) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<div class="text-muted small">Total Return</div>
|
||||
<div class="fs-5 {% if s.metrics.total_return_pct >= 0 %}text-success{% else %}text-danger{% endif %}">
|
||||
{{ "{:+.2f}".format(s.metrics.total_return_pct) }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<div class="text-muted small">Max Drawdown</div>
|
||||
<div class="fs-5 text-danger">{{ "{:.2f}".format(s.metrics.max_drawdown_pct) }}%</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<div class="text-muted small">Win Rate</div>
|
||||
<div class="fs-5">{{ "{:.1f}".format(s.metrics.win_rate_pct) }}%</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<div class="text-muted small">Sharpe</div>
|
||||
<div class="fs-5">{{ "{:.2f}".format(s.metrics.sharpe_ratio) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted small">Trades:</span> {{ s.metrics.num_trades }} ({{ s.metrics.round_trips }} round-trips)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monthly P&L Table -->
|
||||
<h6>Monthly P&L</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
<th class="text-end">Start Equity</th>
|
||||
<th class="text-end">End Equity</th>
|
||||
<th class="text-end">P&L ($)</th>
|
||||
<th class="text-end">P&L (%)</th>
|
||||
<th style="width:30%">Performance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in s.monthly_pnl %}
|
||||
<tr>
|
||||
<td><strong>{{ m.month }}</strong></td>
|
||||
<td class="text-end">${{ "{:,.2f}".format(m.start_equity) }}</td>
|
||||
<td class="text-end">${{ "{:,.2f}".format(m.end_equity) }}</td>
|
||||
<td class="text-end {% if m.pnl >= 0 %}text-success{% else %}text-danger{% endif %}">
|
||||
{{ "{:+,.2f}".format(m.pnl) }}
|
||||
</td>
|
||||
<td class="text-end {% if m.pnl_pct >= 0 %}text-success{% else %}text-danger{% endif %}">
|
||||
{{ "{:+.2f}".format(m.pnl_pct) }}%
|
||||
</td>
|
||||
<td>
|
||||
{% set bar_width = [m.pnl_pct | abs * 10, 100] | min %}
|
||||
{% if m.pnl >= 0 %}
|
||||
<div class="progress" style="height: 18px;">
|
||||
<div class="progress-bar bg-success" style="width: {{ bar_width }}%"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="progress" style="height: 18px;">
|
||||
<div class="progress-bar bg-danger" style="width: {{ bar_width }}%"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}fx-quant{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YcnS/1p0TIMQ77Kv3OSpcXkig1hYZLRFss0K"
|
||||
crossorigin="anonymous">
|
||||
<style>
|
||||
body { background: #f8f9fa; }
|
||||
.navbar-brand { font-weight: 700; letter-spacing: 1px; }
|
||||
.card { margin-bottom: 1rem; }
|
||||
.table-sm td, .table-sm th { font-size: 0.85rem; }
|
||||
.kill-active { background: #dc3545; color: #fff; padding: 4px 10px; border-radius: 4px; }
|
||||
.kill-inactive { background: #198754; color: #fff; padding: 4px 10px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">fx-quant</a>
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link" href="/">Status</a>
|
||||
<a class="nav-link" href="/backtest">Backtest</a>
|
||||
<a class="nav-link" href="/config">Config</a>
|
||||
<a class="nav-link" href="/logs">Logs</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,188 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}fx-quant — Config{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-3">Configuration Editor</h4>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
|
||||
<!-- Instruments & Data -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Instruments & Data</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Instruments (comma-separated)</label>
|
||||
<input type="text" class="form-control" name="instruments"
|
||||
value="{{ cfg.brokers[0].instruments | join(', ') }}">
|
||||
<small class="text-muted">Valid: {{ valid_instruments[:8] | join(', ') }}, ...</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Granularities (comma-separated)</label>
|
||||
<input type="text" class="form-control" name="granularities"
|
||||
value="{{ cfg.data.candle_granularities | join(', ') }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Candle Count</label>
|
||||
<input type="number" class="form-control" name="candle_count"
|
||||
value="{{ cfg.data.candle_count }}" min="50" max="5000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Strategy</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Rule</label>
|
||||
<input type="text" class="form-control" name="strategy_rule"
|
||||
value="{{ cfg.strategy.rule }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">SMA Short</label>
|
||||
<input type="number" class="form-control" name="sma_short"
|
||||
value="{{ cfg.strategy.params.short }}" min="1">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">SMA Long</label>
|
||||
<input type="number" class="form-control" name="sma_long"
|
||||
value="{{ cfg.strategy.params.long }}" min="2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Trade Size (% of equity)</label>
|
||||
<input type="number" class="form-control" name="trade_size_pct"
|
||||
value="{{ cfg.strategy.trade_size_pct_of_equity }}" step="0.001" min="0.001" max="0.1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Drawdown (%)</label>
|
||||
<input type="number" class="form-control" name="max_drawdown_pct"
|
||||
value="{{ cfg.strategy.max_drawdown_pct }}" step="0.01" min="0.01" max="0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<!-- Features -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Features</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMA Windows (comma-separated)</label>
|
||||
<input type="text" class="form-control" name="sma_windows"
|
||||
value="{{ cfg.features.sma_windows | join(', ') }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">EMA Windows (comma-separated)</label>
|
||||
<input type="text" class="form-control" name="ema_windows"
|
||||
value="{{ cfg.features.ema_windows | join(', ') }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">RSI Period</label>
|
||||
<input type="number" class="form-control" name="rsi_period"
|
||||
value="{{ cfg.features.rsi_period }}" min="2">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">ATR Period</label>
|
||||
<input type="number" class="form-control" name="atr_period"
|
||||
value="{{ cfg.features.atr_period }}" min="2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">VWAP Window</label>
|
||||
<input type="number" class="form-control" name="vwap_window"
|
||||
value="{{ cfg.features.vwap_window }}" min="2">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Volatility Window</label>
|
||||
<input type="number" class="form-control" name="volatility_window"
|
||||
value="{{ cfg.features.volatility_window }}" min="2">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Settings -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>AI Settings</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Confidence Threshold</label>
|
||||
<input type="number" class="form-control" name="confidence_threshold"
|
||||
value="{{ cfg.ai.confidence_threshold }}" step="0.01" min="0" max="1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Ensemble Models (comma-separated)</label>
|
||||
<input type="text" class="form-control" name="ensemble_models"
|
||||
value="{{ cfg.ai.ensemble_models | join(', ') }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 mb-3">
|
||||
<label class="form-label">RSI Overbought</label>
|
||||
<input type="number" class="form-control" name="rsi_overbought"
|
||||
value="{{ cfg.ai.sanity_checks.rsi_overbought }}" min="50" max="100">
|
||||
</div>
|
||||
<div class="col-4 mb-3">
|
||||
<label class="form-label">RSI Oversold</label>
|
||||
<input type="number" class="form-control" name="rsi_oversold"
|
||||
value="{{ cfg.ai.sanity_checks.rsi_oversold }}" min="0" max="50">
|
||||
</div>
|
||||
<div class="col-4 mb-3">
|
||||
<label class="form-label">Vol Multiplier</label>
|
||||
<input type="number" class="form-control" name="volatility_multiplier"
|
||||
value="{{ cfg.ai.sanity_checks.volatility_multiplier }}" step="0.1" min="1" max="10">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<!-- Execution -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Execution</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="paper_mode" id="paper_mode"
|
||||
{{ "checked" if cfg.execution.paper_mode }}>
|
||||
<label class="form-check-label" for="paper_mode">Paper Mode</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Canary Size (%)</label>
|
||||
<input type="number" class="form-control" name="canary_size_pct"
|
||||
value="{{ cfg.execution.canary_size_pct }}" step="0.001" min="0.001" max="0.1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Positions</label>
|
||||
<input type="number" class="form-control" name="max_positions"
|
||||
value="{{ cfg.execution.max_positions }}" min="1" max="20">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Interval (seconds)</label>
|
||||
<input type="number" class="form-control" name="interval_seconds"
|
||||
value="{{ cfg.execution.interval_seconds }}" min="10" max="3600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mb-4">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Save & Signal Reload</button>
|
||||
<a href="/config" class="btn btn-secondary btn-lg ms-2">Reset</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}fx-quant — Status{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<!-- Mode & Kill Switch -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Trading Status</strong></div>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless mb-3">
|
||||
<tr>
|
||||
<td>Mode</td>
|
||||
<td><strong>{{ "PAPER" if cfg.execution.paper_mode else "LIVE" }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Strategy</td>
|
||||
<td>{{ cfg.strategy.rule }} ({{ cfg.strategy.params.short }}/{{ cfg.strategy.params.long }})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instruments</td>
|
||||
<td>{{ cfg.brokers[0].instruments | join(", ") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Granularity</td>
|
||||
<td>{{ cfg.data.candle_granularities | join(", ") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Loop Interval</td>
|
||||
<td>{{ cfg.execution.interval_seconds }}s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max Positions</td>
|
||||
<td>{{ cfg.execution.max_positions }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AI Confidence</td>
|
||||
<td>{{ cfg.ai.confidence_threshold }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kill Switch -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Kill Switch</strong></div>
|
||||
<div class="card-body text-center">
|
||||
{% if kill_active %}
|
||||
<p class="kill-active d-inline-block mb-3">KILL SWITCH ACTIVE</p>
|
||||
<form method="post" action="/killswitch">
|
||||
<input type="hidden" name="action" value="deactivate">
|
||||
<button class="btn btn-success btn-lg" onclick="return confirm('Resume trading?')">
|
||||
Deactivate Kill Switch
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="kill-inactive d-inline-block mb-3">Trading Active</p>
|
||||
<form method="post" action="/killswitch">
|
||||
<input type="hidden" name="action" value="activate">
|
||||
<button class="btn btn-danger btn-lg" onclick="return confirm('STOP ALL TRADING?')">
|
||||
Activate Kill Switch
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><strong>Recent Activity</strong> (last 10 orders)</div>
|
||||
<div class="card-body p-0">
|
||||
{% if recent_orders %}
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th><th>Instrument</th><th>Side</th><th>Units</th>
|
||||
<th>Price</th><th>Mode</th><th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in recent_orders | reverse %}
|
||||
<tr>
|
||||
<td>{{ row[0][:19] }}</td>
|
||||
<td>{{ row[1] }}</td>
|
||||
<td>{{ row[2] }}</td>
|
||||
<td>{{ row[3] }}</td>
|
||||
<td>{{ row[4] }}</td>
|
||||
<td>{{ row[6] }}</td>
|
||||
<td>{{ row[7] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="p-3 text-muted mb-0">No orders yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}fx-quant — Logs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#orders" type="button">
|
||||
Order Log ({{ order_rows | length }})
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#ai" type="button">
|
||||
AI Decisions ({{ ai_rows | length }})
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Order Log -->
|
||||
<div class="tab-pane fade show active" id="orders">
|
||||
{% if order_rows %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>{% for h in order_headers %}<th>{{ h }}</th>{% endfor %}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in order_rows %}
|
||||
<tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No order log entries.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- AI Decisions -->
|
||||
<div class="tab-pane fade" id="ai">
|
||||
{% if ai_rows %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>{% for h in ai_headers %}<th>{{ h }}</th>{% endfor %}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in ai_rows %}
|
||||
<tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No AI decision entries.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user