mirror of
https://github.com/umaiskhan-ops/ApexFX-High-Fidelity-Quant-Ecosystem..git
synced 2026-08-01 12:57:45 +00:00
1847 lines
95 KiB
HTML
1847 lines
95 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ApexFX Dashboard</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
|
<style>
|
|
.symbol-card { background-color: #1f2937; padding: 1rem; border-radius: 0.5rem; margin-bottom: 1rem; }
|
|
.kpi-value { font-size: 1.5rem; line-height: 2rem; font-weight: 600; }
|
|
.kpi-label { font-size: 0.875rem; line-height: 1.25rem; color: #9ca3af; }
|
|
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
|
.grid-4 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
|
@media (min-width: 768px) {
|
|
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
}
|
|
.chart-box { max-height: 180px; overflow: hidden; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-900 text-gray-100">
|
|
<div class="container mx-auto p-4">
|
|
<!-- Header & Connection -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="text-3xl font-bold text-blue-500">ApexFX Multi-Symbol Terminal</div>
|
|
<div class="flex items-center space-x-4">
|
|
<div id="status" class="w-4 h-4 rounded-full bg-red-500"></div>
|
|
<div id="statusText" class="text-sm font-medium">disconnected</div>
|
|
<div id="heartbeatText" class="text-xs font-mono text-gray-400">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Config / Login -->
|
|
<div class="bg-gray-800 p-4 rounded mb-6 border border-gray-700">
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<input id="login" type="number" placeholder="MT5 Login" class="bg-gray-700 p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none">
|
|
<input id="password" type="password" placeholder="MT5 Password" class="bg-gray-700 p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none">
|
|
<input id="server" type="text" placeholder="Broker Server" class="bg-gray-700 p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none">
|
|
<button id="submit" class="bg-blue-600 hover:bg-blue-700 transition px-4 py-2 rounded font-semibold">Update Credentials</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Account KPIs -->
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
<div class="bg-blue-900 bg-opacity-30 p-4 rounded border border-blue-800">
|
|
<div class="kpi-label">Account Balance</div>
|
|
<div id="kpi-balance" class="kpi-value">—</div>
|
|
</div>
|
|
<div class="bg-indigo-900 bg-opacity-30 p-4 rounded border border-indigo-800">
|
|
<div class="kpi-label">Account Equity</div>
|
|
<div id="kpi-equity" class="kpi-value">—</div>
|
|
</div>
|
|
<div class="bg-green-900 bg-opacity-30 p-4 rounded border border-green-800">
|
|
<div class="kpi-label">Free Margin</div>
|
|
<div id="kpi-free-margin" class="kpi-value">—</div>
|
|
</div>
|
|
<div class="bg-purple-900 bg-opacity-30 p-4 rounded border border-purple-800">
|
|
<div class="kpi-label">Margin Level</div>
|
|
<div id="kpi-margin-level" class="kpi-value">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Multi-Symbol Main Section -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
<!-- EURUSDm Section -->
|
|
<div class="symbol-section" id="section-EURUSDm">
|
|
<div class="flex items-center justify-between mb-4 border-b border-gray-700 pb-2">
|
|
<h2 class="text-xl font-bold text-green-400">EURUSDm</h2>
|
|
<div class="flex space-x-2">
|
|
<label class="flex items-center space-x-1 text-xs text-gray-400"><input type="checkbox" checked class="toggle-tick" data-symbol="EURUSDm"><span>Tick</span></label>
|
|
<label class="flex items-center space-x-1 text-xs text-gray-400"><input type="checkbox" class="toggle-m1" data-symbol="EURUSDm"><span>M1</span></label>
|
|
<label class="flex items-center space-x-1 text-xs text-gray-400"><input type="checkbox" class="toggle-m5" data-symbol="EURUSDm"><span>M5</span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price Chart -->
|
|
<div class="bg-gray-800 p-2 rounded mb-4">
|
|
<canvas id="chart-EURUSDm" height="150"></canvas>
|
|
</div>
|
|
<div id="stale-banner-EURUSDm" class="hidden bg-red-700 text-red-100 p-2 rounded mb-2 text-xs">Feed stalled for EURUSDm</div>
|
|
<div class="bg-gray-800 p-2 rounded mb-4 chart-box">
|
|
<div class="text-sm text-gray-300">EURUSDm Equity</div>
|
|
<canvas id="eq-EURUSDm" height="120"></canvas>
|
|
</div>
|
|
<div class="bg-gray-800 p-2 rounded mb-4 chart-box">
|
|
<div class="text-sm text-gray-300">EURUSDm Volume Profile</div>
|
|
<canvas id="vp-EURUSDm" height="200"></canvas>
|
|
</div>
|
|
|
|
<!-- Signal & Decision -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Signal</div>
|
|
<div id="signal-name-EURUSDm" class="text-xl font-semibold">—</div>
|
|
<div id="signal-conf-EURUSDm" class="text-xs text-gray-400">—</div>
|
|
</div>
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Decision</div>
|
|
<div id="decision-name-EURUSDm" class="text-xl font-semibold">—</div>
|
|
<div id="decision-approved-EURUSDm" class="text-xs font-bold text-red-500">—</div>
|
|
<div id="decision-reason-EURUSDm" class="text-xs text-gray-400 mt-1">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Regime -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Regime</div>
|
|
<div id="regime-name-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Volatility</div>
|
|
<div id="feat-vol-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Per-Symbol PnL -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Symbol PnL</div>
|
|
<div id="pnl-val-EURUSDm" class="text-lg font-bold">—</div>
|
|
</div>
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Win/Loss</div>
|
|
<div id="pnl-wl-EURUSDm" class="text-lg font-bold">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-3 md:grid-cols-6 gap-4 mb-4">
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">ADX(14)</div>
|
|
<div id="feat-adx-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">MACD</div>
|
|
<div id="feat-macd-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">MACD Sig</div>
|
|
<div id="feat-macd-sig-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">MACD Hist</div>
|
|
<div id="feat-macd-hist-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">BB Width</div>
|
|
<div id="feat-bb-width-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">BB Pos</div>
|
|
<div id="feat-bb-pos-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-3 md:grid-cols-6 gap-4 mb-4">
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Max Trade Size</div>
|
|
<div id="risk-size-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Stop-Loss</div>
|
|
<div id="risk-sl-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Take-Profit</div>
|
|
<div id="risk-tp-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Drawdown Block</div>
|
|
<div id="risk-dd-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Risk Regime</div>
|
|
<div id="risk-regime-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Adjustments</div>
|
|
<div id="risk-adj-EURUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-4">
|
|
<div class="text-lg font-semibold mb-2">EURUSDm Blotter</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="text-gray-400">
|
|
<th class="px-2 py-1 text-left">Time</th>
|
|
<th class="px-2 py-1 text-left">Action</th>
|
|
<th class="px-2 py-1 text-right">Lot</th>
|
|
<th class="px-2 py-1 text-right">Price</th>
|
|
<th class="px-2 py-1 text-right">SL</th>
|
|
<th class="px-2 py-1 text-right">TP</th>
|
|
<th class="px-2 py-1 text-right">PnL</th>
|
|
<th class="px-2 py-1 text-left">Regime</th>
|
|
<th class="px-2 py-1 text-left">Status</th>
|
|
<th class="px-2 py-1 text-left">ID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="trade-blotter-EURUSDm" class="text-gray-200"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- XAUUSDm Section -->
|
|
<div class="symbol-section" id="section-XAUUSDm">
|
|
<div class="flex items-center justify-between mb-4 border-b border-gray-700 pb-2">
|
|
<h2 class="text-xl font-bold text-yellow-400">XAUUSDm</h2>
|
|
<div class="flex space-x-2">
|
|
<label class="flex items-center space-x-1 text-xs text-gray-400"><input type="checkbox" checked class="toggle-tick" data-symbol="XAUUSDm"><span>Tick</span></label>
|
|
<label class="flex items-center space-x-1 text-xs text-gray-400"><input type="checkbox" class="toggle-m1" data-symbol="XAUUSDm"><span>M1</span></label>
|
|
<label class="flex items-center space-x-1 text-xs text-gray-400"><input type="checkbox" class="toggle-m5" data-symbol="XAUUSDm"><span>M5</span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price Chart -->
|
|
<div class="bg-gray-800 p-2 rounded mb-4">
|
|
<canvas id="chart-XAUUSDm" height="150"></canvas>
|
|
</div>
|
|
<div id="stale-banner-XAUUSDm" class="hidden bg-red-700 text-red-100 p-2 rounded mb-2 text-xs">Feed stalled for XAUUSDm</div>
|
|
<div class="bg-gray-800 p-2 rounded mb-4 chart-box">
|
|
<div class="text-sm text-gray-300">XAUUSDm Equity</div>
|
|
<canvas id="eq-XAUUSDm" height="120"></canvas>
|
|
</div>
|
|
<div class="bg-gray-800 p-2 rounded mb-4 chart-box">
|
|
<div class="text-sm text-gray-300">XAUUSDm Volume Profile</div>
|
|
<canvas id="vp-XAUUSDm" height="200"></canvas>
|
|
</div>
|
|
|
|
<!-- Signal & Decision -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Signal</div>
|
|
<div id="signal-name-XAUUSDm" class="text-xl font-semibold">—</div>
|
|
<div id="signal-conf-XAUUSDm" class="text-xs text-gray-400">—</div>
|
|
</div>
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Decision</div>
|
|
<div id="decision-name-XAUUSDm" class="text-xl font-semibold">—</div>
|
|
<div id="decision-approved-XAUUSDm" class="text-xs font-bold text-red-500">—</div>
|
|
<div id="decision-reason-XAUUSDm" class="text-xs text-gray-400 mt-1">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Regime -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Regime</div>
|
|
<div id="regime-name-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Volatility</div>
|
|
<div id="feat-vol-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Per-Symbol PnL -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Symbol PnL</div>
|
|
<div id="pnl-val-XAUUSDm" class="text-lg font-bold">—</div>
|
|
</div>
|
|
<div class="bg-gray-700 p-3 rounded">
|
|
<div class="kpi-label">Win/Loss</div>
|
|
<div id="pnl-wl-XAUUSDm" class="text-lg font-bold">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-3 md:grid-cols-6 gap-4 mb-4">
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">ADX(14)</div>
|
|
<div id="feat-adx-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">MACD</div>
|
|
<div id="feat-macd-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">MACD Sig</div>
|
|
<div id="feat-macd-sig-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">MACD Hist</div>
|
|
<div id="feat-macd-hist-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">BB Width</div>
|
|
<div id="feat-bb-width-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">BB Pos</div>
|
|
<div id="feat-bb-pos-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-3 md:grid-cols-6 gap-4 mb-4">
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Max Trade Size</div>
|
|
<div id="risk-size-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Stop-Loss</div>
|
|
<div id="risk-sl-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Take-Profit</div>
|
|
<div id="risk-tp-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Drawdown Block</div>
|
|
<div id="risk-dd-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Risk Regime</div>
|
|
<div id="risk-regime-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded">
|
|
<div class="kpi-label">Adjustments</div>
|
|
<div id="risk-adj-XAUUSDm" class="text-lg font-medium">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-4">
|
|
<div class="text-lg font-semibold mb-2">XAUUSDm Blotter</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="text-gray-400">
|
|
<th class="px-2 py-1 text-left">Time</th>
|
|
<th class="px-2 py-1 text-left">Action</th>
|
|
<th class="px-2 py-1 text-right">Lot</th>
|
|
<th class="px-2 py-1 text-right">Price</th>
|
|
<th class="px-2 py-1 text-right">SL</th>
|
|
<th class="px-2 py-1 text-right">TP</th>
|
|
<th class="px-2 py-1 text-right">PnL</th>
|
|
<th class="px-2 py-1 text-left">Regime</th>
|
|
<th class="px-2 py-1 text-left">Status</th>
|
|
<th class="px-2 py-1 text-left">ID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="trade-blotter-XAUUSDm" class="text-gray-200"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Portfolio Optimizer Panels -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
|
<div class="lg:col-span-2 bg-gray-800 p-4 rounded border border-gray-700">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="text-lg font-semibold">Efficient Frontier</div>
|
|
<div class="text-xs text-gray-400">Risk (Volatility) vs Return</div>
|
|
</div>
|
|
<div id="efficientFrontierPanel" class="chart-box">
|
|
<canvas id="efficient-frontier" height="200"></canvas>
|
|
</div>
|
|
<div class="text-xs text-gray-400 mt-2">
|
|
Highlighted: <span class="font-bold text-blue-400">Max Sharpe</span>, <span class="font-bold text-green-400">Min Variance</span>, <span class="font-bold text-yellow-400">Risk Parity</span>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded border border-gray-700">
|
|
<div class="text-lg font-semibold mb-2">Method Comparison</div>
|
|
<div id="methodComparisonTable" class="overflow-x-auto">
|
|
<table class="min-w-full text-xs" id="method-table">
|
|
<thead>
|
|
<tr class="text-gray-400 bg-gray-900 border-b border-gray-700">
|
|
<th class="px-2 py-2 text-left cursor-pointer" data-sort="method">Method</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="er">Exp Ret</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="vol">Vol</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="sharpe">Sharpe</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="mdd">Max DD</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="dr">Div Ratio</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="hhi">Conc Idx</th>
|
|
<th class="px-2 py-2 text-right cursor-pointer" data-sort="ena">ENAs</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="method-table-body" class="text-gray-300 divide-y divide-gray-700"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-2">
|
|
<input id="method-filter" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" placeholder="Filter by method name">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Portfolio Health Panel -->
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-gray-700">
|
|
<div class="text-xl font-bold mb-2">Portfolio Health</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-3">
|
|
<div class="md:col-span-3">
|
|
<label class="text-sm text-gray-400 block mb-1">Select Optimizer Method</label>
|
|
<select id="portfolioMethodDropdown" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option value="equal_weight">equal_weight</option>
|
|
<option value="min_variance">min_variance</option>
|
|
<option value="max_sharpe">max_sharpe</option>
|
|
<option value="risk_parity">risk_parity</option>
|
|
<option value="max_diversification">max_diversification</option>
|
|
<option value="hrp">hrp</option>
|
|
<option value="black_litterman">black_litterman</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400">Selected Method</div>
|
|
<div id="ph-method" class="text-lg font-semibold">—</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400">Diversification Ratio</div>
|
|
<div id="ph-dr" class="text-lg font-semibold">—</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400">Effective # Assets</div>
|
|
<div id="ph-ena" class="text-lg font-semibold">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-1">Target Weights</div>
|
|
<table class="min-w-full text-xs">
|
|
<thead>
|
|
<tr class="text-gray-400 bg-gray-900 border-b border-gray-700"><th class="px-2 py-1 text-left">Symbol</th><th class="px-2 py-1 text-right">Weight</th></tr>
|
|
</thead>
|
|
<tbody id="ph-weights" class="text-gray-300 divide-y divide-gray-700"></tbody>
|
|
</table>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-1">Risk Contributions</div>
|
|
<table class="min-w-full text-xs">
|
|
<thead>
|
|
<tr class="text-gray-400 bg-gray-900 border-b border-gray-700"><th class="px-2 py-1 text-left">Symbol</th><th class="px-2 py-1 text-right">Contribution</th></tr>
|
|
</thead>
|
|
<tbody id="ph-rc" class="text-gray-300 divide-y divide-gray-700"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Consolidated Analytics Section -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
|
<!-- Equity Curve -->
|
|
<div class="lg:col-span-2 bg-gray-800 p-4 rounded">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="text-lg font-semibold">Combined Equity Performance</div>
|
|
<label class="flex items-center space-x-2 text-sm text-gray-400">
|
|
<input id="toggle-realized-equity" type="checkbox" class="form-checkbox">
|
|
<span>Realized PnL Only</span>
|
|
</label>
|
|
</div>
|
|
<div class="chart-box">
|
|
<canvas id="equity-curve" height="150"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PnL Metrics -->
|
|
<div class="bg-gray-800 p-4 rounded">
|
|
<div class="text-lg font-semibold mb-4 text-center">Consolidated Stats</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="text-center">
|
|
<div class="kpi-label">Total Win/Loss</div>
|
|
<div id="pnl-wl" class="text-xl font-bold">—</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="kpi-label">Net PnL</div>
|
|
<div id="pnl-avg" class="text-xl font-bold">—</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="kpi-label">Max DD</div>
|
|
<div id="pnl-dd" class="text-xl font-bold text-red-500">—</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="kpi-label">Sharpe/Sortino</div>
|
|
<div id="pnl-sharpe" class="text-xl font-bold">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-6">
|
|
<canvas id="regime-dist" height="150"></canvas>
|
|
</div>
|
|
<div class="mt-6 grid grid-cols-2 gap-4">
|
|
<div class="bg-gray-900 p-3 rounded border border-gray-700">
|
|
<div class="text-sm text-gray-400">Advanced Metrics</div>
|
|
<div class="grid grid-cols-2 gap-2 mt-2 text-xs">
|
|
<div>Sharpe</div><div id="adv-sharpe" class="text-right">—</div>
|
|
<div>Sortino</div><div id="adv-sortino" class="text-right">—</div>
|
|
<div>Calmar</div><div id="adv-calmar" class="text-right">—</div>
|
|
<div>PF</div><div id="adv-pf" class="text-right">—</div>
|
|
<div>Win%</div><div id="adv-winrate" class="text-right">—</div>
|
|
<div>MaxDD</div><div id="adv-maxdd" class="text-right">—</div>
|
|
<div>Tail Ratio</div><div id="adv-tail" class="text-right">—</div>
|
|
<div>PSR</div><div id="adv-psr" class="text-right">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-900 p-3 rounded border border-gray-700">
|
|
<div class="text-sm text-gray-400">Adv Settings</div>
|
|
<label class="flex items-center space-x-2 mt-2">
|
|
<input id="cfg-adv-enabled" type="checkbox" class="form-checkbox">
|
|
<span class="text-xs">Enable Advanced Metrics</span>
|
|
</label>
|
|
<div class="mt-2">
|
|
<label class="text-xs text-gray-400 block mb-1">Interval (sec)</label>
|
|
<input id="cfg-adv-interval" type="number" min="10" max="3600" step="10" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" placeholder="120">
|
|
</div>
|
|
<button id="apply-adv" class="mt-3 bg-blue-700 hover:bg-blue-600 px-3 py-1.5 rounded text-sm">Apply Advanced</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-gray-700 overflow-hidden">
|
|
<div class="text-xl font-bold mb-4">Metrics Comparison</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="text-gray-400 bg-gray-900 border-b border-gray-700">
|
|
<th class="px-3 py-2 text-left">Metric</th>
|
|
<th class="px-3 py-2 text-left">EURUSDm</th>
|
|
<th class="px-3 py-2 text-left">XAUUSDm</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="metrics-compare" class="text-gray-300 divide-y divide-gray-700"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Config -->
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-gray-700">
|
|
<div class="text-xl font-bold mb-4">Global Pipeline Config</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Decision Threshold</label>
|
|
<div class="flex items-center space-x-3">
|
|
<input id="cfg-threshold" type="range" min="0.3" max="0.9" step="0.01" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer">
|
|
<span id="cfg-threshold-val" class="text-sm font-mono text-blue-400 w-12 text-right">—</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Risk per Trade (Decimal)</label>
|
|
<input id="cfg-risk" type="number" min="0" max="0.05" step="0.001" class="bg-gray-700 p-2 rounded w-full border border-gray-600 focus:border-blue-500 outline-none" placeholder="0.010">
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">ATR Mult</label>
|
|
<input id="cfg-atr" type="number" min="0.5" max="5" step="0.1" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" placeholder="1.5">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">RR Ratio</label>
|
|
<input id="cfg-rr" type="number" min="1" max="5" step="0.1" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" placeholder="2.0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex mt-6 space-x-4">
|
|
<button id="apply-risk" class="bg-indigo-600 hover:bg-indigo-700 px-6 py-2 rounded font-bold transition">Apply Pipeline Changes</button>
|
|
<button id="reset-config" class="bg-gray-700 hover:bg-gray-600 px-6 py-2 rounded font-bold transition">Reset Defaults</button>
|
|
</div>
|
|
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="flex items-center justify-between bg-gray-900 p-3 rounded border border-gray-700">
|
|
<label class="flex items-center space-x-2">
|
|
<input id="cfg-hft-enabled" type="checkbox" class="form-checkbox">
|
|
<span class="text-sm">Enable HFT Optimizer</span>
|
|
</label>
|
|
<span id="status-hft" class="px-2 py-0.5 text-xs rounded bg-red-700 text-red-100">OFF</span>
|
|
</div>
|
|
<div class="flex items-center justify-between bg-gray-900 p-3 rounded border border-gray-700">
|
|
<label class="flex items-center space-x-2">
|
|
<input id="cfg-ga-enabled" type="checkbox" class="form-checkbox">
|
|
<span class="text-sm">Enable GA Evaluator</span>
|
|
</label>
|
|
<span id="status-ga" class="px-2 py-0.5 text-xs rounded bg-red-700 text-red-100">OFF</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-green-700">
|
|
<div class="text-xl font-bold mb-4 text-green-400">Backtest Runner</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Symbol</label>
|
|
<select id="bt-symbol" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option>EURUSDm</option>
|
|
<option>XAUUSDm</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Timeframe</label>
|
|
<select id="bt-tf" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option>M1</option>
|
|
<option selected>M5</option>
|
|
<option>M15</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Start</label>
|
|
<input id="bt-start" type="date" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" value="2016-01-01">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">End</label>
|
|
<input id="bt-end" type="date" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" value="2025-12-31">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Mode</label>
|
|
<select id="bt-mode" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option selected>retail</option>
|
|
<option>sample_ma</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-end">
|
|
<button id="bt-run" class="bg-green-700 hover:bg-green-600 px-6 py-2 rounded font-bold transition w-full">Run Backtest</button>
|
|
</div>
|
|
</div>
|
|
<div id="bt-log" class="mt-4 text-xs font-mono bg-gray-900 p-2 rounded h-40 overflow-auto border border-gray-700"></div>
|
|
</div>
|
|
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-gray-700">
|
|
<div class="text-xl font-bold mb-4">Retail Strategies Config</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div class="flex items-center space-x-3">
|
|
<input id="cfg-retail-enabled" type="checkbox" class="form-checkbox">
|
|
<label class="text-sm text-gray-300">Enable Retail Strategies</label>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Merge Mode</label>
|
|
<select id="cfg-retail-mode" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option value="augment">Augment (safe)</option>
|
|
<option value="replace">Replace (aggressive)</option>
|
|
</select>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Min Confidence</label>
|
|
<input id="cfg-retail-minconf" type="number" min="0" max="1" step="0.01" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" placeholder="0.55">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Retail RR</label>
|
|
<input id="cfg-retail-rr" type="number" min="1" max="5" step="0.1" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none" placeholder="1.5">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex mt-6 space-x-4">
|
|
<button id="apply-retail" class="bg-green-700 hover:bg-green-600 px-6 py-2 rounded font-bold transition">Apply Retail Settings</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-yellow-700">
|
|
<div class="text-xl font-bold mb-4 text-yellow-400">Test Trade (Temporary)</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Symbol</label>
|
|
<select id="tt-symbol" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option>EURUSDm</option>
|
|
<option>XAUUSDm</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Side</label>
|
|
<select id="tt-side" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option>BUY</option>
|
|
<option>SELL</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Lot</label>
|
|
<input id="tt-lot" type="number" min="0.01" step="0.01" value="0.01" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">SL (pips)</label>
|
|
<input id="tt-sl" type="number" min="1" step="1" value="20" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">TP RR</label>
|
|
<input id="tt-rr" type="number" min="0.5" step="0.1" value="2.0" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="text-sm text-gray-400 block mb-1">Override Regime</label>
|
|
<select id="tt-regime" class="bg-gray-700 p-2 rounded w-full border border-gray-600 outline-none">
|
|
<option value="">(none)</option>
|
|
<option value="RALLY">RALLY</option>
|
|
<option value="LOW_VOLATILITY">LOW_VOLATILITY</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex mt-4">
|
|
<button id="tt-send" class="bg-yellow-600 hover:bg-yellow-500 px-6 py-2 rounded font-bold transition">Send Test Trade</button>
|
|
</div>
|
|
<div class="text-xs text-gray-400 mt-2">Note: For verification only. Uses current bid/ask; engine applies SL/TP distances.</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-purple-700">
|
|
<div class="text-xl font-bold mb-4 text-purple-400">Signal Debug</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">EURUSDm</div>
|
|
<div id="signal-debug-EURUSDm" class="text-xs font-mono bg-gray-900 p-2 rounded h-40 overflow-auto border border-gray-700"></div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">XAUUSDm</div>
|
|
<div id="signal-debug-XAUUSDm" class="text-xs font-mono bg-gray-900 p-2 rounded h-40 overflow-auto border border-gray-700"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-yellow-700">
|
|
<div class="text-xl font-bold mb-4 text-yellow-400">Pipeline Debug</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">EURUSDm</div>
|
|
<div id="pipeline-debug-EURUSDm" class="text-xs font-mono bg-gray-900 p-2 rounded h-40 overflow-auto border border-gray-700"></div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">XAUUSDm</div>
|
|
<div id="pipeline-debug-XAUUSDm" class="text-xs font-mono bg-gray-900 p-2 rounded h-40 overflow-auto border border-gray-700"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-blue-700">
|
|
<div class="text-xl font-bold mb-4 text-blue-400">Regime Switching & Stress</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">Regime Probabilities</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center"><span class="w-24 text-xs text-gray-300">BULL</span><div class="flex-1 bg-gray-700 h-3 rounded"><div id="reg-bull" class="bg-green-500 h-3 rounded" style="width:0%"></div></div><span id="reg-bull-t" class="ml-2 text-xs">—</span></div>
|
|
<div class="flex items-center"><span class="w-24 text-xs text-gray-300">BEAR</span><div class="flex-1 bg-gray-700 h-3 rounded"><div id="reg-bear" class="bg-red-500 h-3 rounded" style="width:0%"></div></div><span id="reg-bear-t" class="ml-2 text-xs">—</span></div>
|
|
<div class="flex items-center"><span class="w-24 text-xs text-gray-300">SIDEWAYS</span><div class="flex-1 bg-gray-700 h-3 rounded"><div id="reg-side" class="bg-yellow-500 h-3 rounded" style="width:0%"></div></div><span id="reg-side-t" class="ml-2 text-xs">—</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<div class="text-sm text-gray-400 mb-2">Stress Scenarios</div>
|
|
<table class="min-w-full text-xs">
|
|
<thead><tr class="text-gray-400"><th class="px-2 py-1 text-left">Scenario</th><th class="px-2 py-1 text-right">VaR</th><th class="px-2 py-1 text-right">CVaR</th><th class="px-2 py-1 text-right">Max DD</th></tr></thead>
|
|
<tbody id="stress-table" class="text-gray-200"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="panel-hft-optimizer" class="bg-gray-800 p-4 rounded mb-8 border border-indigo-700 hidden">
|
|
<div class="text-xl font-bold mb-4 text-indigo-400">HFT Optimizer</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">Latency (ms)</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Avg</span><span id="hft-lat-avg" class="text-xs text-gray-100">—</span></div>
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">p95</span><span id="hft-lat-p95" class="text-xs text-gray-100">—</span></div>
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">p99</span><span id="hft-lat-p99" class="text-xs text-gray-100">—</span></div>
|
|
<div class="mt-2">
|
|
<div class="flex items-end space-x-1 h-16" id="hft-lat-bars">
|
|
<div id="hft-lat-b-0" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-1" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-2" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-3" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-4" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-5" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-6" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-7" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-8" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
<div id="hft-lat-b-9" class="w-4 bg-indigo-500" style="height:4px"></div>
|
|
</div>
|
|
<div class="text-[10px] text-gray-400 mt-1">Histogram bins: ≤2, ≤5, ≤10, ≤20, ≤30, ≤50, ≤80, ≤120, ≤200, >200</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">Fill Metrics</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Executed</span><span id="hft-exec" class="text-xs text-gray-100">0</span></div>
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Canceled</span><span id="hft-cancel" class="text-xs text-gray-100">0</span></div>
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Fill Rate</span><span id="hft-fillrate" class="text-xs text-gray-100">—</span></div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">Inventory</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Risk</span><span id="hft-inv-risk" class="text-xs text-gray-100">—</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="panel-hft-exec" class="bg-gray-800 p-4 rounded mb-8 border border-teal-700 hidden">
|
|
<div class="text-xl font-bold mb-4 text-teal-400">HFT Execution</div>
|
|
<div>
|
|
<table class="min-w-full text-xs">
|
|
<thead><tr class="text-gray-400"><th class="px-2 py-1 text-left">Time</th><th class="px-2 py-1 text-left">Symbol</th><th class="px-2 py-1 text-left">Side</th><th class="px-2 py-1 text-right">Price</th><th class="px-2 py-1 text-right">Lot</th><th class="px-2 py-1 text-right">Latency</th><th class="px-2 py-1 text-left">Status</th></tr></thead>
|
|
<tbody id="hft-exec-table" class="text-gray-200"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-purple-700">
|
|
<div class="text-xl font-bold mb-4 text-purple-400">Portfolio</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2">Weighted Scores</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Liquidity</span><div class="flex-1 bg-gray-700 h-3 rounded"><div id="port-liq" class="bg-green-500 h-3 rounded" style="width:0%"></div></div><span id="port-liq-t" class="ml-2 text-xs">—</span></div>
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Toxicity</span><div class="flex-1 bg-gray-700 h-3 rounded"><div id="port-tox" class="bg-red-500 h-3 rounded" style="width:0%"></div></div><span id="port-tox-t" class="ml-2 text-xs">—</span></div>
|
|
<div class="flex items-center"><span class="w-28 text-xs text-gray-300">Pressure</span><div class="flex-1 bg-gray-700 h-3 rounded"><div id="port-prs" class="bg-yellow-500 h-3 rounded" style="width:0%"></div></div><span id="port-prs-t" class="ml-2 text-xs">—</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<div class="text-sm text-gray-400 mb-2">Symbol Breakdown</div>
|
|
<table class="min-w-full text-xs">
|
|
<thead><tr class="text-gray-400"><th class="px-2 py-1 text-left">Symbol</th><th class="px-2 py-1 text-right">Weight</th><th class="px-2 py-1 text-right">Liquidity</th><th class="px-2 py-1 text-right">Toxicity</th><th class="px-2 py-1 text-right">Pressure</th></tr></thead>
|
|
<tbody id="portfolio-table" class="text-gray-200"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-4 rounded mb-8 border border-red-700">
|
|
<div class="text-xl font-bold mb-4 text-red-400">Alerts</div>
|
|
<div>
|
|
<table class="min-w-full text-xs">
|
|
<thead><tr class="text-gray-400"><th class="px-2 py-1 text-left">Time</th><th class="px-2 py-1 text-left">Symbol</th><th class="px-2 py-1 text-left">Message</th><th class="px-2 py-1 text-right">Severity</th><th class="px-2 py-1 text-right">Value</th></tr></thead>
|
|
<tbody id="alerts-table" class="text-gray-200"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="global-alert" class="hidden fixed bottom-4 right-4 bg-red-800 text-red-100 px-4 py-2 rounded shadow-lg text-sm">Dashboard disconnected</div>
|
|
|
|
<script>
|
|
let ws;
|
|
const SYMBOLS = ['EURUSDm', 'XAUUSDm'];
|
|
const state = {};
|
|
let lastHeartbeat = 0;
|
|
const seenTrades = new Map();
|
|
const METHOD_COLORS = {
|
|
'min_variance': '#10B981',
|
|
'max_sharpe': '#3B82F6',
|
|
'risk_parity': '#F59E0B',
|
|
'black_litterman': '#A78BFA',
|
|
'hrp': '#EC4899',
|
|
'equal_weight': '#22D3EE',
|
|
'max_diversification': '#F87171',
|
|
'default': '#9CA3AF'
|
|
};
|
|
|
|
SYMBOLS.forEach(symbol => {
|
|
state[symbol] = {
|
|
ticks: { labels: [], data: [] },
|
|
m1: { labels: [], data: [] },
|
|
m5: { labels: [], data: [] },
|
|
chart: null,
|
|
vp: { chart: null, labels: [], data: [], colors: [] },
|
|
signalHistory: [],
|
|
lastTrade: null,
|
|
eqTimes: [],
|
|
eqValues: [],
|
|
};
|
|
});
|
|
|
|
let useRealized = false;
|
|
let regimeBarLabels = []; let regimeBarData = [];
|
|
let hftEnabled = false;
|
|
let gaEnabled = false;
|
|
|
|
// Initialize Charts
|
|
SYMBOLS.forEach(symbol => {
|
|
const ctx = document.getElementById(`chart-${symbol}`).getContext('2d');
|
|
state[symbol].chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: state[symbol].ticks.labels,
|
|
datasets: [
|
|
{ label: 'Tick', data: state[symbol].ticks.data, borderColor: symbol === 'EURUSDm' ? '#34D399' : '#FBBF24', fill: false, pointRadius: 0, hidden: false },
|
|
{ label: 'M1 Close', data: state[symbol].m1.data, borderColor: '#60A5FA', fill: false, pointRadius: 0, hidden: true },
|
|
{ label: 'M5 Close', data: state[symbol].m5.data, borderColor: '#F472B6', fill: false, pointRadius: 0, hidden: true }
|
|
]
|
|
},
|
|
options: {
|
|
animation: false,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: { ticks: { display: false }, grid: { display: false } },
|
|
y: { ticks: { color: '#9CA3AF', font: { size: 10 } }, grid: { color: '#374151' } }
|
|
},
|
|
plugins: { legend: { display: false } }
|
|
}
|
|
});
|
|
const vpEl = document.getElementById(`vp-${symbol}`);
|
|
if (vpEl) {
|
|
state[symbol].vp.chart = new Chart(vpEl.getContext('2d'), {
|
|
type: 'bar',
|
|
data: { labels: [], datasets: [{ label: 'Norm Vol', data: [], backgroundColor: [], borderWidth: 0 }]},
|
|
options: {
|
|
indexAxis: 'y',
|
|
animation: false,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: { ticks: { color: '#9CA3AF' }, grid: { color: '#374151' } },
|
|
y: { ticks: { color: '#9CA3AF', font: { size: 9 } }, grid: { display: false } }
|
|
},
|
|
plugins: { legend: { display: false } }
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const eqEl = document.getElementById('equity-curve');
|
|
let eqChart = new Chart(eqEl.getContext('2d'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{ label: 'EURUSDm', data: [], borderColor: '#34D399', fill: false, pointRadius: 0, tension: 0.15 },
|
|
{ label: 'XAUUSDm', data: [], borderColor: '#FBBF24', fill: false, pointRadius: 0, tension: 0.15 }
|
|
]
|
|
},
|
|
options: {
|
|
animation: false,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: true, labels: { color: '#9CA3AF', boxWidth: 10 } } },
|
|
scales: {
|
|
x: { ticks: { color: '#9CA3AF', maxRotation: 0, font: { size: 9 } }, grid: { display: false } },
|
|
y: { ticks: { color: '#9CA3AF' }, grid: { color: '#374151' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
const rdChart = new Chart(document.getElementById('regime-dist').getContext('2d'), {
|
|
type: 'bar',
|
|
data: { labels: regimeBarLabels, datasets: [{ label: 'Regime Distribution', data: regimeBarData, backgroundColor: '#3B82F6' }]},
|
|
options: {
|
|
animation: false,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { ticks: { color: '#9CA3AF', font: { size: 10 } } },
|
|
y: { ticks: { color: '#9CA3AF' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
const eqCharts = {};
|
|
['EURUSDm','XAUUSDm'].forEach(sym => {
|
|
const el = document.getElementById(`eq-${sym}`);
|
|
if (el) {
|
|
eqCharts[sym] = new Chart(el.getContext('2d'), {
|
|
type: 'line',
|
|
data: { labels: [], datasets: [{ label: sym, data: [], borderColor: sym === 'EURUSDm' ? '#34D399' : '#FBBF24', fill: false, pointRadius: 0, tension: 0.15 }]},
|
|
options: { animation: false, responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#9CA3AF', font: { size: 9 } }, grid: { display: false } }, y: { ticks: { color: '#9CA3AF' }, grid: { color: '#374151' }}}}
|
|
});
|
|
}
|
|
});
|
|
const MAX_EQ_POINTS = 180;
|
|
function updateCombinedEquity() {
|
|
const t1 = state['EURUSDm'].eqTimes || [];
|
|
const t2 = state['XAUUSDm'].eqTimes || [];
|
|
const v1 = state['EURUSDm'].eqValues || [];
|
|
const v2 = state['XAUUSDm'].eqValues || [];
|
|
const baseTimes = (t1.length >= t2.length ? t1 : t2).slice(-MAX_EQ_POINTS);
|
|
const lab = baseTimes.map(n => new Date(n*1000).toLocaleTimeString());
|
|
const align = (vals, len) => {
|
|
const sl = vals.slice(-len);
|
|
const pad = new Array(len - sl.length).fill(null);
|
|
return pad.concat(sl);
|
|
};
|
|
eqChart.data.labels = lab;
|
|
eqChart.data.datasets[0].data = align(v1, baseTimes.length);
|
|
eqChart.data.datasets[1].data = align(v2, baseTimes.length);
|
|
eqChart.update('none');
|
|
}
|
|
|
|
function setStatus(s) {
|
|
// Efficient Frontier Chart
|
|
const frontierCtx = document.getElementById('efficient-frontier').getContext('2d');
|
|
let frontierChart = new Chart(frontierCtx, {
|
|
type: 'scatter',
|
|
data: { datasets: [] },
|
|
options: {
|
|
animation: false,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
parsing: false,
|
|
plugins: {
|
|
legend: { display: true, labels: { color: '#9CA3AF' } },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (ctx) => {
|
|
const p = ctx.raw || {};
|
|
const w = p.weights || {};
|
|
const wStr = Object.keys(w).map(k => `${k}:${(w[k]*100).toFixed(1)}%`).join(' ');
|
|
const sh = typeof p.sharpe === 'number' ? p.sharpe.toFixed(2) : '—';
|
|
const dr = typeof p.dr === 'number' ? p.dr.toFixed(2) : '—';
|
|
return `ER ${p.y?.toFixed(2)}% | Vol ${p.x?.toFixed(2)}% | Sharpe ${sh} | DR ${dr} | ${wStr}`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: { title: { display: true, text: 'Volatility (%)', color: '#9CA3AF' }, ticks: { color: '#9CA3AF' }, grid: { color: '#374151' } },
|
|
y: { title: { display: true, text: 'Expected Return (%)', color: '#9CA3AF' }, ticks: { color: '#9CA3AF' }, grid: { color: '#374151' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
function updateEfficientFrontier(frontier, highlightMap) {
|
|
if (!Array.isArray(frontier)) return;
|
|
const groups = {};
|
|
frontier.forEach(n => {
|
|
const er = Number(((n.metrics && n.metrics.expected_return_annual) || 0) * 100);
|
|
const vol = Number(((n.metrics && n.metrics.volatility_annual) || 0) * 100);
|
|
const sh = Number((n.metrics && n.metrics.sharpe_ratio_annual) || 0);
|
|
const dr = Number((n.metrics && n.metrics.diversification_ratio) || 0);
|
|
const weights = n.weights || {};
|
|
const method = String(n.method || n.label || 'frontier');
|
|
const pt = { x: vol, y: er, sharpe: sh, dr: dr, weights: weights };
|
|
if (!groups[method]) groups[method] = [];
|
|
groups[method].push(pt);
|
|
});
|
|
const ds = [];
|
|
Object.keys(groups).forEach(m => {
|
|
ds.push({
|
|
label: m,
|
|
data: groups[m],
|
|
borderColor: METHOD_COLORS[m] || METHOD_COLORS.default,
|
|
backgroundColor: METHOD_COLORS[m] || METHOD_COLORS.default,
|
|
pointRadius: 3,
|
|
showLine: false
|
|
});
|
|
});
|
|
// Highlights: bigger points
|
|
['max_sharpe','min_variance','risk_parity'].forEach(m => {
|
|
const pts = (groups[m] || []).map(p => ({...p}));
|
|
if (pts.length > 0) {
|
|
ds.push({
|
|
label: `${m}_opt`,
|
|
data: [pts[pts.length-1]],
|
|
borderColor: METHOD_COLORS[m] || '#FFFFFF',
|
|
backgroundColor: METHOD_COLORS[m] || '#FFFFFF',
|
|
pointRadius: 6,
|
|
showLine: false
|
|
});
|
|
}
|
|
});
|
|
if (highlightMap && typeof highlightMap.selected === 'string' && highlightMap.selected) {
|
|
const m = String(highlightMap.selected);
|
|
const pts = (groups[m] || []).map(p => ({...p}));
|
|
if (pts.length > 0) {
|
|
ds.push({
|
|
label: `selected_${m}`,
|
|
data: [pts[pts.length-1]],
|
|
borderColor: '#FFFFFF',
|
|
backgroundColor: '#FFFFFF',
|
|
pointRadius: 7,
|
|
showLine: false
|
|
});
|
|
}
|
|
}
|
|
frontierChart.data.datasets = ds;
|
|
frontierChart.update('none');
|
|
}
|
|
|
|
let methodTableData = [];
|
|
let methodSort = { key: 'method', asc: true };
|
|
function renderMethodTable() {
|
|
const tbody = document.getElementById('method-table-body');
|
|
const filter = (document.getElementById('method-filter')?.value || '').toLowerCase();
|
|
tbody.innerHTML = '';
|
|
const rows = methodTableData
|
|
.filter(r => !filter || r.method.toLowerCase().includes(filter))
|
|
.sort((a,b) => {
|
|
const k = methodSort.key;
|
|
const av = a[k], bv = b[k];
|
|
if (typeof av === 'string' || typeof bv === 'string') {
|
|
return methodSort.asc ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
|
|
}
|
|
return methodSort.asc ? (av - bv) : (bv - av);
|
|
});
|
|
rows.forEach(r => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td class="px-2 py-2">${r.method}</td>
|
|
<td class="px-2 py-2 text-right">${(r.er*100).toFixed(2)}%</td>
|
|
<td class="px-2 py-2 text-right">${(r.vol*100).toFixed(2)}%</td>
|
|
<td class="px-2 py-2 text-right">${r.sharpe.toFixed(2)}</td>
|
|
<td class="px-2 py-2 text-right">${(r.mdd*100).toFixed(2)}%</td>
|
|
<td class="px-2 py-2 text-right">${r.dr.toFixed(2)}</td>
|
|
<td class="px-2 py-2 text-right">${r.hhi.toFixed(3)}</td>
|
|
<td class="px-2 py-2 text-right">${r.ena.toFixed(2)}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
function updateMethodTable(methodsMap) {
|
|
const rows = [];
|
|
if (methodsMap && typeof methodsMap === 'object') {
|
|
Object.keys(methodsMap).forEach(m => {
|
|
const d = methodsMap[m] || {};
|
|
const th = d.theoretical || d.metrics || {};
|
|
const dd = d.realized && d.realized.drawdown || {};
|
|
const ra = d.realized && d.realized.risk_adjusted || {};
|
|
rows.push({
|
|
method: m,
|
|
er: Number(th.expected_return_annual || 0),
|
|
vol: Number(th.volatility_annual || 0),
|
|
sharpe: Number(th.sharpe_ratio_annual || 0),
|
|
mdd: Number(dd.max_drawdown || 0),
|
|
dr: Number(th.diversification_ratio || 0),
|
|
hhi: Number(th.concentration_index || 0),
|
|
ena: Number(th.effective_number_assets || 0)
|
|
});
|
|
});
|
|
}
|
|
methodTableData = rows;
|
|
renderMethodTable();
|
|
}
|
|
document.getElementById('method-filter')?.addEventListener('input', renderMethodTable);
|
|
Array.from(document.querySelectorAll('#method-table thead th[data-sort]')).forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const key = th.getAttribute('data-sort');
|
|
if (!key) return;
|
|
if (methodSort.key === key) methodSort.asc = !methodSort.asc;
|
|
else { methodSort.key = key; methodSort.asc = true; }
|
|
renderMethodTable();
|
|
});
|
|
});
|
|
|
|
const el = document.getElementById('status');
|
|
const txt = document.getElementById('statusText');
|
|
txt.textContent = s;
|
|
el.classList.remove('bg-green-500','bg-yellow-500','bg-red-500');
|
|
if (s === 'connected') el.classList.add('bg-green-500');
|
|
if (s === 'connected') el.classList.add('bg-green-500');
|
|
else if (s === 'reconnecting' || s === 'connecting') el.classList.add('bg-yellow-500');
|
|
else el.classList.add('bg-red-500');
|
|
}
|
|
|
|
function connect() {
|
|
const qs = new URLSearchParams(window.location.search);
|
|
const override = qs.get('ws');
|
|
const wsUrl = override && override.startsWith('ws') ? override : 'ws://127.0.0.1:8090';
|
|
ws = new WebSocket(wsUrl);
|
|
ws.onopen = () => setStatus('connected');
|
|
ws.onmessage = (ev) => {
|
|
try {
|
|
const msg = JSON.parse(ev.data);
|
|
const symbol = msg.symbol;
|
|
if (msg.type === 'backtest_status') {
|
|
const log = document.getElementById('bt-log');
|
|
const ts = new Date().toLocaleTimeString();
|
|
const line = `[${ts}] ${msg.symbol || ''} ${msg.stage || ''} ${msg.message || ''} ${typeof msg.rows==='number' ? ('rows='+msg.rows) : ''}`;
|
|
const div = document.createElement('div');
|
|
div.textContent = line;
|
|
log.prepend(div);
|
|
while (log.children.length > 200) log.lastChild.remove();
|
|
return;
|
|
}
|
|
if (msg.type === 'backtest_done') {
|
|
const log = document.getElementById('bt-log');
|
|
const out = String(msg.out_dir || '').replace(/\\\\/g,'/').replace(/\\/g,'/');
|
|
const a = document.createElement('a');
|
|
a.href = `${out}/report.txt`;
|
|
a.textContent = `Report (${msg.symbol})`;
|
|
a.className = 'text-green-400 underline';
|
|
a.target = '_blank';
|
|
const div = document.createElement('div');
|
|
div.appendChild(a);
|
|
log.prepend(div);
|
|
while (log.children.length > 200) log.lastChild.remove();
|
|
return;
|
|
}
|
|
|
|
if (msg.type === 'status') {
|
|
setStatus(msg.status);
|
|
} else if (msg.type === 'account_info') {
|
|
const cur = msg.currency || '';
|
|
const fmt = (x) => (typeof x === 'number' && isFinite(x)) ? x.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '—';
|
|
document.getElementById('kpi-balance').textContent = fmt(msg.balance) + (cur ? (' ' + cur) : '');
|
|
document.getElementById('kpi-equity').textContent = fmt(msg.equity) + (cur ? (' ' + cur) : '');
|
|
document.getElementById('kpi-free-margin').textContent = fmt(msg.free_margin) + (cur ? (' ' + cur) : '');
|
|
document.getElementById('kpi-margin-level').textContent = (typeof msg.margin_level === 'number' && isFinite(msg.margin_level)) ? msg.margin_level.toFixed(2) + ' %' : '—';
|
|
} else if (msg.type === 'tick' && msg.tick && symbol && state[symbol]) {
|
|
const s = state[symbol];
|
|
const t = new Date((msg.tick.timestamp||0)*1000);
|
|
const price = msg.tick.last || msg.tick.bid || msg.tick.ask;
|
|
s.ticks.labels.push(t.toLocaleTimeString());
|
|
s.ticks.data.push(price);
|
|
if (s.ticks.labels.length > 200) { s.ticks.labels.shift(); s.ticks.data.shift(); }
|
|
s.chart.update('none');
|
|
const tb = document.getElementById(`trade-blotter-${symbol}`);
|
|
if (tb) {
|
|
const rows = Array.from(tb.children||[]);
|
|
rows.forEach(r => {
|
|
const entry = Number(r.dataset.entry||0);
|
|
const lot = Number(r.dataset.lot||0);
|
|
const side = String(r.dataset.side||'').toUpperCase();
|
|
const csize = Number(r.dataset.csize||((symbol.includes('XAU'))?100.0:100000.0));
|
|
if (!entry || !lot || !side) return;
|
|
const sign = side==='BUY'?1.0:-1.0;
|
|
const val = ((price - entry) * sign * csize * lot);
|
|
const cell = r.querySelector(`[data-pnlcell="1"]`);
|
|
if (cell) {
|
|
cell.textContent = (isFinite(val)?val.toFixed(2):'—');
|
|
cell.className = `px-3 py-2 text-right font-mono ${val>=0?'text-green-400':'text-red-400'}`;
|
|
}
|
|
});
|
|
}
|
|
} else if (msg.type === 'candle_update' && symbol && state[symbol]) {
|
|
const s = state[symbol];
|
|
const timeStr = new Date((msg.time||0)*1000).toLocaleTimeString();
|
|
if (msg.timeframe === 'M1') {
|
|
s.m1.labels.push(timeStr);
|
|
s.m1.data.push(msg.close);
|
|
if (s.m1.labels.length > 100) { s.m1.labels.shift(); s.m1.data.shift(); }
|
|
} else if (msg.timeframe === 'M5') {
|
|
s.m5.labels.push(timeStr);
|
|
s.m5.data.push(msg.close);
|
|
if (s.m5.labels.length > 100) { s.m5.labels.shift(); s.m5.data.shift(); }
|
|
}
|
|
s.chart.update('none');
|
|
} else if (msg.type === 'features_update' && symbol && state[symbol]) {
|
|
if (msg.timeframe === 'M1') {
|
|
const el = document.getElementById(`feat-vol-${symbol}`);
|
|
if (el) el.textContent = (msg.features.volatility || 0).toFixed(6);
|
|
} else if (msg.timeframe === 'M5') {
|
|
const f5 = msg.features;
|
|
const set = (id, val, digits=2) => {
|
|
const el = document.getElementById(id);
|
|
if (el) el.textContent = (typeof val === 'number' && isFinite(val)) ? val.toFixed(digits) : '—';
|
|
};
|
|
set(`feat-adx-${symbol}`, f5.adx14, 2);
|
|
set(`feat-macd-${symbol}`, f5.macd, 3);
|
|
set(`feat-macd-sig-${symbol}`, f5.macd_signal, 3);
|
|
set(`feat-macd-hist-${symbol}`, f5.macd_hist, 3);
|
|
set(`feat-bb-width-${symbol}`, f5.bb_width, 3);
|
|
set(`feat-bb-pos-${symbol}`, f5.bb_pos, 3);
|
|
}
|
|
} else if (msg.type === 'regime_update' && symbol) {
|
|
const el = document.getElementById(`regime-name-${symbol}`);
|
|
if (el) el.textContent = msg.regime || '—';
|
|
} else if (msg.type === 'signal_update' && symbol) {
|
|
document.getElementById(`signal-name-${symbol}`).textContent = msg.signal || '—';
|
|
document.getElementById(`signal-conf-${symbol}`).textContent = (msg.confidence * 100).toFixed(0) + '% Conf';
|
|
} else if (msg.type === 'signal_debug' && symbol) {
|
|
const el = document.getElementById(`signal-debug-${symbol}`);
|
|
if (el) {
|
|
const ts = typeof msg.timestamp === 'number' ? new Date(msg.timestamp * 1000).toLocaleTimeString() : '';
|
|
const line = [ts, msg.timeframe || '', msg.strategy || '', msg.reason || '', ((Number(msg.confidence)||0)*100).toFixed(0) + '%'].filter(Boolean).join(' ');
|
|
const div = document.createElement('div');
|
|
div.textContent = line;
|
|
el.prepend(div);
|
|
while (el.children.length > 100) el.lastChild.remove();
|
|
}
|
|
} else if (msg.type === 'pipeline_debug' && symbol) {
|
|
const el = document.getElementById(`pipeline-debug-${symbol}`);
|
|
if (el) {
|
|
const ts = typeof msg.timestamp === 'number' ? new Date(msg.timestamp * 1000).toLocaleTimeString() : '';
|
|
const stage = String(msg.stage || '').toUpperCase();
|
|
let info = '';
|
|
if (stage === 'SIGNAL') {
|
|
info = [msg.regime || '', msg.base_signal || '', '→', msg.signal || '', ((Number(msg.confidence)||0)*100).toFixed(0)+'%', msg.reason || ''].join(' ');
|
|
} else if (stage === 'DECISION') {
|
|
info = [msg.regime || '', msg.signal || '', (msg.approved ? 'APPROVED' : 'FILTERED'), ((Number(msg.confidence)||0)*100).toFixed(0)+'%', 'thr', Number(msg.threshold||0).toFixed(2), msg.reason || ''].join(' ');
|
|
} else if (stage === 'RISK') {
|
|
info = ['lot', (Number(msg.lot_size||0)).toFixed(3), 'atr', (Number(msg.atr||0)).toFixed(6), 'eq', (Number(msg.equity||0)).toFixed(2), msg.reason || ''].join(' ');
|
|
} else if (stage === 'EXECUTION') {
|
|
info = [msg.status || '', 'px', (Number(msg.price||0)).toFixed(5), 'slip', (Number(msg.slippage||0)).toFixed(5), msg.reason || ''].join(' ');
|
|
} else {
|
|
info = JSON.stringify(msg);
|
|
}
|
|
const div = document.createElement('div');
|
|
div.textContent = [ts, stage, info].filter(Boolean).join(' ');
|
|
el.prepend(div);
|
|
while (el.children.length > 200) el.lastChild.remove();
|
|
}
|
|
if (String(msg.stage||'').toUpperCase() === 'VOLUME_PROFILE' && state[symbol] && state[symbol].vp && state[symbol].vp.chart) {
|
|
try {
|
|
const pts = Array.isArray(msg.normalized_points) ? msg.normalized_points.slice() : [];
|
|
pts.sort((a,b) => Number(a[0]) - Number(b[0]));
|
|
const labels = pts.map(p => {
|
|
const v = Number(p[0]||0);
|
|
const dec = symbol.includes('XAU') ? 2 : 5;
|
|
return v.toFixed(dec);
|
|
});
|
|
const data = pts.map(p => Number(p[1]||0));
|
|
const poc = Number(msg.poc||0);
|
|
const hvn = Array.isArray(msg.hvn) ? msg.hvn : [];
|
|
const lvn = Array.isArray(msg.lvn) ? msg.lvn : [];
|
|
const colors = pts.map(p => {
|
|
const price = Number(p[0]||0);
|
|
let c = 'rgba(74,144,226,0.6)';
|
|
const inZone = (zones) => {
|
|
for (let i=0;i<zones.length;i++) {
|
|
const z = zones[i];
|
|
const lo = Number(z[0]||0), hi = Number(z[1]||0);
|
|
if (price >= lo && price <= hi) return true;
|
|
}
|
|
return false;
|
|
};
|
|
const near = Math.abs(price - poc) <= (1e-9 + Math.max(0.0, Number(msg.bin_width||0)));
|
|
if (near) c = 'rgba(255,59,48,0.9)';
|
|
else if (inZone(hvn)) c = 'rgba(52,199,89,0.4)';
|
|
else if (inZone(lvn)) c = 'rgba(255,204,0,0.4)';
|
|
return c;
|
|
});
|
|
const ch = state[symbol].vp.chart;
|
|
ch.data.labels = labels;
|
|
ch.data.datasets[0].data = data;
|
|
ch.data.datasets[0].backgroundColor = colors;
|
|
ch.update('none');
|
|
} catch(e) {}
|
|
}
|
|
if (hftEnabled && String(msg.stage||'').toUpperCase() === 'HFT_OPT') {
|
|
try {
|
|
const avg = Number(msg.avg_latency_ms||0).toFixed(1);
|
|
const ex = Number(msg.orders_executed||0);
|
|
const ca = Number(msg.orders_canceled||0);
|
|
const fr = Number(msg.fill_rate||0)*100;
|
|
const inv = Number(msg.inventory_risk||0).toFixed(3);
|
|
const la = document.getElementById('hft-lat-avg'); if (la) la.textContent = avg+' ms';
|
|
const lp = document.getElementById('hft-lat-p95'); if (lp) lp.textContent = (Number(msg.p95_latency_ms||0)).toFixed(1)+' ms';
|
|
const lq = document.getElementById('hft-lat-p99'); if (lq) lq.textContent = (Number(msg.p99_latency_ms||0)).toFixed(1)+' ms';
|
|
const exEl = document.getElementById('hft-exec'); if (exEl) exEl.textContent = String(ex);
|
|
const caEl = document.getElementById('hft-cancel'); if (caEl) caEl.textContent = String(ca);
|
|
const frEl = document.getElementById('hft-fillrate'); if (frEl) frEl.textContent = isFinite(fr)?fr.toFixed(0)+'%':'—';
|
|
const irEl = document.getElementById('hft-inv-risk'); if (irEl) irEl.textContent = inv;
|
|
const bins = Array.isArray(msg.lat_bins)?msg.lat_bins:[]; const scale=60;
|
|
for (let i=0;i<10;i++){ const el=document.getElementById(`hft-lat-b-${i}`); if (el) { const v = bins[i]||0; el.style.height = Math.max(2, Math.min(scale, v*scale))+'px'; } }
|
|
} catch(e) {}
|
|
}
|
|
if (hftEnabled && String(msg.stage||'').toUpperCase() === 'HFT_EXEC') {
|
|
try {
|
|
const tb = document.getElementById('hft-exec-table'); if (tb) { const tr=document.createElement('tr'); const ts2 = typeof msg.timestamp === 'number' ? new Date(msg.timestamp*1000).toLocaleTimeString() : ''; const fmt=(x)=> (typeof x==='number'&&isFinite(x))?x.toFixed(3):'—'; tr.innerHTML = `<td class='px-2 py-1'>${ts2}</td><td class='px-2 py-1'>${String(msg.symbol||'')}</td><td class='px-2 py-1'>${String(msg.signal||'')}</td><td class='px-2 py-1 text-right'>${fmt(Number(msg.price||0))}</td><td class='px-2 py-1 text-right'>${fmt(Number(msg.lot||0))}</td><td class='px-2 py-1 text-right'>${fmt(Number(msg.latency_ms||0))} ms</td><td class='px-2 py-1'>${String(msg.status||'')}</td>`; tb.prepend(tr); while (tb.children.length>200) tb.lastChild.remove(); }
|
|
} catch(e) {}
|
|
}
|
|
} else if (msg.type === 'regime_stress') {
|
|
try {
|
|
const p = msg.probs || {};
|
|
const bull = Number(p.BULL||p.STRONG_BULL||0)*100;
|
|
const bear = Number(p.BEAR||p.STRONG_BEAR||0)*100;
|
|
const side = Number(p.SIDEWAYS||0)*100;
|
|
const setBar = (id, val) => { const el = document.getElementById(id); if (el) el.style.width = (Math.max(0, Math.min(100, val))).toFixed(0)+'%'; };
|
|
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = (Math.max(0, Math.min(100, val))).toFixed(0)+'%'; };
|
|
setBar('reg-bull', bull); setText('reg-bull-t', isFinite(bull)?bull.toFixed(0)+'%':'—');
|
|
setBar('reg-bear', bear); setText('reg-bear-t', isFinite(bear)?bear.toFixed(0)+'%':'—');
|
|
setBar('reg-side', side); setText('reg-side-t', isFinite(side)?side.toFixed(0)+'%':'—');
|
|
const st = msg.stress || {};
|
|
const tb = document.getElementById('stress-table'); if (tb) { tb.innerHTML = ''; const rows = Object.keys(st).map(k => ({ name: k, ...st[k] })); rows.forEach(r => { const tr = document.createElement('tr'); const fmt = (x) => (typeof x==='number' && isFinite(x))?x.toFixed(3):'—'; tr.innerHTML = `<td class='px-2 py-1'>${r.name}</td><td class='px-2 py-1 text-right'>${fmt(r.var)}</td><td class='px-2 py-1 text-right'>${fmt(r.cvar)}</td><td class='px-2 py-1 text-right'>${fmt(r.max_drawdown)}</td>`; tb.appendChild(tr); }); }
|
|
} catch(e) {}
|
|
} else if (msg.type === 'risk_features' && msg.symbol === 'PORT') {
|
|
try {
|
|
const liq = Number(msg.features.portfolio_liquidity_score||0)*100;
|
|
const tox = Number(msg.features.portfolio_toxicity_score||0)*100;
|
|
const prs = Number(msg.features.portfolio_pressure||0)*100;
|
|
const setBar = (id, val) => { const el = document.getElementById(id); if (el) el.style.width = (Math.max(0, Math.min(100, val))).toFixed(0)+'%'; };
|
|
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = (Math.max(0, Math.min(100, val))).toFixed(0)+'%'; };
|
|
setBar('port-liq', liq); setText('port-liq-t', isFinite(liq)?liq.toFixed(0)+'%':'—');
|
|
setBar('port-tox', tox); setText('port-tox-t', isFinite(tox)?tox.toFixed(0)+'%':'—');
|
|
setBar('port-prs', prs); setText('port-prs-t', isFinite(prs)?prs.toFixed(0)+'%':'—');
|
|
const tb = document.getElementById('portfolio-table'); if (tb) { tb.innerHTML=''; const symfs = msg.features.symbols||{}; const weights=msg.features.weights||{}; Object.keys(symfs).forEach(sym=>{ const r=symfs[sym]; const tr=document.createElement('tr'); const fmt=(x)=> (typeof x==='number'&&isFinite(x))?x.toFixed(3):'—'; const w=Number(weights[sym]||0); tr.innerHTML=`<td class='px-2 py-1'>${sym}</td><td class='px-2 py-1 text-right'>${fmt(w)}</td><td class='px-2 py-1 text-right'>${fmt(r.liquidity_score)}</td><td class='px-2 py-1 text-right'>${fmt(r.toxicity_score)}</td><td class='px-2 py-1 text-right'>${fmt(r.micro_pressure)}</td>`; tb.appendChild(tr); }); }
|
|
} catch(e) {}
|
|
} else if (msg.type === 'alert' && msg.stage === 'MICRO_ALERT') {
|
|
try {
|
|
const tb = document.getElementById('alerts-table'); if (tb) { const tr=document.createElement('tr'); const ts = typeof msg.time === 'number' ? new Date(msg.time*1000).toLocaleTimeString() : ''; let sev=0; let val='—'; if (msg.message==='toxicity_high') { sev=3; val = (Number(msg.toxicity||0)*100).toFixed(0)+'%'; } else if (msg.message==='liquidity_low') { sev=2; val = (Number(msg.liquidity||0)*100).toFixed(0)+'%'; } else if (msg.message==='pressure_block') { sev=2; val = Number(msg.pressure||0).toFixed(3); } tr.innerHTML = `<td class='px-2 py-1'>${ts}</td><td class='px-2 py-1'>${String(msg.symbol||'')}</td><td class='px-2 py-1'>${String(msg.message||'')}</td><td class='px-2 py-1 text-right'>${sev}</td><td class='px-2 py-1 text-right'>${val}</td>`; tb.prepend(tr); while (tb.children.length>300) tb.lastChild.remove(); }
|
|
} catch(e) {}
|
|
} else if (msg.type === 'decision_update' && symbol) {
|
|
const dEl = document.getElementById(`decision-name-${symbol}`);
|
|
const aEl = document.getElementById(`decision-approved-${symbol}`);
|
|
dEl.textContent = msg.decision || '—';
|
|
aEl.textContent = msg.approved ? 'APPROVED' : 'FILTERED';
|
|
aEl.className = msg.approved ? 'text-xs font-bold text-green-500' : 'text-xs font-bold text-red-500';
|
|
const rEl = document.getElementById(`decision-reason-${symbol}`);
|
|
if (rEl && typeof msg.reason === 'string') rEl.textContent = msg.reason;
|
|
} else if (msg.type === 'risk_update' && msg.risk && symbol) {
|
|
const r = msg.risk;
|
|
const set = (id, val, digits=6) => {
|
|
const el = document.getElementById(id);
|
|
if (el) el.textContent = (typeof val === 'number' && isFinite(val)) ? val.toFixed(digits) : '—';
|
|
};
|
|
set(`risk-size-${symbol}`, r.max_trade_size, 6);
|
|
set(`risk-sl-${symbol}`, r.stop_loss, 6);
|
|
set(`risk-tp-${symbol}`, r.take_profit, 6);
|
|
const ddEl = document.getElementById(`risk-dd-${symbol}`);
|
|
if (ddEl) ddEl.textContent = (r.allowed === false && r.reason === 'drawdown') ? 'Blocked' : 'OK';
|
|
const rrEl = document.getElementById(`risk-regime-${symbol}`);
|
|
if (rrEl) rrEl.textContent = String(r.regime || '—');
|
|
const adjEl = document.getElementById(`risk-adj-${symbol}`);
|
|
if (adjEl) adjEl.textContent = Array.isArray(r.adjustments_applied) ? r.adjustments_applied.join(', ') : (r.adjustments_applied || '—');
|
|
} else if (msg.type === 'trade_update' && msg.trade) {
|
|
const tr = msg.trade;
|
|
const status = String(tr.status||'').toLowerCase();
|
|
const tid = String(tr.trade_id||'');
|
|
if (status === 'submitted') return;
|
|
if (tid.startsWith('TT-')) return;
|
|
if (status === 'rejected' && ((Number(tr.sl||0)===0 && Number(tr.tp||0)===0))) return;
|
|
let key = '';
|
|
if (tid && tid !== 'ERR') {
|
|
key = ['tid', tid, status].join('|');
|
|
} else {
|
|
const dec = tr.symbol && tr.symbol.includes('XAU') ? 2 : 5;
|
|
const p = (tr.price || 0).toFixed(dec);
|
|
const bucket = Math.floor((Number(tr.timestamp||0)) / 2);
|
|
key = ['rej', tr.symbol || '', tr.signal || '', status, p, bucket].join('|');
|
|
}
|
|
const rowId = `trade-row-${key}`;
|
|
if (document.getElementById(rowId)) {
|
|
return;
|
|
}
|
|
const nowMs = Date.now();
|
|
const last = seenTrades.get(key);
|
|
if (last && (nowMs - last) < 1500) {
|
|
return;
|
|
}
|
|
seenTrades.set(key, nowMs);
|
|
const ts = typeof tr.timestamp === 'number' ? new Date(tr.timestamp*1000).toLocaleTimeString() : '';
|
|
const lastTick = (state[tr.symbol] && state[tr.symbol].ticks && state[tr.symbol].ticks.data.length) ? Number(state[tr.symbol].ticks.data[state[tr.symbol].ticks.data.length-1]) : Number(tr.price||0);
|
|
const csize = tr.symbol && tr.symbol.includes('XAU') ? 100.0 : 100000.0;
|
|
const sign = String(tr.signal||'').toUpperCase()==='BUY' ? 1.0 : -1.0;
|
|
const pnlVal = ((lastTick - Number(tr.price||0)) * sign * csize * Number(tr.lot||0));
|
|
const pnlFmt = (isFinite(pnlVal) ? pnlVal.toFixed(2) : '—');
|
|
let tb = document.getElementById(`trade-blotter-${tr.symbol}`);
|
|
if (!tb) {
|
|
let dyn = document.getElementById('dynamic-blotters');
|
|
if (!dyn) {
|
|
dyn = document.createElement('div');
|
|
dyn.id = 'dynamic-blotters';
|
|
dyn.className = 'mt-4';
|
|
document.body.appendChild(dyn);
|
|
}
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'bg-gray-800 p-2 rounded mb-4 border border-gray-700';
|
|
const title = document.createElement('div');
|
|
title.className = 'text-sm text-gray-400 mb-2';
|
|
title.textContent = `Trades ${tr.symbol}`;
|
|
const table = document.createElement('table');
|
|
const tbody = document.createElement('tbody');
|
|
tbody.id = `trade-blotter-${tr.symbol}`;
|
|
table.appendChild(tbody);
|
|
wrap.appendChild(title);
|
|
wrap.appendChild(table);
|
|
dyn.prepend(wrap);
|
|
tb = tbody;
|
|
}
|
|
const trEl = document.createElement('tr');
|
|
trEl.id = rowId;
|
|
trEl.className = 'hover:bg-gray-700 transition cursor-default';
|
|
trEl.dataset.entry = String(Number(tr.price||0));
|
|
trEl.dataset.lot = String(Number(tr.lot||0));
|
|
trEl.dataset.side = String(tr.signal||'');
|
|
trEl.dataset.csize = String(csize);
|
|
trEl.innerHTML = `
|
|
<td class="px-3 py-2 text-xs">${ts}</td>
|
|
<td class="px-3 py-2 font-bold ${tr.signal === 'BUY' ? 'text-blue-400' : 'text-red-400'}">${tr.signal}</td>
|
|
<td class="px-3 py-2 text-right font-mono">${(tr.lot || 0).toFixed(2)}</td>
|
|
<td class="px-3 py-2 text-right font-mono">${(tr.price || 0).toFixed(tr.symbol.includes('XAU') ? 2 : 5)}</td>
|
|
<td class="px-3 py-2 text-right text-red-400 font-mono">${(tr.sl || 0).toFixed(tr.symbol.includes('XAU') ? 2 : 5)}</td>
|
|
<td class="px-3 py-2 text-right text-green-400 font-mono">${(tr.tp || 0).toFixed(tr.symbol.includes('XAU') ? 2 : 5)}</td>
|
|
<td class="px-3 py-2 text-right font-mono ${pnlVal>=0?'text-green-400':'text-red-400'}" data-pnlcell="1">${pnlFmt}</td>
|
|
<td class="px-3 py-2 text-xs text-gray-400">${tr.regime || '—'}</td>
|
|
<td class="px-3 py-2"><span class="px-2 py-0.5 rounded text-[10px] bg-blue-900 text-blue-200">${tr.status}</span></td>
|
|
<td class="px-3 py-2 text-xs font-mono text-gray-500">${tr.trade_id || ''}</td>`;
|
|
if (tb) {
|
|
tb.prepend(trEl);
|
|
if (tb.children.length > 20) tb.lastChild.remove();
|
|
}
|
|
} else if (msg.type === 'pnl_report' && msg.report && symbol && state[symbol]) {
|
|
const r = msg.report;
|
|
state[symbol].lastReport = r;
|
|
|
|
const fmt0 = (x) => (typeof x === 'number' && isFinite(x)) ? x.toFixed(0) : '—';
|
|
const fmt2 = (x) => (typeof x === 'number' && isFinite(x)) ? x.toFixed(2) : '—';
|
|
const pct = (x) => (typeof x === 'number' && isFinite(x)) ? (x*100).toFixed(2) + '%' : '—';
|
|
|
|
const times = useRealized ? (r.realized_equity_times||[]) : (r.equity_times||[]);
|
|
const values = useRealized ? (r.realized_equity_values||[]) : (r.equity_values||[]);
|
|
state[symbol].eqTimes = times.slice(-MAX_EQ_POINTS);
|
|
state[symbol].eqValues = values.slice(-MAX_EQ_POINTS);
|
|
|
|
const lastVal = (arr) => {
|
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
const x = arr[i];
|
|
if (typeof x === 'number' && isFinite(x)) return x;
|
|
}
|
|
return 0;
|
|
};
|
|
const pnlVal = lastVal(values);
|
|
const pnlEl = document.getElementById(`pnl-val-${symbol}`);
|
|
if (pnlEl) {
|
|
pnlEl.textContent = fmt2(pnlVal);
|
|
pnlEl.className = pnlVal >= 0 ? 'text-lg font-bold text-green-400' : 'text-lg font-bold text-red-400';
|
|
}
|
|
const wlEl = document.getElementById(`pnl-wl-${symbol}`);
|
|
if (wlEl) wlEl.textContent = fmt0(useRealized ? r.realized_wins : r.wins) + '/' + fmt0(useRealized ? r.realized_losses : r.losses);
|
|
|
|
if (eqCharts[symbol]) {
|
|
const labs = times.slice(-MAX_EQ_POINTS).map(n => new Date(n*1000).toLocaleTimeString());
|
|
const vals = values.slice(-MAX_EQ_POINTS);
|
|
eqCharts[symbol].data.labels = labs;
|
|
eqCharts[symbol].data.datasets[0].data = vals;
|
|
eqCharts[symbol].update('none');
|
|
}
|
|
updateCombinedEquity();
|
|
|
|
let totalWins = 0, totalLosses = 0, totalPnl = 0, maxDD = 0;
|
|
SYMBOLS.forEach(s => {
|
|
const rep = state[s].lastReport;
|
|
if (rep) {
|
|
totalWins += useRealized ? rep.realized_wins : rep.wins;
|
|
totalLosses += useRealized ? rep.realized_losses : rep.losses;
|
|
const vv = useRealized ? (state[s].eqValues || []) : (state[s].eqValues || []);
|
|
totalPnl += lastVal(vv);
|
|
maxDD = Math.max(maxDD, useRealized ? rep.realized_max_drawdown : rep.max_drawdown);
|
|
}
|
|
});
|
|
|
|
document.getElementById('pnl-wl').textContent = totalWins + '/' + totalLosses;
|
|
document.getElementById('pnl-avg').textContent = fmt2(totalPnl);
|
|
document.getElementById('pnl-dd').textContent = fmt2(maxDD) + '%';
|
|
let avgSharpe = 0, avgSortino = 0, count = 0;
|
|
SYMBOLS.forEach(s => {
|
|
const rep = state[s].lastReport;
|
|
if (rep) {
|
|
avgSharpe += useRealized ? rep.realized_sharpe : rep.sharpe;
|
|
avgSortino += useRealized ? rep.realized_sortino : rep.sortino;
|
|
count++;
|
|
}
|
|
});
|
|
if (count > 0) {
|
|
document.getElementById('pnl-sharpe').textContent = fmt2(avgSharpe/count) + ' / ' + fmt2(avgSortino/count);
|
|
}
|
|
|
|
const mc = document.getElementById('metrics-compare');
|
|
if (mc) {
|
|
const getRep = (s) => state[s].lastReport || {};
|
|
const fmtp = (x) => (typeof x === 'number' && isFinite(x)) ? x.toFixed(2) : '—';
|
|
const pct = (x) => (typeof x === 'number' && isFinite(x)) ? (x*100).toFixed(2) + '%' : '—';
|
|
const eu = getRep('EURUSDm'), xa = getRep('XAUUSDm');
|
|
const euWinRate = (eu.wins && (eu.wins+eu.losses)) ? (eu.wins/(eu.wins+eu.losses)) : 0;
|
|
const xaWinRate = (xa.wins && (xa.wins+xa.losses)) ? (xa.wins/(xa.wins+xa.losses)) : 0;
|
|
mc.innerHTML = '';
|
|
[['Sharpe', fmtp(useRealized ? eu.realized_sharpe : eu.sharpe), fmtp(useRealized ? xa.realized_sharpe : xa.sharpe)],
|
|
['Sortino', fmtp(useRealized ? eu.realized_sortino : eu.sortino), fmtp(useRealized ? xa.realized_sortino : xa.sortino)],
|
|
['Max DD', fmtp(useRealized ? eu.realized_max_drawdown : eu.max_drawdown), fmtp(useRealized ? xa.realized_max_drawdown : xa.max_drawdown)],
|
|
['Win Rate', pct(euWinRate), pct(xaWinRate)],
|
|
['Avg PnL', fmtp(useRealized ? eu.realized_avg_pnl : eu.avg_pnl), fmtp(useRealized ? xa.realized_avg_pnl : xa.avg_pnl)]].forEach(rw => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `<td class="px-3 py-2">${rw[0]}</td><td class="px-3 py-2">${rw[1]}</td><td class="px-3 py-2">${rw[2]}</td>`;
|
|
mc.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
const counts = r.regime_counts || {};
|
|
rdChart.data.labels = Object.keys(counts);
|
|
rdChart.data.datasets[0].data = Object.values(counts);
|
|
rdChart.update('none');
|
|
|
|
if (r.adv_metrics) {
|
|
// Display a few consolidated advanced metrics using this symbol as source
|
|
const adv = r.adv_metrics;
|
|
const ra = adv.risk_adjusted || {};
|
|
const dd = adv.drawdown || {};
|
|
const tb = adv.trade_based || {};
|
|
const st = adv.statistical || {};
|
|
const sq = adv.strategy_quality || {};
|
|
document.getElementById('adv-sharpe').textContent = fmt2(ra.sharpe_ratio||0);
|
|
document.getElementById('adv-sortino').textContent = fmt2(ra.sortino_ratio||0);
|
|
document.getElementById('adv-calmar').textContent = fmt2(ra.calmar_ratio||0);
|
|
document.getElementById('adv-pf').textContent = fmt2(tb.profit_factor||0);
|
|
document.getElementById('adv-winrate').textContent = pct(tb.win_rate||0);
|
|
document.getElementById('adv-maxdd').textContent = pct(dd.max_drawdown||0);
|
|
document.getElementById('adv-tail').textContent = fmt2(st.tail_ratio||0);
|
|
document.getElementById('adv-psr').textContent = ((sq.probabilistic_sharpe_ratio||0)*100).toFixed(1) + '%';
|
|
}
|
|
} else if (msg.type === 'monitor') {
|
|
lastHeartbeat = Number(msg.heartbeat||0);
|
|
const hbEl = document.getElementById('heartbeatText');
|
|
if (hbEl) {
|
|
const t = new Date((lastHeartbeat||0)*1000).toLocaleTimeString();
|
|
hbEl.textContent = t;
|
|
}
|
|
const stale = msg.stale_sec || {};
|
|
SYMBOLS.forEach(s => {
|
|
const sec = Number(stale[s]||0);
|
|
const el = document.getElementById(`stale-banner-${s}`);
|
|
if (el) {
|
|
if (sec > 30) {
|
|
el.classList.remove('hidden');
|
|
el.textContent = `Feed stalled for ${s} (${sec.toFixed(0)}s)`;
|
|
} else {
|
|
el.classList.add('hidden');
|
|
}
|
|
}
|
|
});
|
|
} else if (msg.type === 'portfolio_optimizer') {
|
|
try {
|
|
if (Array.isArray(msg.frontier)) {
|
|
updateEfficientFrontier(msg.frontier, msg.highlights || {});
|
|
}
|
|
if (msg.methods) {
|
|
updateMethodTable(msg.methods);
|
|
}
|
|
if (msg.health) {
|
|
const h = msg.health || {};
|
|
const mt = h.metrics || {};
|
|
const dr = Number(mt.diversification_ratio || 0);
|
|
const ena = Number(mt.effective_number_assets || 0);
|
|
const phm = document.getElementById('ph-method'); if (phm) phm.textContent = h.method || '—';
|
|
const phd = document.getElementById('ph-dr'); if (phd) phd.textContent = (dr>0?dr.toFixed(2):'—');
|
|
const phe = document.getElementById('ph-ena'); if (phe) phe.textContent = (ena>0?ena.toFixed(2):'—');
|
|
const wEl = document.getElementById('ph-weights'); const rcEl = document.getElementById('ph-rc');
|
|
if (wEl) {
|
|
wEl.innerHTML = '';
|
|
const w = h.weights || {};
|
|
Object.keys(w).forEach(k => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `<td class="px-2 py-1">${k}</td><td class="px-2 py-1 text-right">${(w[k]*100).toFixed(1)}%</td>`;
|
|
wEl.appendChild(tr);
|
|
});
|
|
}
|
|
if (rcEl) {
|
|
rcEl.innerHTML = '';
|
|
const rc = h.risk_contributions || {};
|
|
Object.keys(rc).forEach(k => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `<td class="px-2 py-1">${k}</td><td class="px-2 py-1 text-right">${(rc[k]*100).toFixed(1)}%</td>`;
|
|
rcEl.appendChild(tr);
|
|
});
|
|
}
|
|
const dd = document.getElementById('portfolioMethodDropdown');
|
|
if (dd && h.method) {
|
|
dd.value = h.method;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('portfolio_optimizer render error', e);
|
|
}
|
|
} else if (msg.type === 'config_update') {
|
|
const p = String(msg.parameter || '');
|
|
const rawVal = msg.value;
|
|
const v = Number(rawVal);
|
|
if (p === 'threshold') {
|
|
document.getElementById('cfg-threshold').value = String(v);
|
|
document.getElementById('cfg-threshold-val').textContent = v.toFixed(2);
|
|
} else if (p === 'risk_per_trade') {
|
|
document.getElementById('cfg-risk').value = v.toFixed(3);
|
|
} else if (p === 'atr_multiplier') {
|
|
document.getElementById('cfg-atr').value = v.toFixed(1);
|
|
} else if (p === 'rr') {
|
|
document.getElementById('cfg-rr').value = v.toFixed(1);
|
|
} else if (p === 'adv_metrics_enabled') {
|
|
document.getElementById('cfg-adv-enabled').checked = v > 0.5;
|
|
} else if (p === 'metrics_interval_sec') {
|
|
document.getElementById('cfg-adv-interval').value = String(parseInt(v));
|
|
} else if (p === 'retail_strategies_enabled') {
|
|
const chk = document.getElementById('cfg-retail-enabled'); if (chk) chk.checked = v > 0.5;
|
|
} else if (p === 'retail_rr') {
|
|
const el = document.getElementById('cfg-retail-rr'); if (el) el.value = v.toFixed(1);
|
|
} else if (p === 'retail_min_conf') {
|
|
const el = document.getElementById('cfg-retail-minconf'); if (el) el.value = v.toFixed(2);
|
|
} else if (p === 'retail_merge_mode') {
|
|
const el = document.getElementById('cfg-retail-mode'); if (el) el.value = (typeof rawVal === 'string') ? rawVal : String(rawVal);
|
|
} else if (p === 'hft_optimizer_enabled') {
|
|
const chk = document.getElementById('cfg-hft-enabled'); if (chk) chk.checked = v > 0.5;
|
|
hftEnabled = v > 0.5;
|
|
const dot = document.getElementById('status-hft'); if (dot) { if (hftEnabled) { dot.className = 'px-2 py-0.5 text-xs rounded bg-green-700 text-green-100'; dot.textContent = 'ON'; } else { dot.className = 'px-2 py-0.5 text-xs rounded bg-red-700 text-red-100'; dot.textContent = 'OFF'; } }
|
|
const p1 = document.getElementById('panel-hft-optimizer'); if (p1) { if (hftEnabled) p1.classList.remove('hidden'); else p1.classList.add('hidden'); }
|
|
const p2 = document.getElementById('panel-hft-exec'); if (p2) { if (hftEnabled) p2.classList.remove('hidden'); else p2.classList.add('hidden'); }
|
|
} else if (p === 'ga_evaluator_enabled') {
|
|
const chk = document.getElementById('cfg-ga-enabled'); if (chk) chk.checked = v > 0.5;
|
|
gaEnabled = v > 0.5;
|
|
const dot = document.getElementById('status-ga'); if (dot) { if (gaEnabled) { dot.className = 'px-2 py-0.5 text-xs rounded bg-green-700 text-green-100'; dot.textContent = 'ON'; } else { dot.className = 'px-2 py-0.5 text-xs rounded bg-red-700 text-red-100'; dot.textContent = 'OFF'; } }
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
};
|
|
ws.onclose = () => {
|
|
setStatus('disconnected');
|
|
setTimeout(connect, 2000);
|
|
};
|
|
}
|
|
|
|
// Event Listeners
|
|
document.getElementById('submit').onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const login = parseInt(document.getElementById('login').value || '0', 10);
|
|
const password = document.getElementById('password').value || '';
|
|
const server = document.getElementById('server').value || '';
|
|
ws.send(JSON.stringify({ type: 'login', login, password, server }));
|
|
};
|
|
|
|
document.querySelectorAll('.toggle-tick').forEach(el => {
|
|
el.onchange = (e) => {
|
|
const sym = e.target.dataset.symbol;
|
|
state[sym].chart.data.datasets[0].hidden = !e.target.checked;
|
|
state[sym].chart.update('none');
|
|
};
|
|
});
|
|
document.querySelectorAll('.toggle-m1').forEach(el => {
|
|
el.onchange = (e) => {
|
|
const sym = e.target.dataset.symbol;
|
|
state[sym].chart.data.datasets[1].hidden = !e.target.checked;
|
|
state[sym].chart.update('none');
|
|
};
|
|
});
|
|
document.querySelectorAll('.toggle-m5').forEach(el => {
|
|
el.onchange = (e) => {
|
|
const sym = e.target.dataset.symbol;
|
|
state[sym].chart.data.datasets[2].hidden = !e.target.checked;
|
|
state[sym].chart.update('none');
|
|
};
|
|
});
|
|
|
|
const toggleRealizedEl = document.getElementById('toggle-realized-equity');
|
|
if (toggleRealizedEl) {
|
|
toggleRealizedEl.onchange = (e) => {
|
|
useRealized = !!e.target.checked;
|
|
SYMBOLS.forEach(s => {
|
|
const rep = state[s].lastReport;
|
|
if (rep) {
|
|
// Trigger a fake message update to refresh UI
|
|
const ev = { data: JSON.stringify({ type: 'pnl_report', symbol: s, report: rep }) };
|
|
ws.onmessage(ev);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
document.getElementById('cfg-threshold').oninput = (e) => {
|
|
document.getElementById('cfg-threshold-val').textContent = Number(e.target.value).toFixed(2);
|
|
};
|
|
|
|
document.getElementById('apply-risk').onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const data = {
|
|
threshold: Number(document.getElementById('cfg-threshold').value),
|
|
risk: Number(document.getElementById('cfg-risk').value),
|
|
atr: Number(document.getElementById('cfg-atr').value),
|
|
rr: Number(document.getElementById('cfg-rr').value)
|
|
};
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'threshold', value: data.threshold }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'risk_per_trade', value: data.risk }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'atr_multiplier', value: data.atr }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'rr', value: data.rr }));
|
|
};
|
|
|
|
document.getElementById('apply-adv').onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const enabled = !!document.getElementById('cfg-adv-enabled').checked;
|
|
const ival = parseInt(document.getElementById('cfg-adv-interval').value || '120', 10);
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'adv_metrics_enabled', value: enabled }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'metrics_interval_sec', value: ival }));
|
|
};
|
|
|
|
document.getElementById('apply-retail').onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const enabled = !!document.getElementById('cfg-retail-enabled').checked;
|
|
const mode = String(document.getElementById('cfg-retail-mode').value || 'augment');
|
|
const minc = Number(document.getElementById('cfg-retail-minconf').value || '0.55');
|
|
const rr = Number(document.getElementById('cfg-retail-rr').value || '1.5');
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'retail_strategies_enabled', value: enabled }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'retail_merge_mode', value: mode }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'retail_min_conf', value: minc }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'retail_rr', value: rr }));
|
|
};
|
|
|
|
const pmDrop = document.getElementById('portfolioMethodDropdown');
|
|
if (pmDrop) {
|
|
pmDrop.onchange = (e) => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const val = String(e.target.value || '');
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'portfolio_selected_method', value: val }));
|
|
};
|
|
}
|
|
|
|
const ttBtn = document.getElementById('tt-send');
|
|
if (ttBtn) {
|
|
ttBtn.onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const payload = {
|
|
symbol: String(document.getElementById('tt-symbol').value || 'EURUSDm'),
|
|
side: String(document.getElementById('tt-side').value || 'BUY'),
|
|
lot: Number(document.getElementById('tt-lot').value || '0.01'),
|
|
sl_pips: Number(document.getElementById('tt-sl').value || '20'),
|
|
tp_rr: Number(document.getElementById('tt-rr').value || '2.0'),
|
|
override_regime: String(document.getElementById('tt-regime').value || '')
|
|
};
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'force_trade', value: payload }));
|
|
};
|
|
}
|
|
document.getElementById('reset-config').onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'threshold', value: 0.60 }));
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'risk_per_trade', value: 0.01 }));
|
|
};
|
|
const hftChk = document.getElementById('cfg-hft-enabled');
|
|
if (hftChk) {
|
|
hftChk.onchange = (e) => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const enabled = !!e.target.checked;
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'hft_optimizer_enabled', value: enabled }));
|
|
};
|
|
}
|
|
const gaChk = document.getElementById('cfg-ga-enabled');
|
|
if (gaChk) {
|
|
gaChk.onchange = (e) => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const enabled = !!e.target.checked;
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'ga_evaluator_enabled', value: enabled }));
|
|
};
|
|
}
|
|
|
|
const btBtn = document.getElementById('bt-run');
|
|
if (btBtn) {
|
|
btBtn.onclick = () => {
|
|
if (!ws || ws.readyState !== 1) return;
|
|
const sym = document.getElementById('bt-symbol').value;
|
|
const tf = document.getElementById('bt-tf').value;
|
|
const start = document.getElementById('bt-start').value || '2016-01-01';
|
|
const end = document.getElementById('bt-end').value || '2025-12-31';
|
|
const mode = document.getElementById('bt-mode').value || 'retail';
|
|
ws.send(JSON.stringify({ type: 'config_update', parameter: 'run_backtest', value: { symbol: sym, timeframe: tf, start, end, strategy_mode: mode }}));
|
|
};
|
|
}
|
|
|
|
connect();
|
|
setTimeout(() => {
|
|
try {
|
|
if (typeof frontierChart !== 'undefined' && frontierChart.data.datasets.length === 0) {
|
|
const demoFrontier = [];
|
|
for (let i=6;i<=20;i+=2) {
|
|
demoFrontier.push({ method: 'frontier', metrics: { expected_return_annual: i/100, volatility_annual: (i/100)*0.8, sharpe_ratio_annual: ((i/100)/((i/100)*0.8)), diversification_ratio: 1.2 }, weights: {'EURUSDm': 0.6, 'XAUUSDm': 0.4} });
|
|
}
|
|
demoFrontier.push({ method: 'min_variance', metrics: { expected_return_annual: 0.07, volatility_annual: 0.06, sharpe_ratio_annual: 1.17, diversification_ratio: 1.3 }, weights: {'EURUSDm':0.8,'XAUUSDm':0.2} });
|
|
demoFrontier.push({ method: 'max_sharpe', metrics: { expected_return_annual: 0.18, volatility_annual: 0.12, sharpe_ratio_annual: 1.50, diversification_ratio: 1.25 }, weights: {'EURUSDm':0.3,'XAUUSDm':0.7} });
|
|
demoFrontier.push({ method: 'risk_parity', metrics: { expected_return_annual: 0.12, volatility_annual: 0.09, sharpe_ratio_annual: 1.33, diversification_ratio: 1.4 }, weights: {'EURUSDm':0.5,'XAUUSDm':0.5} });
|
|
updateEfficientFrontier(demoFrontier, {});
|
|
const demoMethods = {
|
|
'equal_weight': { weights: {'EURUSDm':0.5,'XAUUSDm':0.5}, theoretical: { expected_return_annual: 0.12, volatility_annual: 0.10, sharpe_ratio_annual: 1.20, max_drawdown: 0.06, diversification_ratio: 1.3, concentration_index: 0.5, effective_number_assets: 2.0 } },
|
|
'min_variance': { weights: {'EURUSDm':0.8,'XAUUSDm':0.2}, theoretical: { expected_return_annual: 0.07, volatility_annual: 0.06, sharpe_ratio_annual: 1.17, max_drawdown: 0.04, diversification_ratio: 1.25, concentration_index: 0.68, effective_number_assets: 1.47 } },
|
|
'max_sharpe': { weights: {'EURUSDm':0.3,'XAUUSDm':0.7}, theoretical: { expected_return_annual: 0.18, volatility_annual: 0.12, sharpe_ratio_annual: 1.50, max_drawdown: 0.09, diversification_ratio: 1.2, concentration_index: 0.58, effective_number_assets: 1.72 } },
|
|
'risk_parity': { weights: {'EURUSDm':0.5,'XAUUSDm':0.5}, theoretical: { expected_return_annual: 0.12, volatility_annual: 0.09, sharpe_ratio_annual: 1.33, max_drawdown: 0.06, diversification_ratio: 1.4, concentration_index: 0.5, effective_number_assets: 2.0 } },
|
|
'max_diversification': { weights: {'EURUSDm':0.7,'XAUUSDm':0.3}, theoretical: { expected_return_annual: 0.10, volatility_annual: 0.08, sharpe_ratio_annual: 1.25, max_drawdown: 0.05, diversification_ratio: 1.45, concentration_index: 0.58, effective_number_assets: 1.72 } },
|
|
'hrp': { weights: {'EURUSDm':0.85,'XAUUSDm':0.15}, theoretical: { expected_return_annual: 0.09, volatility_annual: 0.07, sharpe_ratio_annual: 1.29, max_drawdown: 0.045, diversification_ratio: 1.32, concentration_index: 0.75, effective_number_assets: 1.33 } },
|
|
'black_litterman': { weights: {'EURUSDm':0.5,'XAUUSDm':0.5}, theoretical: { expected_return_annual: 0.12, volatility_annual: 0.10, sharpe_ratio_annual: 1.20, max_drawdown: 0.06, diversification_ratio: 1.3, concentration_index: 0.5, effective_number_assets: 2.0 } }
|
|
};
|
|
updateMethodTable(demoMethods);
|
|
}
|
|
} catch (e) {}
|
|
}, 3000);
|
|
setInterval(() => {
|
|
const now = Math.floor(Date.now()/1000);
|
|
const el = document.getElementById('global-alert');
|
|
if (!el) return;
|
|
if (now - lastHeartbeat > 60) {
|
|
el.classList.remove('hidden');
|
|
} else {
|
|
el.classList.add('hidden');
|
|
}
|
|
}, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|