3183 lines
124 KiB
HTML
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>
|