419 lines
14 KiB
HTML
419 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Enhanced RL COB Trading Dashboard</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #0c0c0c 0%, #1a1a1a 100%);
|
|
color: #e0e0e0;
|
|
margin: 0;
|
|
padding: 20px;
|
|
}
|
|
.header {
|
|
background: rgba(20, 20, 20, 0.9);
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #333;
|
|
}
|
|
.title {
|
|
font-size: 1.8rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(45deg, #00d4ff, #00ff88);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
.status-indicators {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-top: 10px;
|
|
}
|
|
.status-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #ff4444;
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
.status-dot.active {
|
|
background: #00ff88;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
.main-container {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
.chart-section, .sidebar {
|
|
background: rgba(20, 20, 20, 0.8);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
border: 1px solid #333;
|
|
}
|
|
.chart-container {
|
|
width: 100%;
|
|
height: 400px;
|
|
position: relative;
|
|
}
|
|
.predictions-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
.prediction-item {
|
|
background: rgba(40, 40, 40, 0.6);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-bottom: 8px;
|
|
border-left: 4px solid #666;
|
|
}
|
|
.prediction-item.up { border-left-color: #00ff88; }
|
|
.prediction-item.down { border-left-color: #ff4444; }
|
|
.prediction-item.sideways { border-left-color: #ffaa00; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="title">🔥 Enhanced RL COB Trading Dashboard</div>
|
|
<div class="status-indicators">
|
|
<div>
|
|
<span class="status-dot" id="rl-status"></span>
|
|
<span>RL Model</span>
|
|
</div>
|
|
<div>
|
|
<span class="status-dot" id="cob-status"></span>
|
|
<span>COB Data</span>
|
|
</div>
|
|
<div>
|
|
<span class="status-dot" id="ws-status"></span>
|
|
<span>WebSocket</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-container">
|
|
<div class="chart-section">
|
|
<h3>📈 Price Chart with RL Predictions</h3>
|
|
<div class="chart-container">
|
|
<canvas id="priceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar">
|
|
<h3>🤖 RL Predictions</h3>
|
|
<div class="predictions-list" id="predictions-list">
|
|
<div>Loading predictions...</div>
|
|
</div>
|
|
|
|
<h3 style="margin-top: 30px;">📊 COB Data</h3>
|
|
<div id="cob-data">
|
|
<div>Loading COB data...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let priceChart = null;
|
|
let websocket = null;
|
|
let currentSymbol = 'BTC/USDT';
|
|
|
|
// Initialize dashboard
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeChart();
|
|
initializeWebSocket();
|
|
loadInitialData();
|
|
|
|
// Update data every 2 seconds
|
|
setInterval(loadData, 2000);
|
|
});
|
|
|
|
function initializeChart() {
|
|
const ctx = document.getElementById('priceChart').getContext('2d');
|
|
|
|
priceChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
datasets: [
|
|
{
|
|
label: 'Price',
|
|
data: [],
|
|
borderColor: '#00d4ff',
|
|
backgroundColor: 'rgba(0, 212, 255, 0.1)',
|
|
borderWidth: 2,
|
|
fill: false,
|
|
pointRadius: 0
|
|
},
|
|
{
|
|
label: 'RL Predictions (UP)',
|
|
data: [],
|
|
borderColor: '#00ff88',
|
|
backgroundColor: '#00ff88',
|
|
pointRadius: 6,
|
|
pointStyle: 'triangle',
|
|
showLine: false
|
|
},
|
|
{
|
|
label: 'RL Predictions (DOWN)',
|
|
data: [],
|
|
borderColor: '#ff4444',
|
|
backgroundColor: '#ff4444',
|
|
pointRadius: 6,
|
|
pointStyle: 'triangle',
|
|
rotation: 180,
|
|
showLine: false
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
time: {
|
|
unit: 'minute'
|
|
},
|
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
|
ticks: { color: '#aaa' }
|
|
},
|
|
y: {
|
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
|
ticks: { color: '#aaa' }
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
labels: { color: '#e0e0e0' }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeWebSocket() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
|
|
websocket = new WebSocket(wsUrl);
|
|
|
|
websocket.onopen = function() {
|
|
updateStatusDot('ws-status', true);
|
|
websocket.send(JSON.stringify({
|
|
type: 'subscribe',
|
|
symbol: currentSymbol
|
|
}));
|
|
};
|
|
|
|
websocket.onmessage = function(event) {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'cob_update') {
|
|
handleCOBUpdate(data);
|
|
}
|
|
};
|
|
|
|
websocket.onerror = function() {
|
|
updateStatusDot('ws-status', false);
|
|
};
|
|
|
|
websocket.onclose = function() {
|
|
updateStatusDot('ws-status', false);
|
|
setTimeout(initializeWebSocket, 5000);
|
|
};
|
|
}
|
|
|
|
async function loadInitialData() {
|
|
await loadRLStatus();
|
|
await loadData();
|
|
}
|
|
|
|
async function loadData() {
|
|
await loadCOBData();
|
|
await loadRLPredictions();
|
|
}
|
|
|
|
async function loadRLStatus() {
|
|
try {
|
|
const response = await fetch('/api/rl-status');
|
|
const data = await response.json();
|
|
updateStatusDot('rl-status', data.status === 'active');
|
|
} catch (error) {
|
|
updateStatusDot('rl-status', false);
|
|
}
|
|
}
|
|
|
|
async function loadCOBData() {
|
|
try {
|
|
const response = await fetch(`/api/cob-data/${encodeURIComponent(currentSymbol)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.data) {
|
|
updateCOBDisplay(data.data);
|
|
updateStatusDot('cob-status', true);
|
|
|
|
// Add price point to chart
|
|
if (data.data.stats && data.data.stats.mid_price) {
|
|
addPricePoint(data.data.stats.mid_price);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
updateStatusDot('cob-status', false);
|
|
}
|
|
}
|
|
|
|
async function loadRLPredictions() {
|
|
try {
|
|
const response = await fetch(`/api/rl-predictions/${encodeURIComponent(currentSymbol)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.predictions) {
|
|
updatePredictionsDisplay(data.predictions);
|
|
updatePredictionsChart(data.predictions);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading RL predictions:', error);
|
|
}
|
|
}
|
|
|
|
function handleCOBUpdate(data) {
|
|
if (data.symbol === currentSymbol) {
|
|
updateCOBDisplay(data.data);
|
|
|
|
if (data.data.stats && data.data.stats.mid_price) {
|
|
addPricePoint(data.data.stats.mid_price);
|
|
}
|
|
|
|
if (data.data.rl_data && data.data.rl_data.rl_predictions) {
|
|
updatePredictionsFromWS(data.data.rl_data.rl_predictions);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addPricePoint(price) {
|
|
const now = new Date();
|
|
priceChart.data.datasets[0].data.push({
|
|
x: now,
|
|
y: price
|
|
});
|
|
|
|
// Keep last 300 points (5 minutes)
|
|
if (priceChart.data.datasets[0].data.length > 300) {
|
|
priceChart.data.datasets[0].data.shift();
|
|
}
|
|
|
|
priceChart.update('none');
|
|
}
|
|
|
|
function updateCOBDisplay(cobData) {
|
|
const container = document.getElementById('cob-data');
|
|
|
|
if (!cobData.bids || !cobData.asks) {
|
|
container.innerHTML = '<div>No COB data available</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div style="font-size: 0.9rem;">';
|
|
|
|
// Show top 3 asks
|
|
cobData.asks.slice(0, 3).reverse().forEach(ask => {
|
|
html += `<div style="color: #ff4444; margin: 2px 0;">Ask: $${ask.price.toFixed(2)} (${ask.size.toFixed(4)})</div>`;
|
|
});
|
|
|
|
// Show spread
|
|
if (cobData.stats && cobData.stats.mid_price) {
|
|
html += `<div style="background: rgba(255,255,255,0.1); padding: 4px; margin: 4px 0; text-align: center;">Mid: $${cobData.stats.mid_price.toFixed(2)}</div>`;
|
|
}
|
|
|
|
// Show top 3 bids
|
|
cobData.bids.slice(0, 3).forEach(bid => {
|
|
html += `<div style="color: #00ff88; margin: 2px 0;">Bid: $${bid.price.toFixed(2)} (${bid.size.toFixed(4)})</div>`;
|
|
});
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function updatePredictionsDisplay(predictions) {
|
|
const container = document.getElementById('predictions-list');
|
|
|
|
if (!predictions.recent_predictions || predictions.recent_predictions.length === 0) {
|
|
container.innerHTML = '<div>No recent predictions</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
const recent = predictions.recent_predictions.slice(-10).reverse();
|
|
|
|
recent.forEach(pred => {
|
|
const directionClass = pred.direction === 2 ? 'up' : (pred.direction === 0 ? 'down' : 'sideways');
|
|
const directionText = ['DOWN', 'SIDEWAYS', 'UP'][pred.direction];
|
|
const confidence = (pred.confidence * 100).toFixed(1);
|
|
const time = new Date(pred.timestamp).toLocaleTimeString();
|
|
|
|
html += `
|
|
<div class="prediction-item ${directionClass}">
|
|
<div style="font-weight: bold;">${directionText}</div>
|
|
<div style="font-size: 0.8rem; color: #aaa;">${time} - ${confidence}% confidence</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function updatePredictionsChart(predictions) {
|
|
if (!predictions.prediction_history) return;
|
|
|
|
const upPredictions = [];
|
|
const downPredictions = [];
|
|
|
|
predictions.prediction_history.slice(-50).forEach(pred => {
|
|
const point = {
|
|
x: new Date(pred.timestamp),
|
|
y: pred.price || 0
|
|
};
|
|
|
|
if (pred.direction === 2) upPredictions.push(point);
|
|
else if (pred.direction === 0) downPredictions.push(point);
|
|
});
|
|
|
|
priceChart.data.datasets[1].data = upPredictions;
|
|
priceChart.data.datasets[2].data = downPredictions;
|
|
priceChart.update('none');
|
|
}
|
|
|
|
function updatePredictionsFromWS(predictions) {
|
|
predictions.forEach(pred => {
|
|
const point = {
|
|
x: new Date(pred.timestamp),
|
|
y: pred.price || priceChart.data.datasets[0].data[priceChart.data.datasets[0].data.length - 1]?.y || 0
|
|
};
|
|
|
|
if (pred.direction === 2) {
|
|
priceChart.data.datasets[1].data.push(point);
|
|
} else if (pred.direction === 0) {
|
|
priceChart.data.datasets[2].data.push(point);
|
|
}
|
|
});
|
|
|
|
priceChart.update('none');
|
|
}
|
|
|
|
function updateStatusDot(elementId, isActive) {
|
|
const dot = document.getElementById(elementId);
|
|
if (dot) {
|
|
dot.classList.toggle('active', isActive);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|