Files
btc-trading/src/api/dashboard/static/index.html
2026-02-17 10:39:39 +01:00

3183 lines
124 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Trading Dashboard</title>
<link rel="icon" href="data:,">
<script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js"></script>
<style>
:root {
--tv-bg: #131722;
--tv-panel-bg: #1e222d;
--tv-border: #2a2e39;
--tv-text: #d1d4dc;
--tv-text-secondary: #787b86;
--tv-green: #26a69a;
--tv-red: #ef5350;
--tv-blue: #2962ff;
--tv-hover: #2a2e39;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--tv-bg);
color: var(--tv-text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.toolbar {
background: var(--tv-panel-bg);
border-bottom: 1px solid var(--tv-border);
padding: 8px 16px;
display: flex;
align-items: center;
gap: 16px;
height: 56px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
overflow: hidden;
}
.symbol-badge {
background: var(--tv-bg);
padding: 6px 12px;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
border: 1px solid var(--tv-border);
white-space: nowrap;
}
.timeframe-scroll {
display: flex;
gap: 2px;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: var(--tv-border) transparent;
flex: 1;
}
.timeframe-scroll::-webkit-scrollbar {
height: 4px;
}
.timeframe-scroll::-webkit-scrollbar-track {
background: transparent;
}
.timeframe-scroll::-webkit-scrollbar-thumb {
background: var(--tv-border);
border-radius: 2px;
}
.timeframe-btn {
background: transparent;
border: none;
color: var(--tv-text-secondary);
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
min-width: 36px;
}
.timeframe-btn:hover {
background: var(--tv-hover);
color: var(--tv-text);
}
.timeframe-btn.active {
background: var(--tv-blue);
color: white;
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
white-space: nowrap;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--tv-green);
}
.status-text {
font-size: 12px;
color: var(--tv-text-secondary);
}
.stats-panel {
background: var(--tv-panel-bg);
border-bottom: 1px solid var(--tv-border);
padding: 8px 16px;
display: flex;
gap: 32px;
height: 44px;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 10px;
color: var(--tv-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
}
.stat-value.positive { color: var(--tv-green); }
.stat-value.negative { color: var(--tv-red); }
.main-container {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
}
.chart-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.chart-wrapper {
flex: 2;
position: relative;
background: var(--tv-bg);
min-height: 0;
}
#chart {
width: 100%;
height: 100%;
}
.ta-panel {
flex: 1;
background: var(--tv-panel-bg);
border-top: 1px solid var(--tv-border);
display: flex;
flex-direction: column;
}
/* Right Sidebar - Strategy Simulation */
.right-sidebar {
width: 350px;
background: var(--tv-panel-bg);
border-left: 1px solid var(--tv-border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: width 0.3s ease;
}
.right-sidebar.collapsed {
width: 44px;
min-width: 44px;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--tv-border);
background: var(--tv-bg);
min-height: 48px;
}
.right-sidebar.collapsed .sidebar-header {
justify-content: center;
padding: 12px 4px;
}
.sidebar-title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.right-sidebar.collapsed .sidebar-title {
display: none;
}
.sidebar-toggle {
background: transparent;
border: none;
color: var(--tv-text);
cursor: pointer;
padding: 4px;
font-size: 16px;
transition: transform 0.3s ease;
min-width: 24px;
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-toggle:hover {
background: var(--tv-hover);
border-radius: 4px;
}
.right-sidebar.collapsed .sidebar-toggle {
transform: rotate(180deg);
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.right-sidebar.collapsed .sidebar-content {
display: none;
}
.sidebar-section {
margin-bottom: 16px;
border: 1px solid var(--tv-border);
border-radius: 6px;
overflow: hidden;
}
.sidebar-section-header {
background: var(--tv-bg);
padding: 10px 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--tv-border);
display: flex;
align-items: center;
gap: 6px;
}
.sidebar-section-content {
padding: 12px;
}
/* Strategy Config Form */
.config-group {
margin-bottom: 12px;
}
.config-label {
display: block;
font-size: 11px;
color: var(--tv-text-secondary);
text-transform: uppercase;
margin-bottom: 4px;
}
.config-input {
width: 100%;
background: var(--tv-bg);
border: 1px solid var(--tv-border);
border-radius: 4px;
padding: 8px;
color: var(--tv-text);
font-size: 13px;
}
.config-input:focus {
outline: none;
border-color: var(--tv-blue);
}
/* Strategy List */
.strategy-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
border-bottom: 1px solid var(--tv-border);
cursor: pointer;
transition: background 0.2s;
}
.strategy-item:last-child {
border-bottom: none;
}
.strategy-item:hover {
background: var(--tv-hover);
}
.strategy-item.selected {
background: rgba(41, 98, 255, 0.1);
border-left: 3px solid var(--tv-blue);
}
.strategy-radio {
width: 16px;
height: 16px;
accent-color: var(--tv-blue);
}
.strategy-name {
flex: 1;
font-size: 13px;
}
.strategy-info {
color: var(--tv-text-secondary);
font-size: 14px;
cursor: help;
}
/* Results Panel */
.results-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.result-stat {
background: var(--tv-bg);
padding: 8px;
border-radius: 4px;
text-align: center;
}
.result-stat-value {
font-size: 18px;
font-weight: 600;
}
.result-stat-label {
font-size: 10px;
color: var(--tv-text-secondary);
text-transform: uppercase;
}
/* Equity Sparkline */
.equity-sparkline {
height: 60px;
background: var(--tv-bg);
border-radius: 4px;
margin-bottom: 12px;
position: relative;
}
/* Action Buttons */
.action-btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
margin-bottom: 8px;
}
.action-btn:hover {
opacity: 0.9;
}
.action-btn.primary {
background: var(--tv-blue);
color: white;
}
.action-btn.secondary {
background: var(--tv-bg);
color: var(--tv-text);
border: 1px solid var(--tv-border);
}
.action-btn.success {
background: var(--tv-green);
color: white;
}
/* Saved Simulations List */
.saved-sim-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
background: var(--tv-bg);
border-radius: 4px;
margin-bottom: 6px;
font-size: 12px;
}
.saved-sim-name {
flex: 1;
cursor: pointer;
}
.saved-sim-name:hover {
color: var(--tv-blue);
}
.saved-sim-actions {
display: flex;
gap: 4px;
}
.sim-action-btn {
background: transparent;
border: none;
color: var(--tv-text-secondary);
cursor: pointer;
padding: 2px 4px;
font-size: 12px;
}
.sim-action-btn:hover {
color: var(--tv-text);
}
/* Export Dialog */
.export-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--tv-panel-bg);
border: 1px solid var(--tv-border);
border-radius: 8px;
padding: 20px;
z-index: 10000;
min-width: 300px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.export-dialog-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
}
.export-options {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.export-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: var(--tv-bg);
border-radius: 4px;
cursor: pointer;
}
.export-option:hover {
background: var(--tv-hover);
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 9999;
}
/* Price Scale Controls */
.price-scale-controls {
position: absolute;
right: 10px;
bottom: 10px;
display: flex;
flex-direction: row;
gap: 2px;
z-index: 10;
opacity: 0;
transition: opacity 0.2s;
}
.chart-wrapper:hover .price-scale-controls {
opacity: 1;
}
.ps-control-btn {
width: 20px;
height: 20px;
background: rgba(42, 46, 57, 0.9);
border: 1px solid #363c4e;
color: #d1d4dc;
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
transition: all 0.2s;
padding: 0;
}
.ps-control-btn:hover {
background: #363c4e;
border-color: #4a4f5e;
}
.ps-control-btn.active {
background: #2962ff;
border-color: #2962ff;
color: white;
}
.ha-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--tv-border);
background: var(--tv-bg);
}
.ta-title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.ta-interval {
background: var(--tv-blue);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
}
.ta-actions {
display: flex;
gap: 8px;
}
.ta-btn {
background: var(--tv-bg);
border: 1px solid var(--tv-border);
color: var(--tv-text);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.ta-btn:hover {
background: var(--tv-hover);
border-color: var(--tv-blue);
}
.ta-btn.ai-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
}
.ta-btn.ai-btn:hover {
opacity: 0.9;
}
.ta-last-update {
font-size: 11px;
color: var(--tv-text-secondary);
}
.ta-content {
flex: 1;
padding: 16px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
overflow-y: auto;
}
.ta-section {
background: var(--tv-bg);
border: 1px solid var(--tv-border);
border-radius: 8px;
padding: 12px;
}
.ta-section-title {
font-size: 11px;
color: var(--tv-text-secondary);
text-transform: uppercase;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.ta-trend {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
}
.ta-trend.bullish { color: var(--tv-green); }
.ta-trend.bearish { color: var(--tv-red); }
.ta-trend.neutral { color: var(--tv-text-secondary); }
.ta-strength {
font-size: 12px;
color: var(--tv-text-secondary);
margin-top: 4px;
}
.ta-signal {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-top: 8px;
}
.ta-signal.buy {
background: rgba(38, 166, 154, 0.2);
color: var(--tv-green);
}
.ta-signal.sell {
background: rgba(239, 83, 80, 0.2);
color: var(--tv-red);
}
.ta-signal.hold {
background: rgba(120, 123, 134, 0.2);
color: var(--tv-text-secondary);
}
.ta-ma-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--tv-border);
font-size: 13px;
}
.ta-ma-row:last-child {
border-bottom: none;
}
.ta-ma-label {
color: var(--tv-text-secondary);
}
.ta-ma-value {
font-weight: 600;
}
.ta-ma-change {
font-size: 11px;
margin-left: 4px;
}
.ta-ma-change.positive { color: var(--tv-green); }
.ta-ma-change.negative { color: var(--tv-red); }
.ta-level {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
}
.ta-level-label {
color: var(--tv-text-secondary);
}
.ta-level-value {
font-weight: 600;
font-family: 'Courier New', monospace;
}
.ta-position-bar {
width: 100%;
height: 6px;
background: var(--tv-border);
border-radius: 3px;
margin-top: 8px;
position: relative;
}
.ta-position-marker {
position: absolute;
width: 12px;
height: 12px;
background: var(--tv-blue);
border-radius: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid var(--tv-bg);
}
.ta-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--tv-text-secondary);
font-size: 14px;
}
.ta-error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--tv-red);
font-size: 14px;
}
/* Simulation Panel Styles */
.sim-strategies {
display: flex;
flex-direction: column;
gap: 4px;
margin: 8px 0;
}
.sim-strategy-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.sim-strategy-option:hover {
background: var(--tv-hover);
}
.sim-strategy-option input[type="radio"] {
cursor: pointer;
}
.sim-strategy-option label {
cursor: pointer;
flex: 1;
font-size: 13px;
}
.sim-strategy-info {
color: var(--tv-text-secondary);
cursor: help;
position: relative;
font-size: 14px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
}
.sim-strategy-info:hover {
background: var(--tv-hover);
color: var(--tv-text);
}
/* Tooltip */
.sim-strategy-info:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
right: 0;
background: var(--tv-panel-bg);
border: 1px solid var(--tv-border);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
width: 250px;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
color: var(--tv-text);
margin-bottom: 4px;
line-height: 1.4;
pointer-events: none;
}
.sim-input-group {
margin-bottom: 12px;
}
.sim-input-group label {
display: block;
font-size: 11px;
color: var(--tv-text-secondary);
margin-bottom: 4px;
text-transform: uppercase;
}
.sim-input {
width: 100%;
padding: 8px 12px;
background: var(--tv-bg);
border: 1px solid var(--tv-border);
border-radius: 4px;
color: var(--tv-text);
font-size: 13px;
font-family: inherit;
}
.sim-input:focus {
outline: none;
border-color: var(--tv-blue);
}
.sim-run-btn {
width: 100%;
padding: 10px;
background: var(--tv-blue);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: opacity 0.2s;
margin-top: 8px;
}
.sim-run-btn:hover:not(:disabled) {
opacity: 0.9;
}
.sim-run-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.sim-results {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--tv-border);
}
.sim-stat-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
}
.sim-stat-row span:first-child {
color: var(--tv-text-secondary);
}
.sim-value {
font-weight: 600;
font-family: 'Courier New', monospace;
}
.sim-value.positive { color: var(--tv-green); }
.sim-value.negative { color: var(--tv-red); }
.loading-strategies {
padding: 12px;
text-align: center;
color: var(--tv-text-secondary);
font-size: 12px;
}
@media (max-width: 1400px) {
.ta-content {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1000px) {
.ta-content {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.ta-content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="toolbar">
<div class="toolbar-left">
<span class="symbol-badge">BTC/USD</span>
<div class="timeframe-scroll" id="timeframeContainer">
<!-- Timeframes will be inserted here by JS -->
</div>
</div>
<div class="connection-status">
<div class="status-dot" id="statusDot"></div>
<span class="status-text" id="statusText">Live</span>
</div>
</div>
<div class="stats-panel">
<div class="stat-item">
<span class="stat-label">Price</span>
<span class="stat-value" id="currentPrice">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Change</span>
<span class="stat-value" id="priceChange">--</span>
</div>
<div class="stat-item">
<span class="stat-label">High</span>
<span class="stat-value" id="dailyHigh">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Low</span>
<span class="stat-value" id="dailyLow">--</span>
</div>
</div>
<div class="main-container">
<div class="chart-area">
<div class="chart-wrapper">
<div id="chart"></div>
<div class="price-scale-controls" id="priceScaleControls">
<button class="ps-control-btn auto-scale active" id="btnAutoScale" title="Auto Scale (A)">A</button>
<button class="ps-control-btn log-scale" id="btnLogScale" title="Logarithmic Scale">L</button>
</div>
</div>
<div class="ta-panel" id="taPanel">
<div class="ha-header">
<div class="ta-title">
Technical Analysis
<span class="ta-interval" id="taInterval">1D</span>
</div>
<div class="ta-actions">
<span class="ta-last-update" id="taLastUpdate">--</span>
<button class="ta-btn ai-btn" id="aiBtn" onclick="openAIAnalysis()">
🤖 AI Analysis
</button>
<button class="ta-btn" onclick="refreshTA()">
🔄 Refresh
</button>
</div>
</div>
<div class="ta-content" id="taContent">
<div class="ta-loading">Loading technical analysis...</div>
</div>
</div>
</div>
<!-- Right Sidebar - Strategy Simulation -->
<div class="right-sidebar" id="rightSidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<span>🎯</span>
<span class="sidebar-title-text">Strategy Sim</span>
</div>
<button class="sidebar-toggle" onclick="toggleSidebar()"></button>
</div>
<div class="sidebar-content">
<!-- Strategy Selection -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>📋</span> Select Strategy
</div>
<div class="sidebar-section-content" id="strategyList">
<div class="loading-strategies" style="text-align: center; color: var(--tv-text-secondary); padding: 20px;">
Loading strategies...
</div>
</div>
</div>
<!-- Configuration -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>⚙️</span> Configuration
</div>
<div class="sidebar-section-content">
<div class="config-group">
<label class="config-label">Start Date</label>
<input type="datetime-local" id="simStartDate" class="config-input">
</div>
<div class="config-group">
<label class="config-label">Confirmation TF (Optional)</label>
<select id="simSecondaryTF" class="config-input">
<option value="">None</option>
<option value="1h">1h</option>
<option value="4h">4h</option>
<option value="1d" selected>1d</option>
<option value="1w">1w</option>
</select>
</div>
<div class="config-group">
<label class="config-label">Risk % per Trade</label>
<input type="number" id="simRiskPercent" class="config-input" value="2" min="0.1" max="100" step="0.1">
</div>
<div class="config-group">
<label class="config-label">Stop Loss %</label>
<input type="number" id="simStopLoss" class="config-input" value="2" min="0.1" max="20" step="0.1">
</div>
<!-- Dynamic Strategy Parameters -->
<div id="strategyParams"></div>
</div>
</div>
<!-- Run Button -->
<button class="action-btn primary" onclick="runSimulation()" id="runSimBtn" disabled>
▶ Run Simulation
</button>
<!-- Results Section -->
<div class="sidebar-section" id="resultsSection" style="display: none;">
<div class="sidebar-section-header">
<span>📊</span> Results
</div>
<div class="sidebar-section-content">
<!-- Equity Sparkline -->
<div class="equity-sparkline" id="equitySparkline"></div>
<!-- Stats -->
<div class="results-summary">
<div class="result-stat">
<div class="result-stat-value" id="simTrades">--</div>
<div class="result-stat-label">Trades</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="simWinRate">--</div>
<div class="result-stat-label">Win Rate</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="simPnL">--</div>
<div class="result-stat-label">Total P&L</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="simProfitFactor">--</div>
<div class="result-stat-label">Profit Factor</div>
</div>
</div>
<!-- Action Buttons -->
<button class="action-btn secondary" onclick="showSimulationMarkers()">
📍 Plot on Chart
</button>
<button class="action-btn secondary" onclick="saveSimulation()">
💾 Save Simulation
</button>
<button class="action-btn success" onclick="showExportDialog()">
📥 Export Report
</button>
</div>
</div>
<!-- Saved Simulations -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>💾</span> Saved Simulations
</div>
<div class="sidebar-section-content" id="savedSimulations">
<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">
No saved simulations
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// --- Indicator Library ---
class BaseIndicator {
constructor(config) {
this.name = config.name;
this.type = config.type;
this.params = config.params || {};
this.timeframe = config.timeframe || '1m';
}
calculate(candles) { throw new Error("Not implemented"); }
}
class SMAIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 44;
const results = new Array(candles.length).fill(null);
let sum = 0;
for (let i = 0; i < candles.length; i++) {
sum += candles[i].close;
if (i >= period) sum -= candles[i - period].close;
if (i >= period - 1) results[i] = sum / period;
}
return results;
}
}
class EMAIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 44;
const multiplier = 2 / (period + 1);
const results = new Array(candles.length).fill(null);
let ema = 0;
let sum = 0;
for (let i = 0; i < candles.length; i++) {
if (i < period) {
sum += candles[i].close;
if (i === period - 1) {
ema = sum / period;
results[i] = ema;
}
} else {
ema = (candles[i].close - ema) * multiplier + ema;
results[i] = ema;
}
}
return results;
}
}
class RSIIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 14;
const results = new Array(candles.length).fill(null);
let gains = 0, losses = 0;
for (let i = 1; i < candles.length; i++) {
const diff = candles[i].close - candles[i-1].close;
const gain = diff > 0 ? diff : 0;
const loss = diff < 0 ? -diff : 0;
if (i <= period) {
gains += gain;
losses += loss;
if (i === period) {
let avgGain = gains / period;
let avgLoss = losses / period;
results[i] = 100 - (100 / (1 + (avgGain / (avgLoss || 0.00001))));
}
} else {
const lastAvgGain = (results[i-1] ? (results[i-1] > 0 ? (period-1) * (results[i-1] * (gains+losses)/100) : 0) : 0); // Simplified
// Wilder's smoothing
// Note: This is a simplified RSI for MVP
gains = (gains * (period - 1) + gain) / period;
losses = (losses * (period - 1) + loss) / period;
results[i] = 100 - (100 / (1 + (gains / (losses || 0.00001))));
}
}
return results;
}
}
class BollingerBandsIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 20;
const stdDevMult = this.params.stdDev || 2;
const results = new Array(candles.length).fill(null);
for (let i = period - 1; i < candles.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) sum += candles[i-j].close;
const sma = sum / period;
let diffSum = 0;
for (let j = 0; j < period; j++) diffSum += Math.pow(candles[i-j].close - sma, 2);
const stdDev = Math.sqrt(diffSum / period);
results[i] = {
middle: sma,
upper: sma + (stdDevMult * stdDev),
lower: sma - (stdDevMult * stdDev)
};
}
return results;
}
}
class MACDIndicator extends BaseIndicator {
calculate(candles) {
const fast = this.params.fast || 12;
const slow = this.params.slow || 26;
const signal = this.params.signal || 9;
const fastEmaInd = new EMAIndicator({ params: { period: fast } });
const slowEmaInd = new EMAIndicator({ params: { period: slow } });
const fastEma = fastEmaInd.calculate(candles);
const slowEma = slowEmaInd.calculate(candles);
const macdLine = fastEma.map((f, i) => (f !== null && slowEma[i] !== null) ? f - slowEma[i] : null);
// Signal line is EMA of MACD line
const signalLine = new Array(candles.length).fill(null);
const multiplier = 2 / (signal + 1);
let ema = 0;
let sum = 0;
let count = 0;
for (let i = 0; i < macdLine.length; i++) {
if (macdLine[i] === null) continue;
count++;
if (count < signal) {
sum += macdLine[i];
} else if (count === signal) {
sum += macdLine[i];
ema = sum / signal;
signalLine[i] = ema;
} else {
ema = (macdLine[i] - ema) * multiplier + ema;
signalLine[i] = ema;
}
}
return macdLine.map((m, i) => ({
macd: m,
signal: signalLine[i],
histogram: (m !== null && signalLine[i] !== null) ? m - signalLine[i] : null
}));
}
}
class StochasticIndicator extends BaseIndicator {
calculate(candles) {
const kPeriod = this.params.kPeriod || 14;
const dPeriod = this.params.dPeriod || 3;
const results = new Array(candles.length).fill(null);
const kValues = new Array(candles.length).fill(null);
for (let i = kPeriod - 1; i < candles.length; i++) {
let lowest = Infinity;
let highest = -Infinity;
for (let j = 0; j < kPeriod; j++) {
lowest = Math.min(lowest, candles[i-j].low);
highest = Math.max(highest, candles[i-j].high);
}
const diff = highest - lowest;
kValues[i] = diff === 0 ? 50 : ((candles[i].close - lowest) / diff) * 100;
}
// D is SMA of K
for (let i = kPeriod + dPeriod - 2; i < candles.length; i++) {
let sum = 0;
for (let j = 0; j < dPeriod; j++) sum += kValues[i-j];
results[i] = { k: kValues[i], d: sum / dPeriod };
}
return results;
}
}
class ATRIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 14;
const results = new Array(candles.length).fill(null);
const tr = new Array(candles.length).fill(0);
for (let i = 1; i < candles.length; i++) {
const h_l = candles[i].high - candles[i].low;
const h_pc = Math.abs(candles[i].high - candles[i-1].close);
const l_pc = Math.abs(candles[i].low - candles[i-1].close);
tr[i] = Math.max(h_l, h_pc, l_pc);
}
let atr = 0;
let sum = 0;
for (let i = 1; i <= period; i++) sum += tr[i];
atr = sum / period;
results[period] = atr;
for (let i = period + 1; i < candles.length; i++) {
atr = (atr * (period - 1) + tr[i]) / period;
results[i] = atr;
}
return results;
}
}
// --- Risk Management ---
class RiskManager {
constructor(config, initialBalance = 1000) {
this.config = config || {
positionSizing: { method: 'percent', value: 0.1 }, // 10%
stopLoss: { enabled: true, method: 'percent', value: 0.02 }, // 2%
takeProfit: { enabled: true, method: 'percent', value: 0.04 } // 4%
};
this.balance = initialBalance;
this.equity = initialBalance;
}
calculateSize(price) {
if (this.config.positionSizing.method === 'percent') {
return (this.balance * this.config.positionSizing.value) / price;
}
return this.config.positionSizing.value / price;
}
}
// --- Strategy Engine ---
class ClientStrategyEngine {
constructor() {
this.indicatorTypes = {
'sma': SMAIndicator,
'ema': EMAIndicator,
'rsi': RSIIndicator,
'bb': BollingerBandsIndicator,
'macd': MACDIndicator,
'stoch': StochasticIndicator,
'atr': ATRIndicator
};
}
run(candlesMap, strategyConfig, riskConfig, simulationStart) {
const primaryTF = strategyConfig.timeframes?.primary || '1d';
const candles = candlesMap[primaryTF];
if (!candles) return { error: `No candles for primary timeframe ${primaryTF}` };
// 1. Calculate Indicators for all TFs
const indicatorResults = {};
console.log('Calculating indicators for timeframes:', Object.keys(candlesMap));
for (const tf in candlesMap) {
indicatorResults[tf] = {};
const tfCandles = candlesMap[tf];
const tfIndicators = (strategyConfig.indicators || []).filter(ind => (ind.timeframe || primaryTF) === tf);
console.log(` TF ${tf}: ${tfIndicators.length} indicators to calculate`);
for (const ind of tfIndicators) {
const IndicatorClass = this.indicatorTypes[ind.type];
if (IndicatorClass) {
const instance = new IndicatorClass(ind);
indicatorResults[tf][ind.name] = instance.calculate(tfCandles);
const validValues = indicatorResults[tf][ind.name].filter(v => v !== null).length;
console.log(` Calculated ${ind.name} on ${tf}: ${validValues} valid values`);
}
}
}
// 2. Run Simulation
const risk = new RiskManager(riskConfig);
const trades = [];
let position = null;
// Convert simulation start to seconds (to match candle timestamps)
const startTimeSec = Math.floor(new Date(simulationStart).getTime() / 1000);
console.log('Simulation start (seconds):', startTimeSec, 'Date:', simulationStart);
console.log('Total candles available:', candles.length);
console.log('First candle time:', candles[0].time, 'Last candle time:', candles[candles.length - 1].time);
// Optimized Pointer-based Timeframe Alignment
const pointers = {};
for (const tf in candlesMap) pointers[tf] = 0;
let processedCandles = 0;
for (let i = 1; i < candles.length; i++) {
const time = candles[i].time; // Already in seconds
const price = candles[i].close;
// Skip candles before simulation start (used for indicator warm-up)
if (time < startTimeSec) {
// Update pointers even for skipped candles
for (const tf in candlesMap) {
while (pointers[tf] < candlesMap[tf].length - 1 &&
candlesMap[tf][pointers[tf] + 1].time <= time) {
pointers[tf]++;
}
}
continue;
}
processedCandles++;
// Update pointers to current time
for (const tf in candlesMap) {
while (pointers[tf] < candlesMap[tf].length - 1 &&
candlesMap[tf][pointers[tf] + 1].time <= time) {
pointers[tf]++;
}
}
const signal = this.evaluate(i, pointers, candles, candlesMap, indicatorResults, strategyConfig, position);
if (signal === 'BUY' && !position) {
const size = risk.calculateSize(price);
position = { type: 'long', entryPrice: price, entryTime: candles[i].time, size };
} else if (signal === 'SELL' && position) {
const pnl = (price - position.entryPrice) * position.size;
trades.push({ ...position, exitPrice: price, exitTime: candles[i].time, pnl, pnlPct: (pnl / (position.entryPrice * position.size)) * 100 });
risk.balance += pnl;
position = null;
}
}
console.log(`Simulation complete: ${processedCandles} candles processed after start date, ${trades.length} trades`);
return {
total_trades: trades.length,
win_rate: (trades.filter(t => t.pnl > 0).length / (trades.length || 1)) * 100,
total_pnl: risk.balance - 1000,
trades
};
}
evaluate(index, pointers, candles, candlesMap, indicatorResults, config, position) {
const primaryTF = config.timeframes?.primary || '1d';
// Optimized getter using pointers
const getVal = (indName, tf) => {
const tfValues = indicatorResults[tf]?.[indName];
if (!tfValues) return null;
return tfValues[pointers[tf]];
};
const getPrice = (tf) => {
const tfCandles = candlesMap[tf];
if (!tfCandles) return null;
return tfCandles[pointers[tf]].close;
};
// Simple logic for MA Trend strategy
if (config.id === 'ma_trend') {
const period = config.params?.period || 44;
// Debug logging for first evaluation
if (index === 1) {
console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000));
console.log(`MA${period} value:`, getVal(`ma${period}`, primaryTF));
}
const maValue = getVal(`ma${period}`, primaryTF);
const price = candles[index].close;
// Optional: Multi-TF trend filter (must align for both entry and exit)
const secondaryTF = config.timeframes?.secondary?.[0];
let secondaryBullish = true;
let secondaryBearish = true;
if (secondaryTF) {
const secondaryPrice = getPrice(secondaryTF);
const secondaryMA = getVal(`ma${period}_${secondaryTF}`, secondaryTF);
if (secondaryPrice !== null && secondaryMA !== null) {
secondaryBullish = secondaryPrice > secondaryMA;
secondaryBearish = secondaryPrice < secondaryMA;
}
if (index === 1) {
console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, bullish=${secondaryBullish}, bearish=${secondaryBearish}`);
}
}
if (maValue) {
if (price > maValue && secondaryBullish) return 'BUY';
if (price < maValue && secondaryBearish) return 'SELL';
}
}
// Generic Logic for custom strategies
const evaluateConditions = (conds) => {
if (!conds || !conds.conditions) return false;
const results = conds.conditions.map(c => {
const targetTF = c.timeframe || primaryTF;
const leftVal = c.indicator === 'price' ? getPrice(targetTF) : getVal(c.indicator, targetTF);
const rightVal = typeof c.value === 'number' ? c.value : (c.value === 'price' ? getPrice(targetTF) : getVal(c.value, targetTF));
if (leftVal === null || rightVal === null) return false;
switch(c.operator) {
case '>': return leftVal > rightVal;
case '<': return leftVal < rightVal;
case '>=': return leftVal >= rightVal;
case '<=': return leftVal <= rightVal;
case '==': return leftVal == rightVal;
default: return false;
}
});
if (conds.logic === 'OR') return results.some(r => r);
return results.every(r => r);
};
if (evaluateConditions(config.entryLong)) return 'BUY';
if (evaluateConditions(config.exitLong)) return 'SELL';
return 'HOLD';
}
}
class TradingDashboard {
constructor() {
this.chart = null;
this.candleSeries = null;
this.currentInterval = '1d';
this.intervals = ['1m', '3m', '5m', '15m', '30m', '37m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M'];
this.allData = new Map();
this.isLoading = false;
this.hasInitialLoad = false;
this.taData = null;
this.init();
}
init() {
this.createTimeframeButtons();
this.initChart();
this.initEventListeners();
this.loadInitialData();
this.loadTA();
// Refresh every 10 seconds for new data
setInterval(() => {
this.loadNewData();
// Occasionally refresh TA
if (new Date().getSeconds() < 15) this.loadTA();
}, 10000);
}
isAtRightEdge() {
const timeScale = this.chart.timeScale();
const visibleRange = timeScale.getVisibleLogicalRange();
if (!visibleRange) return true;
const data = this.candleSeries.data();
if (!data || data.length === 0) return true;
// If the right-most visible bar is within 5 bars of the last data point
return visibleRange.to >= data.length - 5;
}
createTimeframeButtons() {
const container = document.getElementById('timeframeContainer');
container.innerHTML = ''; // Clear existing
this.intervals.forEach(interval => {
const btn = document.createElement('button');
btn.className = 'timeframe-btn';
btn.dataset.interval = interval;
btn.textContent = interval;
if (interval === this.currentInterval) {
btn.classList.add('active');
}
btn.addEventListener('click', () => this.switchTimeframe(interval));
container.appendChild(btn);
});
}
initChart() {
const chartContainer = document.getElementById('chart');
this.chart = LightweightCharts.createChart(chartContainer, {
layout: {
background: { color: '#131722' },
textColor: '#d1d4dc',
},
grid: {
vertLines: { color: '#2a2e39' },
horzLines: { color: '#2a2e39' },
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: '#2a2e39',
autoScale: true,
},
timeScale: {
borderColor: '#2a2e39',
timeVisible: true,
secondsVisible: false,
rightOffset: 12,
barSpacing: 10,
},
handleScroll: {
vertTouchDrag: false,
},
});
this.candleSeries = this.chart.addCandlestickSeries({
upColor: '#ff9800',
downColor: '#ff9800',
borderUpColor: '#ff9800',
borderDownColor: '#ff9800',
wickUpColor: '#ff9800',
wickDownColor: '#ff9800',
});
// Initialize price scale controls
this.initPriceScaleControls();
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
window.addEventListener('resize', () => {
this.chart.applyOptions({
width: chartContainer.clientWidth,
height: chartContainer.clientHeight,
});
});
// Handle tab visibility and focus
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.loadNewData();
this.loadTA();
}
});
window.addEventListener('focus', () => {
this.loadNewData();
this.loadTA();
});
}
initPriceScaleControls() {
const btnAutoScale = document.getElementById('btnAutoScale');
const btnLogScale = document.getElementById('btnLogScale');
if (!btnAutoScale || !btnLogScale) return;
// Track current state
this.priceScaleState = {
autoScale: true,
logScale: false
};
// Auto Scale toggle
btnAutoScale.addEventListener('click', () => {
this.priceScaleState.autoScale = !this.priceScaleState.autoScale;
btnAutoScale.classList.toggle('active', this.priceScaleState.autoScale);
this.candleSeries.priceScale().applyOptions({
autoScale: this.priceScaleState.autoScale
});
console.log('Auto Scale:', this.priceScaleState.autoScale ? 'ON' : 'OFF');
});
// Log Scale toggle
btnLogScale.addEventListener('click', () => {
this.priceScaleState.logScale = !this.priceScaleState.logScale;
btnLogScale.classList.toggle('active', this.priceScaleState.logScale);
// Get current visible price range and time range before changing mode
let currentPriceRange = null;
let currentTimeRange = null;
if (!this.priceScaleState.autoScale) {
try {
currentPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
} catch (e) {
console.log('Could not get price range');
}
}
try {
currentTimeRange = this.chart.timeScale().getVisibleLogicalRange();
} catch (e) {
console.log('Could not get time range');
}
// Apply log/linear mode change
this.candleSeries.priceScale().applyOptions({
mode: this.priceScaleState.logScale ? LightweightCharts.PriceScaleMode.Logarithmic : LightweightCharts.PriceScaleMode.Normal
});
// Force chart recalculation and redraw
this.chart.applyOptions({});
// Restore ranges after mode change
setTimeout(() => {
// Restore time range (X-axis position)
if (currentTimeRange) {
try {
this.chart.timeScale().setVisibleLogicalRange(currentTimeRange);
} catch (e) {
console.log('Could not restore time range');
}
}
// Restore price range if auto-scale is off
if (!this.priceScaleState.autoScale && currentPriceRange) {
try {
this.candleSeries.priceScale().setVisiblePriceRange(currentPriceRange);
} catch (e) {
console.log('Could not restore price range');
}
}
}, 100);
console.log('Log Scale:', this.priceScaleState.logScale ? 'ON' : 'OFF');
});
// Keyboard shortcut 'A' for auto-scale
document.addEventListener('keydown', (e) => {
if (e.key === 'a' || e.key === 'A') {
if (e.target.tagName !== 'INPUT') {
btnAutoScale.click();
}
}
});
}
initEventListeners() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
const shortcuts = {
'1': '1m', '2': '3m', '3': '5m', '4': '15m', '5': '30m', '7': '37m',
'6': '1h', '8': '4h', '9': '8h', '0': '12h',
'd': '1d', 'D': '1d', 'w': '1w', 'W': '1w', 'm': '1M', 'M': '1M'
};
if (shortcuts[e.key]) {
this.switchTimeframe(shortcuts[e.key]);
}
});
}
async loadInitialData() {
await this.loadData(1000, true);
this.hasInitialLoad = true;
}
async loadData(limit = 1000, fitToContent = false) {
if (this.isLoading) return;
this.isLoading = true;
try {
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
const existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
if (fitToContent) {
this.chart.timeScale().fitContent();
} else if (visibleRange) {
this.chart.timeScale().setVisibleLogicalRange(visibleRange);
}
const latest = mergedData[mergedData.length - 1];
this.updateStats(latest);
}
} catch (error) {
console.error('Error loading data:', error);
} finally {
this.isLoading = false;
}
}
async loadNewData() {
if (!this.hasInitialLoad || this.isLoading) return;
try {
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const atEdge = this.isAtRightEdge();
// Get current data from series to find the last timestamp
const currentSeriesData = this.candleSeries.data();
const lastTimestamp = currentSeriesData.length > 0
? currentSeriesData[currentSeriesData.length - 1].time
: 0;
const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
// Only update with data that is newer than or equal to the last candle
// update() handles equal timestamps by updating the existing bar
chartData.forEach(candle => {
if (candle.time >= lastTimestamp) {
this.candleSeries.update(candle);
}
});
// Update cache
const existingData = this.allData.get(this.currentInterval) || [];
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
// If we were at the edge, scroll to show the new candle
if (atEdge) {
this.chart.timeScale().scrollToRealTime();
}
const latest = chartData[chartData.length - 1];
this.updateStats(latest);
}
} catch (error) {
console.error('Error loading new data:', error);
}
}
mergeData(existing, newData) {
const dataMap = new Map();
existing.forEach(c => dataMap.set(c.time, c));
newData.forEach(c => dataMap.set(c.time, c));
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
}
onVisibleRangeChange() {
if (!this.hasInitialLoad || this.isLoading) {
console.log('Skipping range change:', { hasInitialLoad: this.hasInitialLoad, isLoading: this.isLoading });
return;
}
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
if (!visibleRange) {
console.log('No visible range');
return;
}
const data = this.candleSeries.data();
if (!data || data.length === 0) {
console.log('No data available');
return;
}
// Check if we need to load more historical data
// Load when user scrolls within 50 bars of the leftmost visible data
const barsFromLeft = visibleRange.from;
const totalBars = data.length;
console.log('Visible range:', { from: visibleRange.from, to: visibleRange.to, barsFromLeft, totalBars });
// Trigger when within 50 bars of the earliest loaded data
if (barsFromLeft < 50) {
console.log('Near left edge (within 50 bars), loading historical data...');
const oldestCandle = data[0];
if (oldestCandle) {
this.loadHistoricalData(oldestCandle.time);
}
}
}
async loadHistoricalData(beforeTime) {
if (this.isLoading) {
console.log('Already loading, skipping...');
return;
}
this.isLoading = true;
console.log(`Loading historical data for ${this.currentInterval} before ${beforeTime}`);
// Get the timestamp of the leftmost visible candle to preserve view
const currentData = this.candleSeries.data();
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
let leftmostTime = null;
// Save current price range if autoScale is disabled
let savedPriceRange = null;
const isAutoScale = this.priceScaleState?.autoScale !== false;
if (!isAutoScale) {
try {
savedPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
console.log('Saving price range:', savedPriceRange);
} catch (e) {
console.log('Could not save price range');
}
}
if (visibleRange && currentData.length > 0) {
const leftmostIndex = Math.floor(visibleRange.from);
if (leftmostIndex >= 0 && leftmostIndex < currentData.length) {
leftmostTime = currentData[leftmostIndex].time;
}
}
try {
// Fetch candles before the oldest visible candle
// Use end parameter to get candles up to (but not including) the oldest candle
const endTime = new Date((beforeTime - 1) * 1000); // 1 second before oldest
console.log(`Loading historical data before ${new Date((beforeTime - 1) * 1000).toISOString()}`);
const response = await fetch(
`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&end=${endTime.toISOString()}&limit=500`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
const existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
// Scroll back to the same timestamp to keep chart view static
if (leftmostTime) {
this.chart.timeScale().scrollToTime(leftmostTime);
}
// Restore price range if autoScale was disabled
if (!isAutoScale && savedPriceRange) {
setTimeout(() => {
try {
this.candleSeries.priceScale().setVisiblePriceRange(savedPriceRange);
console.log('Restored price range:', savedPriceRange);
} catch (e) {
console.log('Could not restore price range');
}
}, 50);
}
console.log(`Loaded ${chartData.length} historical candles`);
} else {
console.log('No more historical data available');
}
} catch (error) {
console.error('Error loading historical data:', error);
} finally {
this.isLoading = false;
}
}
async loadTA() {
try {
const response = await fetch(`/api/v1/ta?symbol=BTC&interval=${this.currentInterval}`);
this.taData = await response.json();
this.renderTA();
} catch (error) {
console.error('Error loading TA:', error);
document.getElementById('taContent').innerHTML = '<div class="ta-error">Failed to load technical analysis</div>';
}
}
renderTA() {
if (!this.taData || this.taData.error) {
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
return;
}
const data = this.taData;
const trendClass = data.trend.direction.toLowerCase();
const signalClass = data.trend.signal.toLowerCase();
const ma44Change = data.moving_averages.price_vs_ma44;
const ma125Change = data.moving_averages.price_vs_ma125;
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
document.getElementById('taContent').innerHTML = `
<div class="ta-section">
<div class="ta-section-title">Trend Analysis</div>
<div class="ta-trend ${trendClass}">
${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'}
</div>
<div class="ta-strength">${data.trend.strength}</div>
<span class="ta-signal ${signalClass}">${data.trend.signal}</span>
</div>
<div class="ta-section">
<div class="ta-section-title">Moving Averages</div>
<div class="ta-ma-row">
<span class="ta-ma-label">MA 44</span>
<span class="ta-ma-value">
${data.moving_averages.ma_44 ? data.moving_averages.ma_44.toFixed(2) : 'N/A'}
${ma44Change !== null ? `<span class="ta-ma-change ${ma44Change >= 0 ? 'positive' : 'negative'}">${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%</span>` : ''}
</span>
</div>
<div class="ta-ma-row">
<span class="ta-ma-label">MA 125</span>
<span class="ta-ma-value">
${data.moving_averages.ma_125 ? data.moving_averages.ma_125.toFixed(2) : 'N/A'}
${ma125Change !== null ? `<span class="ta-ma-change ${ma125Change >= 0 ? 'positive' : 'negative'}">${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%</span>` : ''}
</span>
</div>
</div>
<div class="ta-section">
<div class="ta-section-title">Key Levels</div>
<div class="ta-level">
<span class="ta-level-label">Resistance</span>
<span class="ta-level-value">$${data.levels.resistance.toLocaleString()}</span>
</div>
<div class="ta-level">
<span class="ta-level-label">Support</span>
<span class="ta-level-value">$${data.levels.support.toLocaleString()}</span>
</div>
<div class="ta-position-bar">
<div class="ta-position-marker" style="left: ${data.levels.position_in_range}%"></div>
</div>
<div style="font-size: 11px; color: var(--tv-text-secondary); margin-top: 4px; text-align: center;">
Position in range: ${data.levels.position_in_range.toFixed(1)}%
</div>
</div>
<div class="ta-section">
<div class="ta-section-title">AI Insight</div>
<div style="font-size: 13px; margin-top: 8px;">
${data.ai_placeholder ? data.ai_placeholder.message : 'AI analysis available'}
</div>
<button class="ta-btn ai-btn" onclick="openAIAnalysis()" style="width: 100%; margin-top: 12px; font-size: 11px;">
Analyze
</button>
</div>
`;
}
updateStats(candle) {
const price = candle.close;
const change = ((price - candle.open) / candle.open * 100);
document.getElementById('currentPrice').textContent = price.toFixed(2);
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('dailyHigh').textContent = candle.high.toFixed(2);
document.getElementById('dailyLow').textContent = candle.low.toFixed(2);
}
switchTimeframe(interval) {
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
this.currentInterval = interval;
this.hasInitialLoad = false;
document.querySelectorAll('.timeframe-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.interval === interval);
});
this.allData.delete(interval);
this.loadInitialData();
this.loadTA();
// Clear simulation results when changing timeframe
clearSimulationResults();
// Update simulation panel timeframe display
updateTimeframeDisplay();
}
}
function refreshTA() {
if (window.dashboard) {
window.dashboard.loadTA();
}
}
function openAIAnalysis() {
const symbol = 'BTC';
const interval = window.dashboard?.currentInterval || '1d';
const prompt = `Analyze Bitcoin (${symbol}) ${interval} chart. Current trend, support/resistance levels, and trading recommendation. Technical indicators: MA44, MA125.`;
const geminiUrl = `https://gemini.google.com/app?prompt=${encodeURIComponent(prompt)}`;
window.open(geminiUrl, '_blank');
}
// Setup marker tooltips on chart hover
function setupMarkerTooltips(trades) {
if (!window.dashboard || !window.dashboard.chart) return;
const chart = window.dashboard.chart;
const candleSeries = window.dashboard.candleSeries;
// Create tooltip element
let tooltip = document.getElementById('marker-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'marker-tooltip';
tooltip.style.cssText = `
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: #fff;
padding: 10px 14px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
display: none;
border: 1px solid #444;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
max-width: 280px;
line-height: 1.5;
`;
document.body.appendChild(tooltip);
}
// Track mouse movement
chart.subscribeCrosshairMove(param => {
if (!param.time || !param.point) {
tooltip.style.display = 'none';
return;
}
// Find nearby trade
const currentTime = param.time;
const nearbyTrade = trades.find(trade => {
const entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
const exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
// Check if within 5 bars
return Math.abs(currentTime - entryTime) <= 5 * 60 || Math.abs(currentTime - exitTime) <= 5 * 60;
});
if (nearbyTrade) {
const isEntry = Math.abs(currentTime - Math.floor(new Date(nearbyTrade.entryTime).getTime() / 1000)) <=
Math.abs(currentTime - Math.floor(new Date(nearbyTrade.exitTime).getTime() / 1000));
const pnlColor = nearbyTrade.pnl > 0 ? '#4caf50' : '#f44336';
const pnlSymbol = nearbyTrade.pnl > 0 ? '+' : '';
tooltip.innerHTML = `
<div style="font-weight: bold; margin-bottom: 6px; ${isEntry ? 'color: #26a69a;' : 'color: ' + pnlColor + ';'}">
${isEntry ? '🟢 BUY ENTRY' : '🔴 SELL EXIT'}
</div>
<div>Entry: $${nearbyTrade.entryPrice.toFixed(2)}</div>
<div>Exit: $${nearbyTrade.exitPrice.toFixed(2)}</div>
<div style="color: ${pnlColor}; font-weight: bold; margin-top: 4px;">
P&L: ${pnlSymbol}$${nearbyTrade.pnl.toFixed(2)} (${pnlSymbol}${nearbyTrade.pnlPct.toFixed(2)}%)
</div>
<div style="color: #888; font-size: 10px; margin-top: 4px;">
Duration: ${((new Date(nearbyTrade.exitTime) - new Date(nearbyTrade.entryTime)) / (1000 * 60)).toFixed(0)} min
</div>
`;
tooltip.style.left = (param.point.x + 15) + 'px';
tooltip.style.top = (param.point.y - 10) + 'px';
tooltip.style.display = 'block';
} else {
tooltip.style.display = 'none';
}
});
}
// Track trade line series for cleanup
window.tradeLineSeries = [];
// Show simulation buy/sell markers on the chart
function showSimulationMarkers() {
if (!window.lastSimulationResults || !window.dashboard) return;
const trades = window.lastSimulationResults.trades;
const markers = [];
// Clear existing trade lines
window.tradeLineSeries.forEach(series => {
window.dashboard.chart.removeSeries(series);
});
window.tradeLineSeries = [];
trades.forEach(trade => {
const entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
const exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
const pnlSymbol = trade.pnl > 0 ? '+' : '';
// Entry Marker - Buy signal (blue arrow and text)
markers.push({
time: entryTime,
position: 'belowBar',
color: '#2196f3',
shape: 'arrowUp',
text: 'BUY',
size: 1
});
// Exit Marker - Sell signal (green for profit, red for loss)
markers.push({
time: exitTime,
position: 'aboveBar',
color: trade.pnl > 0 ? '#4caf50' : '#f44336',
shape: 'arrowDown',
text: 'SELL ' + pnlSymbol + trade.pnlPct.toFixed(1) + '%',
size: 1
});
// Create separate line series for this trade (entry to exit)
const tradeLine = window.dashboard.chart.addLineSeries({
color: '#2196f3',
lineWidth: 1,
lastValueVisible: false,
title: '',
priceLineVisible: false,
crosshairMarkerVisible: false
});
tradeLine.setData([
{ time: entryTime, value: trade.entryPrice },
{ time: exitTime, value: trade.exitPrice }
]);
window.tradeLineSeries.push(tradeLine);
});
// Sort markers by time
markers.sort((a, b) => a.time - b.time);
// Apply markers to chart
window.dashboard.candleSeries.setMarkers(markers);
// Log to console instead of alert
console.log(`Plotted ${trades.length} trades on the chart.`);
// Show tooltip on marker hover via chart crosshair
setupMarkerTooltips(trades);
}
// Clear simulation markers
function clearSimulationMarkers() {
if (window.dashboard) {
window.dashboard.candleSeries.setMarkers([]);
// Remove trade line series
window.tradeLineSeries.forEach(series => {
window.dashboard.chart.removeSeries(series);
});
window.tradeLineSeries = [];
}
}
function clearSimulationResults() {
// Clear markers from chart
clearSimulationMarkers();
// Clear simulation data
window.lastSimulationResults = null;
// Hide results section
const resultsSection = document.getElementById('resultsSection');
if (resultsSection) {
resultsSection.style.display = 'none';
}
// Reset results display
const simTrades = document.getElementById('simTrades');
const simWinRate = document.getElementById('simWinRate');
const simPnL = document.getElementById('simPnL');
const simProfitFactor = document.getElementById('simProfitFactor');
const equitySparkline = document.getElementById('equitySparkline');
if (simTrades) simTrades.textContent = '0';
if (simWinRate) simWinRate.textContent = '0%';
if (simPnL) {
simPnL.textContent = '$0.00';
simPnL.style.color = '';
}
if (simProfitFactor) simProfitFactor.textContent = '0';
if (equitySparkline) equitySparkline.innerHTML = '';
}
// Set default start date (7 days ago)
function setDefaultStartDate() {
const startDateInput = document.getElementById('simStartDate');
if (startDateInput) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
startDateInput.value = sevenDaysAgo.toISOString().slice(0, 16);
}
}
// ==========================================
// SIMULATION STORAGE SERVICE (localStorage)
// ==========================================
const SimulationStorage = {
STORAGE_KEY: 'btc_bot_simulations',
getAll() {
try {
const data = localStorage.getItem(this.STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Error reading simulations:', e);
return [];
}
},
save(simulation) {
try {
const simulations = this.getAll();
simulation.id = simulation.id || 'sim_' + Date.now();
simulation.createdAt = new Date().toISOString();
simulations.push(simulation);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations));
return simulation.id;
} catch (e) {
console.error('Error saving simulation:', e);
return null;
}
},
delete(id) {
try {
let simulations = this.getAll();
simulations = simulations.filter(s => s.id !== id);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations));
return true;
} catch (e) {
console.error('Error deleting simulation:', e);
return false;
}
},
get(id) {
return this.getAll().find(s => s.id === id);
},
clear() {
localStorage.removeItem(this.STORAGE_KEY);
}
};
// ==========================================
// SIDEBAR FUNCTIONS
// ==========================================
function toggleSidebar() {
const sidebar = document.getElementById('rightSidebar');
sidebar.classList.toggle('collapsed');
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
}
function restoreSidebarState() {
const collapsed = localStorage.getItem('sidebar_collapsed') === 'true';
if (collapsed) {
document.getElementById('rightSidebar').classList.add('collapsed');
}
}
// ==========================================
// STRATEGY MANAGEMENT
// ==========================================
let currentStrategy = null;
let strategyParameters = {};
// Strategy parameter definitions
const StrategyParams = {
ma_trend: [
{ name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 }
]
};
function renderStrategies(strategies) {
const container = document.getElementById('strategyList');
if (!strategies || strategies.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px;">No strategies available</div>';
return;
}
container.innerHTML = strategies.map((s, index) => `
<div class="strategy-item ${index === 0 ? 'selected' : ''}" data-strategy-id="${s.id}" onclick="selectStrategy('${s.id}')">
<input type="radio" name="strategy" class="strategy-radio" ${index === 0 ? 'checked' : ''}>
<span class="strategy-name">${s.name}</span>
<span class="strategy-info" title="${s.description}">ⓘ</span>
</div>
`).join('');
// Select first strategy by default
if (strategies.length > 0) {
selectStrategy(strategies[0].id);
}
document.getElementById('runSimBtn').disabled = false;
}
function selectStrategy(strategyId) {
// Update UI
document.querySelectorAll('.strategy-item').forEach(item => {
item.classList.toggle('selected', item.dataset.strategyId === strategyId);
const radio = item.querySelector('input[type="radio"]');
if (radio) radio.checked = item.dataset.strategyId === strategyId;
});
currentStrategy = strategyId;
// Render parameters
renderStrategyParams(strategyId);
}
function renderStrategyParams(strategyId) {
const container = document.getElementById('strategyParams');
const params = StrategyParams[strategyId] || [];
if (params.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = params.map(param => `
<div class="config-group">
<label class="config-label">${param.label}</label>
<input type="${param.type}"
id="param_${param.name}"
class="config-input"
value="${param.default}"
${param.min !== undefined ? `min="${param.min}"` : ''}
${param.max !== undefined ? `max="${param.max}"` : ''}
${param.step !== undefined ? `step="${param.step}"` : ''}
>
</div>
`).join('');
}
function getStrategyConfig() {
const strategyId = currentStrategy;
if (!strategyId) return null;
const params = {};
const paramDefs = StrategyParams[strategyId] || [];
paramDefs.forEach(def => {
const input = document.getElementById(`param_${def.name}`);
if (input) {
params[def.name] = def.type === 'number' ? parseFloat(input.value) : input.value;
}
});
return {
id: strategyId,
params: params
};
}
// ==========================================
// ENHANCED SIMULATION FUNCTIONS
// ==========================================
async function runSimulation() {
const strategyConfig = getStrategyConfig();
if (!strategyConfig) {
alert('Please select a strategy');
return;
}
const startDateInput = document.getElementById('simStartDate').value;
if (!startDateInput) {
alert('Please select a start date');
return;
}
const runBtn = document.getElementById('runSimBtn');
runBtn.disabled = true;
runBtn.textContent = '⏳ Running...';
try {
const start = new Date(startDateInput);
const fetchStart = new Date(start.getTime() - 200 * 24 * 60 * 60 * 1000);
// Use chart's current timeframe
if (!window.dashboard) {
throw new Error('Dashboard not initialized');
}
const interval = window.dashboard.currentInterval;
const secondaryTF = document.getElementById('simSecondaryTF').value;
const riskPercent = parseFloat(document.getElementById('simRiskPercent').value);
const stopLossPercent = parseFloat(document.getElementById('simStopLoss').value);
// Fetch data
const timeframes = [interval];
if (secondaryTF && secondaryTF !== '') {
timeframes.push(secondaryTF);
}
const query = new URLSearchParams({ symbol: 'BTC', start: fetchStart.toISOString() });
timeframes.forEach(tf => query.append('timeframes', tf));
console.log('Fetching candles with query:', query.toString());
const response = await fetch(`/api/v1/candles/bulk?${query.toString()}`);
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('Candle data received:', data);
console.log('Looking for interval:', interval);
console.log('Available timeframes:', Object.keys(data));
// The API returns data directly by timeframe, not wrapped in 'candles' property
if (!data[interval] || data[interval].length === 0) {
throw new Error(`No candle data available for ${interval} timeframe. Check if data exists in database.`);
}
const candlesMap = {
[interval]: data[interval].map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}))
};
// Add secondary timeframe data if requested and available
if (secondaryTF && data[secondaryTF]) {
candlesMap[secondaryTF] = data[secondaryTF].map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
}
// Build strategy config
const engineConfig = {
id: strategyConfig.id,
params: strategyConfig.params,
timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] },
indicators: []
};
console.log('Building strategy config:');
console.log(' Primary TF:', interval);
console.log(' Secondary TF:', secondaryTF);
console.log(' Available candles:', Object.keys(candlesMap));
// Add indicator based on strategy
if (strategyConfig.id === 'ma_trend') {
const period = strategyConfig.params?.period || 44;
// Primary timeframe indicator
engineConfig.indicators.push({
name: `ma${period}`,
type: 'sma',
params: { period: period },
timeframe: interval
});
// Confirmation timeframe indicator (for trend filter)
if (secondaryTF) {
engineConfig.indicators.push({
name: `ma${period}_${secondaryTF}`,
type: 'sma',
params: { period: period },
timeframe: secondaryTF
});
}
}
console.log(' Indicators configured:', engineConfig.indicators.map(i => `${i.name} on ${i.timeframe}`));
// Run engine
const riskConfig = {
positionSizing: { method: 'percent', value: riskPercent },
stopLoss: { enabled: true, method: 'percent', value: stopLossPercent }
};
const engine = new ClientStrategyEngine();
const results = engine.run(candlesMap, engineConfig, riskConfig, start);
if (results.error) throw new Error(results.error);
// Add metadata
window.lastSimulationResults = {
...results,
config: {
strategyId: strategyConfig.id,
strategyName: window.availableStrategies.find(s => s.id === strategyConfig.id)?.name || strategyConfig.id,
timeframe: interval,
secondaryTimeframe: secondaryTF,
startDate: startDateInput,
riskPercent: riskPercent,
stopLossPercent: stopLossPercent,
params: strategyConfig.params
},
runAt: new Date().toISOString()
};
// Display results
displayEnhancedResults(window.lastSimulationResults);
// Show results section
document.getElementById('resultsSection').style.display = 'block';
// Update chart with full historical data from simulation
if (window.dashboard && candlesMap[interval]) {
const chartData = candlesMap[interval].map(c => ({
time: c.time,
open: c.open,
high: c.high,
low: c.low,
close: c.close
}));
window.dashboard.candleSeries.setData(chartData);
window.dashboard.allData.set(interval, chartData);
console.log(`Chart updated with ${chartData.length} candles from simulation range`);
}
// Show simulation markers on chart
showSimulationMarkers();
} catch (error) {
console.error('Simulation error:', error);
alert('Simulation error: ' + error.message);
} finally {
runBtn.disabled = false;
runBtn.textContent = '▶ Run Simulation';
}
}
function displayEnhancedResults(simulation) {
const results = simulation.results || simulation;
document.getElementById('simTrades').textContent = results.total_trades || '0';
document.getElementById('simWinRate').textContent = (results.win_rate || 0).toFixed(1) + '%';
const pnl = results.total_pnl || 0;
const pnlElement = document.getElementById('simPnL');
pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
pnlElement.style.color = pnl >= 0 ? '#4caf50' : '#f44336';
// Calculate profit factor
let grossProfit = 0;
let grossLoss = 0;
(results.trades || []).forEach(trade => {
if (trade.pnl > 0) grossProfit += trade.pnl;
else grossLoss += Math.abs(trade.pnl);
});
const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss).toFixed(2) : grossProfit > 0 ? '∞' : '0';
document.getElementById('simProfitFactor').textContent = profitFactor;
// Draw equity sparkline
drawEquitySparkline(results);
}
function drawEquitySparkline(results) {
const container = document.getElementById('equitySparkline');
if (!container || !results.trades || results.trades.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 11px;">No trades</div>';
return;
}
// Build equity curve
let equity = 1000;
const equityData = [{ time: results.trades[0].entryTime, equity: equity }];
results.trades.forEach(trade => {
equity += trade.pnl;
equityData.push({ time: trade.exitTime, equity: equity });
});
// Store for later use
window.lastSimulationResults.equity_curve = equityData;
// Draw simple sparkline using canvas
container.innerHTML = '<canvas id="sparklineCanvas" width="300" height="60"></canvas>';
const canvas = document.getElementById('sparklineCanvas');
const ctx = canvas.getContext('2d');
const minEquity = Math.min(...equityData.map(d => d.equity));
const maxEquity = Math.max(...equityData.map(d => d.equity));
const range = maxEquity - minEquity || 1;
ctx.strokeStyle = equityData[equityData.length - 1].equity >= equityData[0].equity ? '#4caf50' : '#f44336';
ctx.lineWidth = 2;
ctx.beginPath();
equityData.forEach((point, i) => {
const x = (i / (equityData.length - 1)) * canvas.width;
const y = canvas.height - ((point.equity - minEquity) / range) * canvas.height;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// Add start/end labels
ctx.fillStyle = '#888';
ctx.font = '9px sans-serif';
ctx.fillText('$' + equityData[0].equity.toFixed(0), 2, canvas.height - 2);
ctx.fillText('$' + equityData[equityData.length - 1].equity.toFixed(0), canvas.width - 30, 10);
}
// ==========================================
// SAVE SIMULATION
// ==========================================
function saveSimulation() {
if (!window.lastSimulationResults) {
alert('Please run a simulation first');
return;
}
const defaultName = generateSimulationName(window.lastSimulationResults.config);
const name = prompt('Save simulation as:', defaultName);
if (!name || name.trim() === '') return;
const simulation = {
name: name.trim(),
config: window.lastSimulationResults.config,
results: {
total_trades: window.lastSimulationResults.total_trades,
win_rate: window.lastSimulationResults.win_rate,
total_pnl: window.lastSimulationResults.total_pnl,
trades: window.lastSimulationResults.trades,
equity_curve: window.lastSimulationResults.equity_curve
}
};
const id = SimulationStorage.save(simulation);
if (id) {
renderSavedSimulations();
alert('Simulation saved successfully!');
} else {
alert('Error saving simulation');
}
}
function generateSimulationName(config) {
if (!config) return 'Unnamed Simulation';
const start = new Date(config.startDate);
const now = new Date();
const duration = now - start;
const oneDay = 24 * 60 * 60 * 1000;
let dateStr;
if (duration < oneDay) {
dateStr = start.toISOString().slice(0, 16).replace('T', ' ');
} else {
dateStr = start.toISOString().slice(0, 10);
}
return `${config.strategyName}_${config.timeframe}_${dateStr}`;
}
function renderSavedSimulations() {
const container = document.getElementById('savedSimulations');
const simulations = SimulationStorage.getAll();
if (simulations.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">No saved simulations</div>';
return;
}
container.innerHTML = simulations.map(sim => `
<div class="saved-sim-item">
<span class="saved-sim-name" onclick="loadSavedSimulation('${sim.id}')" title="${sim.name}">
${sim.name.length > 25 ? sim.name.slice(0, 25) + '...' : sim.name}
</span>
<div class="saved-sim-actions">
<button class="sim-action-btn" onclick="loadSavedSimulation('${sim.id}')" title="Load">📂</button>
<button class="sim-action-btn" onclick="exportSavedSimulation('${sim.id}')" title="Export">📥</button>
<button class="sim-action-btn" onclick="deleteSavedSimulation('${sim.id}')" title="Delete">🗑️</button>
</div>
</div>
`).join('');
}
function loadSavedSimulation(id) {
const sim = SimulationStorage.get(id);
if (!sim) {
alert('Simulation not found');
return;
}
// Restore config
if (sim.config) {
document.getElementById('simTimeframe').value = sim.config.timeframe || '37m';
document.getElementById('simSecondaryTF').value = sim.config.secondaryTimeframe || '';
document.getElementById('simStartDate').value = sim.config.startDate || '';
document.getElementById('simRiskPercent').value = sim.config.riskPercent || 2;
document.getElementById('simStopLoss').value = sim.config.stopLossPercent || 2;
// Select strategy
if (sim.config.strategyId) {
selectStrategy(sim.config.strategyId);
// Restore params
if (sim.config.params) {
Object.entries(sim.config.params).forEach(([key, value]) => {
const input = document.getElementById(`param_${key}`);
if (input) input.value = value;
});
}
}
}
// Restore results
window.lastSimulationResults = sim;
displayEnhancedResults(sim.results);
document.getElementById('resultsSection').style.display = 'block';
}
function deleteSavedSimulation(id) {
if (!confirm('Are you sure you want to delete this simulation?')) return;
if (SimulationStorage.delete(id)) {
renderSavedSimulations();
}
}
// ==========================================
// EXPORT FUNCTIONS
// ==========================================
function showExportDialog() {
if (!window.lastSimulationResults) {
alert('Please run a simulation first');
return;
}
// Create overlay
const overlay = document.createElement('div');
overlay.className = 'dialog-overlay';
overlay.onclick = () => closeExportDialog();
document.body.appendChild(overlay);
// Create dialog
const dialog = document.createElement('div');
dialog.className = 'export-dialog';
dialog.id = 'exportDialog';
dialog.innerHTML = `
<div class="export-dialog-title">📥 Export Simulation Report</div>
<div class="export-options">
<label class="export-option">
<input type="radio" name="exportFormat" value="csv" checked>
<span>CSV (Trades list)</span>
</label>
<label class="export-option">
<input type="radio" name="exportFormat" value="json">
<span>JSON (Full data)</span>
</label>
<label class="export-option">
<input type="radio" name="exportFormat" value="both">
<span>Both CSV + JSON</span>
</label>
</div>
<div style="display: flex; gap: 8px;">
<button class="action-btn secondary" onclick="closeExportDialog()" style="flex: 1;">Cancel</button>
<button class="action-btn primary" onclick="performExport()" style="flex: 1;">Export</button>
</div>
`;
document.body.appendChild(dialog);
}
function closeExportDialog() {
const overlay = document.querySelector('.dialog-overlay');
const dialog = document.getElementById('exportDialog');
if (overlay) overlay.remove();
if (dialog) dialog.remove();
}
function performExport() {
const format = document.querySelector('input[name="exportFormat"]:checked').value;
const sim = window.lastSimulationResults;
const config = sim.config || {};
const dateStr = new Date().toISOString().slice(0, 10);
const baseFilename = generateSimulationName(config).replace(/[^a-zA-Z0-9_-]/g, '_');
if (format === 'csv' || format === 'both') {
exportToCSV(sim, `${baseFilename}.csv`);
}
if (format === 'json' || format === 'both') {
exportToJSON(sim, `${baseFilename}.json`);
}
closeExportDialog();
}
function exportToCSV(simulation, filename) {
const results = simulation.results || simulation;
const config = simulation.config || {};
let csv = 'Trade #,Entry Time,Exit Time,Entry Price,Exit Price,Size,P&L ($),P&L (%),Type\n';
(results.trades || []).forEach((trade, i) => {
csv += `${i + 1},${trade.entryTime},${trade.exitTime},${trade.entryPrice},${trade.exitPrice},${trade.size},${trade.pnl},${trade.pnlPct},${trade.type}\n`;
});
csv += '\n';
csv += 'Summary\n';
csv += `Strategy,${config.strategyName || 'Unknown'}\n`;
csv += `Timeframe,${config.timeframe || 'Unknown'}\n`;
csv += `Start Date,${config.startDate || 'Unknown'}\n`;
csv += `Total Trades,${results.total_trades || 0}\n`;
csv += `Win Rate (%),${(results.win_rate || 0).toFixed(2)}\n`;
csv += `Total P&L ($),${(results.total_pnl || 0).toFixed(2)}\n`;
csv += `Risk % per Trade,${config.riskPercent || 2}\n`;
csv += `Stop Loss %,${config.stopLossPercent || 2}\n`;
downloadFile(csv, filename, 'text/csv');
}
function exportToJSON(simulation, filename) {
const exportData = {
metadata: {
exported_at: new Date().toISOString(),
version: '1.0'
},
configuration: simulation.config || {},
results: {
summary: {
total_trades: simulation.total_trades || simulation.results?.total_trades || 0,
win_rate: simulation.win_rate || simulation.results?.win_rate || 0,
total_pnl: simulation.total_pnl || simulation.results?.total_pnl || 0
},
trades: simulation.trades || simulation.results?.trades || [],
equity_curve: simulation.equity_curve || []
}
};
downloadFile(JSON.stringify(exportData, null, 2), filename, 'application/json');
}
function exportSavedSimulation(id) {
const sim = SimulationStorage.get(id);
if (!sim) {
alert('Simulation not found');
return;
}
window.lastSimulationResults = sim;
showExportDialog();
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// ==========================================
// ENHANCED MARKER FUNCTIONS
// ==========================================
let tradeLineSeries = [];
function showSimulationMarkers() {
if (!window.lastSimulationResults || !window.dashboard) return;
const trades = window.lastSimulationResults.trades || window.lastSimulationResults.results?.trades || [];
const markers = [];
// Clear existing trade lines
clearSimulationMarkers();
console.log('Plotting trades:', trades.length);
console.log('First trade entryTime:', trades[0]?.entryTime, 'type:', typeof trades[0]?.entryTime);
trades.forEach((trade, i) => {
// Trade times are already in seconds (Unix timestamp)
// If they're strings (ISO dates), convert to seconds
let entryTime, exitTime;
if (typeof trade.entryTime === 'number') {
entryTime = trade.entryTime;
} else {
entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
}
if (typeof trade.exitTime === 'number') {
exitTime = trade.exitTime;
} else {
exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
}
if (i === 0) {
console.log('Converted entryTime:', entryTime, 'Date:', new Date(entryTime * 1000));
}
const pnlSymbol = trade.pnl > 0 ? '+' : '';
// Entry Marker - Buy signal (blue)
markers.push({
time: entryTime,
position: 'belowBar',
color: '#2196f3',
shape: 'arrowUp',
text: 'BUY',
size: 1
});
// Exit Marker - Sell signal (green/red based on P&L)
markers.push({
time: exitTime,
position: 'aboveBar',
color: trade.pnl > 0 ? '#4caf50' : '#f44336',
shape: 'arrowDown',
text: `SELL ${pnlSymbol}${trade.pnlPct.toFixed(1)}%`,
size: 1
});
// Create line connecting entry to exit
const lineSeries = window.dashboard.chart.addLineSeries({
color: '#2196f3',
lineWidth: 1,
lastValueVisible: false,
title: '',
priceLineVisible: false,
crosshairMarkerVisible: false
});
lineSeries.setData([
{ time: entryTime, value: trade.entryPrice },
{ time: exitTime, value: trade.exitPrice }
]);
tradeLineSeries.push(lineSeries);
});
// Sort markers by time
markers.sort((a, b) => a.time - b.time);
// Apply markers
window.dashboard.candleSeries.setMarkers(markers);
console.log(`Plotted ${trades.length} trades with connection lines`);
}
function clearSimulationMarkers() {
if (window.dashboard) {
window.dashboard.candleSeries.setMarkers([]);
tradeLineSeries.forEach(series => {
try {
window.dashboard.chart.removeSeries(series);
} catch (e) {
// Series might already be removed
}
});
tradeLineSeries = [];
}
}
// ==========================================
// TIMEFRAME DISPLAY UPDATE
// ==========================================
function updateTimeframeDisplay() {
const display = document.getElementById('simTimeframeDisplay');
if (display && window.dashboard) {
display.value = window.dashboard.currentInterval.toUpperCase();
}
}
// ==========================================
// STRATEGY LOADING
// ==========================================
async function loadStrategies() {
try {
console.log('Fetching strategies from API...');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch('/api/v1/strategies?_=' + Date.now(), {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Strategies loaded:', data);
if (!data.strategies) {
throw new Error('Invalid response format: missing strategies array');
}
window.availableStrategies = data.strategies;
renderStrategies(data.strategies);
} catch (error) {
console.error('Error loading strategies:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = 'Request timeout - API server not responding';
} else if (error.message.includes('Failed to fetch')) {
errorMessage = 'Cannot connect to API server - is it running?';
}
document.getElementById('strategyList').innerHTML =
`<div style="color: var(--tv-red); padding: 20px; text-align: center;">
${errorMessage}<br>
<small style="color: var(--tv-text-secondary);">Check console (F12) for details</small>
</div>`;
}
}
// ==========================================
// INITIALIZATION
// ==========================================
document.addEventListener('DOMContentLoaded', () => {
window.dashboard = new TradingDashboard();
restoreSidebarState();
setDefaultStartDate();
updateTimeframeDisplay();
renderSavedSimulations();
// Load strategies
loadStrategies();
});
</script>
</body>
</html>