Update
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Main Application Entry Point
|
||||
* Initializes all components and manages the application lifecycle
|
||||
*/
|
||||
|
||||
import { MarketsComponent } from './components/MarketsComponent.js';
|
||||
import { StrategyComponent } from './components/StrategyComponent.js';
|
||||
import { PositionsComponent } from './components/PositionsComponent.js';
|
||||
import { BacktestComponent } from './components/BacktestComponent.js';
|
||||
import { Notification } from './utils/Notification.js';
|
||||
import { WebSocketManager } from './utils/WebSocketManager.js';
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.components = {};
|
||||
this.wsManager = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('[App] Initializing application...');
|
||||
|
||||
// Initialize WebSocket
|
||||
if (window.io) {
|
||||
this.wsManager = new WebSocketManager(io());
|
||||
this.setupWebSocketHandlers();
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
this.components.markets = new MarketsComponent('marketsList');
|
||||
this.components.strategy = new StrategyComponent();
|
||||
this.components.positions = new PositionsComponent('positionsList');
|
||||
this.components.backtest = new BacktestComponent();
|
||||
|
||||
// Initialize controls
|
||||
this.initializeControls();
|
||||
|
||||
// Load initial data
|
||||
try {
|
||||
await this.components.markets.load();
|
||||
this.components.markets.startAutoRefresh(30000);
|
||||
} catch (error) {
|
||||
console.error('[App] Error loading markets:', error);
|
||||
if (this.components.markets.container) {
|
||||
this.components.markets.showError('Failed to load markets. Check console for details.');
|
||||
}
|
||||
}
|
||||
|
||||
this.components.strategy.initialize();
|
||||
this.components.positions.load();
|
||||
this.components.backtest.initialize();
|
||||
|
||||
// Start position updates
|
||||
setInterval(() => this.components.positions.load(), 5000);
|
||||
|
||||
console.log('[App] Application initialized');
|
||||
}
|
||||
|
||||
initializeControls() {
|
||||
// Threshold and confidence sliders
|
||||
const threshold = document.getElementById('threshold');
|
||||
const confidence = document.getElementById('confidence');
|
||||
const thresholdValue = document.getElementById('thresholdValue');
|
||||
const confidenceValue = document.getElementById('confidenceValue');
|
||||
|
||||
if (threshold && thresholdValue) {
|
||||
threshold.addEventListener('input', (e) => {
|
||||
thresholdValue.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
}
|
||||
|
||||
if (confidence && confidenceValue) {
|
||||
confidence.addEventListener('input', (e) => {
|
||||
confidenceValue.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setupWebSocketHandlers() {
|
||||
if (!this.wsManager) return;
|
||||
|
||||
// Backtest handlers
|
||||
this.wsManager.on('backtest_log', (data) => {
|
||||
this.components.backtest.addTerminalLine(data.message, data.type || 'info');
|
||||
});
|
||||
|
||||
this.wsManager.on('backtest_trade', (data) => {
|
||||
this.components.backtest.addTrade(data);
|
||||
this.components.backtest.updateStats(data);
|
||||
});
|
||||
|
||||
this.wsManager.on('backtest_equity', (data) => {
|
||||
this.components.backtest.updateChart(data);
|
||||
this.components.backtest.updateStats(data);
|
||||
});
|
||||
|
||||
this.wsManager.on('backtest_complete', (data) => {
|
||||
this.components.backtest.displayResults(data);
|
||||
const btn = document.getElementById('runBacktestBtn');
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
}
|
||||
this.components.backtest.isRunning = false;
|
||||
});
|
||||
|
||||
this.wsManager.on('backtest_error', (data) => {
|
||||
this.components.backtest.addTerminalLine('ERROR: ' + data.error, 'error');
|
||||
Notification.show('BACKTEST ERROR: ' + data.error, 'error');
|
||||
const btn = document.getElementById('runBacktestBtn');
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
}
|
||||
document.getElementById('backtestStatus').style.display = 'none';
|
||||
this.components.backtest.isRunning = false;
|
||||
});
|
||||
|
||||
// Strategy handlers
|
||||
this.wsManager.on('strategy_update', (data) => {
|
||||
document.getElementById('balanceValue').textContent = '$' + data.balance.toFixed(2);
|
||||
document.getElementById('equityValue').textContent = '$' + data.equity.toFixed(2);
|
||||
document.getElementById('positionsValue').textContent = data.positions;
|
||||
document.getElementById('tradesValue').textContent = data.trades;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const app = new App();
|
||||
app.initialize();
|
||||
window.app = app; // Make available globally for debugging
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Backtest Component
|
||||
* Handles backtesting functionality
|
||||
*/
|
||||
|
||||
export class BacktestComponent {
|
||||
constructor() {
|
||||
this.equityData = [];
|
||||
this.chartCanvas = null;
|
||||
this.chartCtx = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Set default dates
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 30);
|
||||
|
||||
const startInput = document.getElementById('backtestStart');
|
||||
const endInput = document.getElementById('backtestEnd');
|
||||
if (startInput) startInput.value = startDate.toISOString().split('T')[0];
|
||||
if (endInput) endInput.value = endDate.toISOString().split('T')[0];
|
||||
|
||||
// Event listeners
|
||||
const runBtn = document.getElementById('runBacktestBtn');
|
||||
const clearBtn = document.getElementById('clearTerminalBtn');
|
||||
|
||||
if (runBtn) runBtn.addEventListener('click', () => this.run());
|
||||
if (clearBtn) clearBtn.addEventListener('click', () => this.clearTerminal());
|
||||
|
||||
// Initialize chart
|
||||
setTimeout(() => this.initChart(), 100);
|
||||
}
|
||||
|
||||
initChart() {
|
||||
this.chartCanvas = document.getElementById('realtimeChart');
|
||||
if (!this.chartCanvas) return;
|
||||
|
||||
this.chartCtx = this.chartCanvas.getContext('2d');
|
||||
const container = this.chartCanvas.parentElement;
|
||||
this.chartCanvas.width = container.clientWidth - 30;
|
||||
this.chartCanvas.height = 250;
|
||||
|
||||
this.drawChart();
|
||||
}
|
||||
|
||||
async run() {
|
||||
if (this.isRunning) {
|
||||
this.showNotification('Backtest already running', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = document.getElementById('backtestStart')?.value;
|
||||
const endDate = document.getElementById('backtestEnd')?.value;
|
||||
const balance = parseFloat(document.getElementById('backtestBalance')?.value || 1000);
|
||||
const threshold = parseFloat(document.getElementById('threshold')?.value || 0.15);
|
||||
const confidence = parseFloat(document.getElementById('confidence')?.value || 0.7);
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
this.showNotification('PLEASE SELECT START AND END DATES', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('runBacktestBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳ RUNNING...</span>';
|
||||
|
||||
const statusDiv = document.getElementById('backtestStatus');
|
||||
statusDiv.innerHTML = '<div class="loading">RUNNING BACKTEST...</div>';
|
||||
statusDiv.style.display = 'block';
|
||||
|
||||
this.clearTerminal();
|
||||
this.addTerminalLine('Starting backtest...', 'info');
|
||||
|
||||
this.equityData = [];
|
||||
const tradesList = document.getElementById('tradesList');
|
||||
if (tradesList) {
|
||||
tradesList.innerHTML = '<div class="empty-state">No trades yet</div>';
|
||||
}
|
||||
|
||||
setTimeout(() => this.initChart(), 100);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/backtest/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
initial_balance: balance,
|
||||
threshold: threshold,
|
||||
min_confidence: confidence
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.isRunning = true;
|
||||
this.showNotification('BACKTEST STARTED', 'success');
|
||||
} else {
|
||||
this.showNotification('ERROR: ' + data.error, 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Backtest] Error running backtest:', error);
|
||||
this.showNotification('ERROR RUNNING BACKTEST', 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
}
|
||||
}
|
||||
|
||||
displayResults(results) {
|
||||
const resultsDiv = document.getElementById('backtestResults');
|
||||
const statusDiv = document.getElementById('backtestStatus');
|
||||
|
||||
if (resultsDiv && statusDiv) {
|
||||
document.getElementById('backtestReturn').textContent =
|
||||
results.total_return.toFixed(2) + '%';
|
||||
document.getElementById('backtestTrades').textContent = results.total_trades;
|
||||
document.getElementById('backtestWinRate').textContent =
|
||||
results.win_rate.toFixed(1) + '%';
|
||||
document.getElementById('backtestSharpe').textContent =
|
||||
results.sharpe_ratio.toFixed(2);
|
||||
document.getElementById('backtestDrawdown').textContent =
|
||||
results.max_drawdown.toFixed(2) + '%';
|
||||
document.getElementById('backtestEquity').textContent =
|
||||
'$' + results.final_equity.toFixed(2);
|
||||
|
||||
statusDiv.style.display = 'none';
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
clearTerminal() {
|
||||
const terminal = document.getElementById('terminalOutput');
|
||||
if (terminal) {
|
||||
terminal.innerHTML = '<div class="terminal-line">[SYSTEM] Terminal cleared...</div>';
|
||||
}
|
||||
}
|
||||
|
||||
addTerminalLine(message, type = 'info') {
|
||||
const terminal = document.getElementById('terminalOutput');
|
||||
if (!terminal) return;
|
||||
|
||||
const line = document.createElement('div');
|
||||
line.className = `terminal-line ${type}`;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
line.textContent = `[${timestamp}] ${message}`;
|
||||
|
||||
terminal.appendChild(line);
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
|
||||
const lines = terminal.querySelectorAll('.terminal-line');
|
||||
if (lines.length > 100) {
|
||||
lines[0].remove();
|
||||
}
|
||||
}
|
||||
|
||||
updateChart(data) {
|
||||
if (!this.chartCtx) return;
|
||||
|
||||
this.equityData.push({
|
||||
date: new Date(data.date),
|
||||
equity: data.equity,
|
||||
balance: data.balance,
|
||||
unrealized_pnl: data.unrealized_pnl
|
||||
});
|
||||
|
||||
if (this.equityData.length > 1000) {
|
||||
this.equityData.shift();
|
||||
}
|
||||
|
||||
this.drawChart();
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
if (!this.chartCtx || this.equityData.length === 0) return;
|
||||
|
||||
const canvas = this.chartCanvas;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const padding = 40;
|
||||
const chartWidth = width - padding * 2;
|
||||
const chartHeight = height - padding * 2;
|
||||
|
||||
this.chartCtx.fillStyle = '#000';
|
||||
this.chartCtx.fillRect(0, 0, width, height);
|
||||
|
||||
if (this.equityData.length < 2) return;
|
||||
|
||||
const equities = this.equityData.map(d => d.equity);
|
||||
const minEquity = Math.min(...equities);
|
||||
const maxEquity = Math.max(...equities);
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
// Draw grid
|
||||
this.chartCtx.strokeStyle = 'rgba(0, 255, 255, 0.2)';
|
||||
this.chartCtx.lineWidth = 1;
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = padding + (chartHeight / 5) * i;
|
||||
this.chartCtx.beginPath();
|
||||
this.chartCtx.moveTo(padding, y);
|
||||
this.chartCtx.lineTo(width - padding, y);
|
||||
this.chartCtx.stroke();
|
||||
}
|
||||
|
||||
// Draw equity curve
|
||||
this.chartCtx.strokeStyle = '#00ffff';
|
||||
this.chartCtx.lineWidth = 2;
|
||||
this.chartCtx.beginPath();
|
||||
|
||||
this.equityData.forEach((point, index) => {
|
||||
const x = padding + (chartWidth / (this.equityData.length - 1)) * index;
|
||||
const y = padding + chartHeight - ((point.equity - minEquity) / range) * chartHeight;
|
||||
|
||||
if (index === 0) {
|
||||
this.chartCtx.moveTo(x, y);
|
||||
} else {
|
||||
this.chartCtx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
this.chartCtx.stroke();
|
||||
|
||||
// Draw labels
|
||||
this.chartCtx.fillStyle = '#00ffff';
|
||||
this.chartCtx.font = '10px Orbitron';
|
||||
this.chartCtx.fillText(`$${minEquity.toFixed(0)}`, 5, height - padding + 5);
|
||||
this.chartCtx.fillText(`$${maxEquity.toFixed(0)}`, 5, padding + 5);
|
||||
}
|
||||
|
||||
addTrade(trade) {
|
||||
const tradesList = document.getElementById('tradesList');
|
||||
if (!tradesList) return;
|
||||
|
||||
const emptyState = tradesList.querySelector('.empty-state');
|
||||
if (emptyState) emptyState.remove();
|
||||
|
||||
const tradeItem = document.createElement('div');
|
||||
tradeItem.className = `trade-item ${trade.action.toLowerCase()}`;
|
||||
|
||||
const pnl = trade.trade_pnl || 0;
|
||||
const pnlClass = pnl >= 0 ? 'positive' : 'negative';
|
||||
const pnlSign = pnl >= 0 ? '+' : '';
|
||||
|
||||
tradeItem.innerHTML = `
|
||||
<div class="trade-info">
|
||||
<div class="trade-action">${trade.action}</div>
|
||||
<div class="trade-details">
|
||||
Price: ${trade.price.toFixed(4)} | Size: $${trade.size.toFixed(2)} |
|
||||
${new Date(trade.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="trade-pnl ${pnlClass}">
|
||||
${pnlSign}$${Math.abs(pnl).toFixed(2)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
tradesList.insertBefore(tradeItem, tradesList.firstChild);
|
||||
|
||||
while (tradesList.children.length > 50) {
|
||||
tradesList.removeChild(tradesList.lastChild);
|
||||
}
|
||||
|
||||
const tradesCount = document.getElementById('tradesCount');
|
||||
if (tradesCount) {
|
||||
tradesCount.textContent = `${trade.total_trades} trades`;
|
||||
}
|
||||
}
|
||||
|
||||
updateStats(data) {
|
||||
const equityEl = document.getElementById('realtimeEquity');
|
||||
const pnlEl = document.getElementById('realtimePnL');
|
||||
|
||||
if (equityEl) equityEl.textContent = `$${data.equity.toFixed(2)}`;
|
||||
|
||||
if (pnlEl) {
|
||||
const pnl = data.unrealized_pnl || 0;
|
||||
pnlEl.textContent = `${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`;
|
||||
pnlEl.className = pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
if (window.showNotification) {
|
||||
window.showNotification(message, type);
|
||||
} else {
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Markets Component
|
||||
* Handles market data fetching and display
|
||||
*/
|
||||
|
||||
export class MarketsComponent {
|
||||
constructor(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.markets = [];
|
||||
this.updateInterval = null;
|
||||
}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
console.log('[Markets] Loading markets from API...');
|
||||
|
||||
// Show loading state
|
||||
if (this.container) {
|
||||
this.container.innerHTML = '<div class="empty-state">LOADING MARKETS...</div>';
|
||||
}
|
||||
|
||||
const response = await fetch('/api/markets');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('[Markets] API response:', data);
|
||||
console.log('[Markets] Markets count:', data.markets ? data.markets.length : 0);
|
||||
|
||||
if (data.markets && Array.isArray(data.markets)) {
|
||||
this.markets = data.markets;
|
||||
console.log('[Markets] Rendering', this.markets.length, 'markets');
|
||||
this.render();
|
||||
} else {
|
||||
console.error('[Markets] Invalid markets data:', data);
|
||||
this.showError('Invalid data format: ' + JSON.stringify(data).substring(0, 100));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Markets] Error loading markets:', error);
|
||||
this.showError('Failed to load markets: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
|
||||
if (this.markets.length === 0) {
|
||||
this.container.innerHTML = '<div class="empty-state">NO ACTIVE MARKETS</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.innerHTML = this.markets.map(market => {
|
||||
const question = market.question || market.event || 'Unknown Market';
|
||||
const yesPrice = (market.yes_price || 0) * 100;
|
||||
const noPrice = (market.no_price || 0) * 100;
|
||||
const spread = market.spread || Math.abs(yesPrice - noPrice) / 100;
|
||||
|
||||
return `
|
||||
<div class="market-item">
|
||||
<div class="market-question">${question}</div>
|
||||
<div class="market-prices">
|
||||
<div class="price-yes">
|
||||
YES: <span class="price-value">${yesPrice.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="price-no">
|
||||
NO: <span class="price-value">${noPrice.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 0.8rem; color: var(--text-secondary);">
|
||||
Spread: ${(spread * 100).toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
if (this.container) {
|
||||
this.container.innerHTML = `<div class="empty-state">${message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
startAutoRefresh(interval = 30000) {
|
||||
this.updateInterval = setInterval(() => this.load(), interval);
|
||||
}
|
||||
|
||||
stopAutoRefresh() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Positions Component
|
||||
* Handles position display and updates
|
||||
*/
|
||||
|
||||
export class PositionsComponent {
|
||||
constructor(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.positions = [];
|
||||
}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const response = await fetch('/api/strategy/positions');
|
||||
const data = await response.json();
|
||||
this.positions = data.positions || [];
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('[Positions] Error loading positions:', error);
|
||||
this.showError('Failed to load positions');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
|
||||
if (this.positions.length === 0) {
|
||||
this.container.innerHTML = '<div class="empty-state">NO OPEN POSITIONS</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.innerHTML = this.positions.map(pos => {
|
||||
const isProfit = pos.pnl >= 0;
|
||||
return `
|
||||
<div class="position-card ${isProfit ? 'profit' : 'loss'}">
|
||||
<div class="position-header">
|
||||
<div class="position-outcome">${pos.outcome}</div>
|
||||
<div class="position-pnl ${isProfit ? 'positive' : 'negative'}">
|
||||
${isProfit ? '+' : ''}$${pos.pnl.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-details">
|
||||
<div>Size: ${pos.size.toFixed(2)}</div>
|
||||
<div>Entry: ${(pos.entry_price * 100).toFixed(2)}%</div>
|
||||
<div>Current: ${(pos.current_price * 100).toFixed(2)}%</div>
|
||||
<div>P&L: ${pos.pnl_percent.toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
if (this.container) {
|
||||
this.container.innerHTML = `<div class="empty-state">${message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Strategy Component
|
||||
* Handles strategy controls and status
|
||||
*/
|
||||
|
||||
export class StrategyComponent {
|
||||
constructor() {
|
||||
this.isActive = false;
|
||||
this.statusInterval = null;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
|
||||
if (startBtn) startBtn.addEventListener('click', () => this.start());
|
||||
if (stopBtn) stopBtn.addEventListener('click', () => this.stop());
|
||||
|
||||
this.updateStatus();
|
||||
this.startStatusUpdates();
|
||||
}
|
||||
|
||||
async start() {
|
||||
try {
|
||||
const threshold = parseFloat(document.getElementById('threshold')?.value || 0.15);
|
||||
const confidence = parseFloat(document.getElementById('confidence')?.value || 0.7);
|
||||
const balance = parseFloat(document.getElementById('balance')?.value || 1000);
|
||||
const category = document.getElementById('category')?.value || '21';
|
||||
|
||||
const response = await fetch('/api/strategy/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
threshold,
|
||||
min_confidence: confidence,
|
||||
initial_balance: balance,
|
||||
tag_id: parseInt(category)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('startBtn').disabled = true;
|
||||
document.getElementById('stopBtn').disabled = false;
|
||||
this.isActive = true;
|
||||
this.updateStatusIndicator(true);
|
||||
this.showNotification('TRADING STARTED', 'success');
|
||||
} else {
|
||||
this.showNotification('ERROR: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Strategy] Error starting:', error);
|
||||
this.showNotification('ERROR STARTING TRADING', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
try {
|
||||
const response = await fetch('/api/strategy/stop', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('startBtn').disabled = false;
|
||||
document.getElementById('stopBtn').disabled = true;
|
||||
this.isActive = false;
|
||||
this.updateStatusIndicator(false);
|
||||
this.showNotification('TRADING STOPPED', 'info');
|
||||
} else {
|
||||
this.showNotification('ERROR: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Strategy] Error stopping:', error);
|
||||
this.showNotification('ERROR STOPPING TRADING', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/strategy/status');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('balanceValue').textContent = '$' + data.balance.toFixed(2);
|
||||
document.getElementById('equityValue').textContent = '$' + data.equity.toFixed(2);
|
||||
document.getElementById('positionsValue').textContent = data.positions;
|
||||
document.getElementById('tradesValue').textContent = data.trades;
|
||||
document.getElementById('winRateValue').textContent = data.win_rate.toFixed(1) + '%';
|
||||
|
||||
const pnlElement = document.getElementById('pnlValue');
|
||||
const pnl = data.profit || 0;
|
||||
pnlElement.textContent = '$' + pnl.toFixed(2);
|
||||
pnlElement.style.color = pnl >= 0 ? 'var(--neon-green)' : 'var(--neon-pink)';
|
||||
} catch (error) {
|
||||
console.error('[Strategy] Error updating status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusIndicator(active) {
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
if (statusDot && statusText) {
|
||||
if (active) {
|
||||
statusDot.classList.add('active');
|
||||
statusText.textContent = 'ONLINE';
|
||||
} else {
|
||||
statusDot.classList.remove('active');
|
||||
statusText.textContent = 'OFFLINE';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startStatusUpdates() {
|
||||
this.statusInterval = setInterval(() => this.updateStatus(), 2000);
|
||||
}
|
||||
|
||||
stopStatusUpdates() {
|
||||
if (this.statusInterval) {
|
||||
clearInterval(this.statusInterval);
|
||||
this.statusInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Use global notification system if available
|
||||
if (window.showNotification) {
|
||||
window.showNotification(message, type);
|
||||
} else {
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Notification Utility
|
||||
* Global notification system
|
||||
*/
|
||||
|
||||
export class Notification {
|
||||
static show(message, type = 'info') {
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
border: 2px solid var(--neon-cyan);
|
||||
color: var(--neon-cyan);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 0 20px var(--neon-cyan);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Make it globally available
|
||||
window.showNotification = Notification.show;
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* WebSocket Manager
|
||||
* Handles all WebSocket connections and events
|
||||
*/
|
||||
|
||||
export class WebSocketManager {
|
||||
constructor(socket) {
|
||||
this.socket = socket;
|
||||
this.handlers = new Map();
|
||||
this.setup();
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.socket.on('connect', () => {
|
||||
console.log('[WebSocket] Connected to server');
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('[WebSocket] Disconnected from server');
|
||||
});
|
||||
|
||||
// Backtest events
|
||||
this.socket.on('backtest_log', (data) => {
|
||||
this.emit('backtest_log', data);
|
||||
});
|
||||
|
||||
this.socket.on('backtest_trade', (data) => {
|
||||
this.emit('backtest_trade', data);
|
||||
});
|
||||
|
||||
this.socket.on('backtest_equity', (data) => {
|
||||
this.emit('backtest_equity', data);
|
||||
});
|
||||
|
||||
this.socket.on('backtest_complete', (data) => {
|
||||
this.emit('backtest_complete', data);
|
||||
});
|
||||
|
||||
this.socket.on('backtest_error', (data) => {
|
||||
this.emit('backtest_error', data);
|
||||
});
|
||||
|
||||
// Strategy events
|
||||
this.socket.on('strategy_update', (data) => {
|
||||
this.emit('strategy_update', data);
|
||||
});
|
||||
}
|
||||
|
||||
on(event, handler) {
|
||||
if (!this.handlers.has(event)) {
|
||||
this.handlers.set(event, []);
|
||||
}
|
||||
this.handlers.get(event).push(handler);
|
||||
}
|
||||
|
||||
off(event, handler) {
|
||||
if (this.handlers.has(event)) {
|
||||
const handlers = this.handlers.get(event);
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.handlers.has(event)) {
|
||||
this.handlers.get(event).forEach(handler => handler(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,752 @@
|
||||
// Cyberpunk Dashboard JavaScript
|
||||
|
||||
const socket = io();
|
||||
let updateInterval;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeControls();
|
||||
loadMarkets();
|
||||
startStatusUpdates();
|
||||
setupWebSocket();
|
||||
initializeBacktest();
|
||||
});
|
||||
|
||||
// Control Initialization
|
||||
function initializeControls() {
|
||||
const threshold = document.getElementById('threshold');
|
||||
const confidence = document.getElementById('confidence');
|
||||
const thresholdValue = document.getElementById('thresholdValue');
|
||||
const confidenceValue = document.getElementById('confidenceValue');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
|
||||
threshold.addEventListener('input', (e) => {
|
||||
thresholdValue.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
|
||||
confidence.addEventListener('input', (e) => {
|
||||
confidenceValue.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
|
||||
startBtn.addEventListener('click', startTrading);
|
||||
stopBtn.addEventListener('click', stopTrading);
|
||||
}
|
||||
|
||||
// Load Markets
|
||||
async function loadMarkets() {
|
||||
try {
|
||||
console.log('[DEBUG] Loading markets from API...');
|
||||
const response = await fetch('/api/markets');
|
||||
const data = await response.json();
|
||||
|
||||
console.log('[DEBUG] API response:', data);
|
||||
console.log('[DEBUG] Markets array:', data.markets);
|
||||
console.log('[DEBUG] Markets count:', data.markets ? data.markets.length : 0);
|
||||
|
||||
if (data.markets && Array.isArray(data.markets)) {
|
||||
console.log('[DEBUG] Displaying', data.markets.length, 'markets');
|
||||
displayMarkets(data.markets);
|
||||
} else {
|
||||
console.error('[DEBUG] Invalid markets data:', data);
|
||||
document.getElementById('marketsList').innerHTML =
|
||||
'<div class="empty-state">NO ACTIVE MARKETS (Invalid data format)</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading markets:', error);
|
||||
document.getElementById('marketsList').innerHTML =
|
||||
'<div class="loading">ERROR LOADING MARKETS: ' + error.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Display Markets
|
||||
function displayMarkets(markets) {
|
||||
const container = document.getElementById('marketsList');
|
||||
|
||||
if (!container) {
|
||||
console.error('[DEBUG] marketsList container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[DEBUG] displayMarkets called with', markets.length, 'markets');
|
||||
|
||||
if (!markets || markets.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">NO ACTIVE MARKETS</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = markets.map(market => {
|
||||
const question = market.question || market.event || 'Unknown Market';
|
||||
const yesPrice = (market.yes_price || 0) * 100;
|
||||
const noPrice = (market.no_price || 0) * 100;
|
||||
const spread = market.spread || Math.abs(yesPrice - noPrice) / 100;
|
||||
|
||||
return `
|
||||
<div class="market-item">
|
||||
<div class="market-question">${question}</div>
|
||||
<div class="market-prices">
|
||||
<div class="price-yes">
|
||||
YES: <span class="price-value">${yesPrice.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="price-no">
|
||||
NO: <span class="price-value">${noPrice.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 0.8rem; color: var(--text-secondary);">
|
||||
Spread: ${(spread * 100).toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
console.log('[DEBUG] Markets displayed successfully');
|
||||
}
|
||||
|
||||
// Start Trading
|
||||
async function startTrading() {
|
||||
const threshold = parseFloat(document.getElementById('threshold').value);
|
||||
const confidence = parseFloat(document.getElementById('confidence').value);
|
||||
const balance = parseFloat(document.getElementById('balance').value);
|
||||
const category = document.getElementById('category').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/strategy/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
threshold,
|
||||
min_confidence: confidence,
|
||||
initial_balance: balance,
|
||||
tag_id: parseInt(category)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('startBtn').disabled = true;
|
||||
document.getElementById('stopBtn').disabled = false;
|
||||
updateStatus(true);
|
||||
showNotification('TRADING STARTED', 'success');
|
||||
} else {
|
||||
showNotification('ERROR: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting trading:', error);
|
||||
showNotification('ERROR STARTING TRADING', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Stop Trading
|
||||
async function stopTrading() {
|
||||
try {
|
||||
const response = await fetch('/api/strategy/stop', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('startBtn').disabled = false;
|
||||
document.getElementById('stopBtn').disabled = true;
|
||||
updateStatus(false);
|
||||
showNotification('TRADING STOPPED', 'info');
|
||||
} else {
|
||||
showNotification('ERROR: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stopping trading:', error);
|
||||
showNotification('ERROR STOPPING TRADING', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Status Updates
|
||||
function startStatusUpdates() {
|
||||
updateInterval = setInterval(async () => {
|
||||
await updateStrategyStatus();
|
||||
await updatePositions();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Update Strategy Status
|
||||
async function updateStrategyStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/strategy/status');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('balanceValue').textContent =
|
||||
'$' + data.balance.toFixed(2);
|
||||
document.getElementById('equityValue').textContent =
|
||||
'$' + data.equity.toFixed(2);
|
||||
document.getElementById('positionsValue').textContent =
|
||||
data.positions;
|
||||
document.getElementById('tradesValue').textContent =
|
||||
data.trades;
|
||||
document.getElementById('winRateValue').textContent =
|
||||
data.win_rate.toFixed(1) + '%';
|
||||
|
||||
const pnlElement = document.getElementById('pnlValue');
|
||||
const pnl = data.profit || 0;
|
||||
pnlElement.textContent = '$' + pnl.toFixed(2);
|
||||
pnlElement.style.color = pnl >= 0 ? 'var(--neon-green)' : 'var(--neon-pink)';
|
||||
} catch (error) {
|
||||
console.error('Error updating status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Positions
|
||||
async function updatePositions() {
|
||||
try {
|
||||
const response = await fetch('/api/strategy/positions');
|
||||
const data = await response.json();
|
||||
|
||||
displayPositions(data.positions || []);
|
||||
} catch (error) {
|
||||
console.error('Error updating positions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Display Positions
|
||||
function displayPositions(positions) {
|
||||
const container = document.getElementById('positionsList');
|
||||
|
||||
if (positions.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">NO OPEN POSITIONS</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = positions.map(pos => {
|
||||
const isProfit = pos.pnl >= 0;
|
||||
return `
|
||||
<div class="position-card ${isProfit ? 'profit' : 'loss'}">
|
||||
<div class="position-header">
|
||||
<div class="position-outcome">${pos.outcome}</div>
|
||||
<div class="position-pnl ${isProfit ? 'positive' : 'negative'}">
|
||||
${isProfit ? '+' : ''}$${pos.pnl.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-details">
|
||||
<div>Size: ${pos.size.toFixed(2)}</div>
|
||||
<div>Entry: ${(pos.entry_price * 100).toFixed(2)}%</div>
|
||||
<div>Current: ${(pos.current_price * 100).toFixed(2)}%</div>
|
||||
<div>P&L: ${pos.pnl_percent.toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update Status Indicator
|
||||
function updateStatus(active) {
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
if (active) {
|
||||
statusDot.classList.add('active');
|
||||
statusText.textContent = 'ONLINE';
|
||||
} else {
|
||||
statusDot.classList.remove('active');
|
||||
statusText.textContent = 'OFFLINE';
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket Setup
|
||||
function setupWebSocket() {
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to server');
|
||||
});
|
||||
|
||||
socket.on('strategy_update', (data) => {
|
||||
// Real-time updates via WebSocket
|
||||
document.getElementById('balanceValue').textContent =
|
||||
'$' + data.balance.toFixed(2);
|
||||
document.getElementById('equityValue').textContent =
|
||||
'$' + data.equity.toFixed(2);
|
||||
document.getElementById('positionsValue').textContent =
|
||||
data.positions;
|
||||
document.getElementById('tradesValue').textContent =
|
||||
data.trades;
|
||||
});
|
||||
|
||||
socket.on('backtest_log', (data) => {
|
||||
addTerminalLine(data.message, data.type || 'info');
|
||||
});
|
||||
|
||||
socket.on('backtest_trade', (data) => {
|
||||
addTradeToList(data);
|
||||
updateRealtimeStats(data);
|
||||
});
|
||||
|
||||
socket.on('backtest_equity', (data) => {
|
||||
updateRealtimeChart(data);
|
||||
updateRealtimeStats(data);
|
||||
});
|
||||
|
||||
socket.on('backtest_complete', (data) => {
|
||||
displayBacktestResults(data);
|
||||
});
|
||||
|
||||
socket.on('backtest_error', (data) => {
|
||||
addTerminalLine('ERROR: ' + data.error, 'error');
|
||||
showNotification('BACKTEST ERROR: ' + data.error, 'error');
|
||||
const btn = document.getElementById('runBacktestBtn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
document.getElementById('backtestStatus').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Notification System
|
||||
function showNotification(message, type = 'info') {
|
||||
// Simple notification - can be enhanced with a toast system
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
border: 2px solid var(--neon-cyan);
|
||||
color: var(--neon-cyan);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 0 20px var(--neon-cyan);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Backtesting Functions
|
||||
function initializeBacktest() {
|
||||
// Set default dates (last 30 days)
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 30);
|
||||
|
||||
document.getElementById('backtestStart').value = startDate.toISOString().split('T')[0];
|
||||
document.getElementById('backtestEnd').value = endDate.toISOString().split('T')[0];
|
||||
|
||||
document.getElementById('runBacktestBtn').addEventListener('click', runBacktest);
|
||||
document.getElementById('clearTerminalBtn').addEventListener('click', clearTerminal);
|
||||
|
||||
// Initialize real-time chart (wait for DOM to be ready)
|
||||
setTimeout(() => {
|
||||
initRealtimeChart();
|
||||
}, 100);
|
||||
|
||||
// Clear trades list on new backtest
|
||||
const tradesList = document.getElementById('tradesList');
|
||||
if (tradesList) {
|
||||
tradesList.innerHTML = '<div class="empty-state">No trades yet</div>';
|
||||
}
|
||||
equityData = [];
|
||||
}
|
||||
|
||||
function clearTerminal() {
|
||||
document.getElementById('terminalOutput').innerHTML =
|
||||
'<div class="terminal-line">[SYSTEM] Terminal cleared...</div>';
|
||||
}
|
||||
|
||||
function addTerminalLine(message, type = 'info') {
|
||||
const terminal = document.getElementById('terminalOutput');
|
||||
const line = document.createElement('div');
|
||||
line.className = `terminal-line ${type}`;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
line.textContent = `[${timestamp}] ${message}`;
|
||||
|
||||
terminal.appendChild(line);
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
|
||||
// Keep only last 100 lines
|
||||
const lines = terminal.querySelectorAll('.terminal-line');
|
||||
if (lines.length > 100) {
|
||||
lines[0].remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Real-time chart data
|
||||
let equityData = [];
|
||||
let chartCanvas = null;
|
||||
let chartCtx = null;
|
||||
|
||||
function initRealtimeChart() {
|
||||
chartCanvas = document.getElementById('realtimeChart');
|
||||
if (!chartCanvas) return;
|
||||
|
||||
chartCtx = chartCanvas.getContext('2d');
|
||||
equityData = [];
|
||||
|
||||
// Set canvas size
|
||||
const container = chartCanvas.parentElement;
|
||||
chartCanvas.width = container.clientWidth - 30;
|
||||
chartCanvas.height = 250;
|
||||
|
||||
// Draw initial chart
|
||||
drawChart();
|
||||
}
|
||||
|
||||
function updateRealtimeChart(data) {
|
||||
if (!chartCtx) return;
|
||||
|
||||
equityData.push({
|
||||
date: new Date(data.date),
|
||||
equity: data.equity,
|
||||
balance: data.balance,
|
||||
unrealized_pnl: data.unrealized_pnl
|
||||
});
|
||||
|
||||
// Keep only last 1000 points
|
||||
if (equityData.length > 1000) {
|
||||
equityData.shift();
|
||||
}
|
||||
|
||||
drawChart();
|
||||
}
|
||||
|
||||
function drawChart() {
|
||||
if (!chartCtx || equityData.length === 0) return;
|
||||
|
||||
const canvas = chartCanvas;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const padding = 40;
|
||||
const chartWidth = width - padding * 2;
|
||||
const chartHeight = height - padding * 2;
|
||||
|
||||
// Clear canvas
|
||||
chartCtx.fillStyle = '#000';
|
||||
chartCtx.fillRect(0, 0, width, height);
|
||||
|
||||
if (equityData.length < 2) return;
|
||||
|
||||
// Find min/max equity
|
||||
const equities = equityData.map(d => d.equity);
|
||||
const minEquity = Math.min(...equities);
|
||||
const maxEquity = Math.max(...equities);
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
// Draw grid
|
||||
chartCtx.strokeStyle = 'rgba(0, 255, 255, 0.2)';
|
||||
chartCtx.lineWidth = 1;
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = padding + (chartHeight / 5) * i;
|
||||
chartCtx.beginPath();
|
||||
chartCtx.moveTo(padding, y);
|
||||
chartCtx.lineTo(width - padding, y);
|
||||
chartCtx.stroke();
|
||||
}
|
||||
|
||||
// Draw equity curve
|
||||
chartCtx.strokeStyle = '#00ffff';
|
||||
chartCtx.lineWidth = 2;
|
||||
chartCtx.beginPath();
|
||||
|
||||
equityData.forEach((point, index) => {
|
||||
const x = padding + (chartWidth / (equityData.length - 1)) * index;
|
||||
const y = padding + chartHeight - ((point.equity - minEquity) / range) * chartHeight;
|
||||
|
||||
if (index === 0) {
|
||||
chartCtx.moveTo(x, y);
|
||||
} else {
|
||||
chartCtx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
chartCtx.stroke();
|
||||
|
||||
// Draw balance line
|
||||
chartCtx.strokeStyle = 'rgba(255, 0, 255, 0.5)';
|
||||
chartCtx.lineWidth = 1;
|
||||
chartCtx.beginPath();
|
||||
|
||||
equityData.forEach((point, index) => {
|
||||
const x = padding + (chartWidth / (equityData.length - 1)) * index;
|
||||
const y = padding + chartHeight - ((point.balance - minEquity) / range) * chartHeight;
|
||||
|
||||
if (index === 0) {
|
||||
chartCtx.moveTo(x, y);
|
||||
} else {
|
||||
chartCtx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
chartCtx.stroke();
|
||||
|
||||
// Draw labels
|
||||
chartCtx.fillStyle = '#00ffff';
|
||||
chartCtx.font = '10px Orbitron';
|
||||
chartCtx.fillText(`$${minEquity.toFixed(0)}`, 5, height - padding + 5);
|
||||
chartCtx.fillText(`$${maxEquity.toFixed(0)}`, 5, padding + 5);
|
||||
}
|
||||
|
||||
function addTradeToList(trade) {
|
||||
const tradesList = document.getElementById('tradesList');
|
||||
if (!tradesList) return;
|
||||
|
||||
// Remove empty state
|
||||
const emptyState = tradesList.querySelector('.empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
const tradeItem = document.createElement('div');
|
||||
tradeItem.className = `trade-item ${trade.action.toLowerCase()}`;
|
||||
|
||||
const pnl = trade.trade_pnl || 0;
|
||||
const pnlClass = pnl >= 0 ? 'positive' : 'negative';
|
||||
const pnlSign = pnl >= 0 ? '+' : '';
|
||||
|
||||
tradeItem.innerHTML = `
|
||||
<div class="trade-info">
|
||||
<div class="trade-action">${trade.action}</div>
|
||||
<div class="trade-details">
|
||||
Price: ${trade.price.toFixed(4)} | Size: $${trade.size.toFixed(2)} |
|
||||
${new Date(trade.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="trade-pnl ${pnlClass}">
|
||||
${pnlSign}$${Math.abs(pnl).toFixed(2)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
tradesList.insertBefore(tradeItem, tradesList.firstChild);
|
||||
|
||||
// Keep only last 50 trades
|
||||
while (tradesList.children.length > 50) {
|
||||
tradesList.removeChild(tradesList.lastChild);
|
||||
}
|
||||
|
||||
// Update trades count
|
||||
const tradesCount = document.getElementById('tradesCount');
|
||||
if (tradesCount) {
|
||||
tradesCount.textContent = `${trade.total_trades} trades`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRealtimeStats(data) {
|
||||
const equityEl = document.getElementById('realtimeEquity');
|
||||
const pnlEl = document.getElementById('realtimePnL');
|
||||
|
||||
if (equityEl) {
|
||||
equityEl.textContent = `$${data.equity.toFixed(2)}`;
|
||||
}
|
||||
|
||||
if (pnlEl) {
|
||||
const pnl = data.unrealized_pnl || 0;
|
||||
pnlEl.textContent = `${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`;
|
||||
pnlEl.className = pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
|
||||
}
|
||||
}
|
||||
|
||||
async function runBacktest() {
|
||||
const startDate = document.getElementById('backtestStart').value;
|
||||
const endDate = document.getElementById('backtestEnd').value;
|
||||
const balance = parseFloat(document.getElementById('backtestBalance').value);
|
||||
const threshold = parseFloat(document.getElementById('threshold').value);
|
||||
const confidence = parseFloat(document.getElementById('confidence').value);
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
showNotification('PLEASE SELECT START AND END DATES', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('runBacktestBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳ RUNNING...</span>';
|
||||
|
||||
const statusDiv = document.getElementById('backtestStatus');
|
||||
statusDiv.innerHTML = '<div class="loading">RUNNING BACKTEST...</div>';
|
||||
statusDiv.style.display = 'block';
|
||||
|
||||
// Clear terminal and add initial message
|
||||
clearTerminal();
|
||||
addTerminalLine('Starting backtest...', 'info');
|
||||
|
||||
// Reset chart and trades
|
||||
equityData = [];
|
||||
const tradesList = document.getElementById('tradesList');
|
||||
if (tradesList) {
|
||||
tradesList.innerHTML = '<div class="empty-state">No trades yet</div>';
|
||||
}
|
||||
|
||||
// Reinitialize chart
|
||||
setTimeout(() => {
|
||||
initRealtimeChart();
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/backtest/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
initial_balance: balance,
|
||||
threshold: threshold,
|
||||
min_confidence: confidence
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showNotification('BACKTEST STARTED', 'success');
|
||||
// Results will come via WebSocket
|
||||
} else {
|
||||
showNotification('ERROR: ' + data.error, 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error running backtest:', error);
|
||||
showNotification('ERROR RUNNING BACKTEST', 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayBacktestResults(results) {
|
||||
const resultsDiv = document.getElementById('backtestResults');
|
||||
const statusDiv = document.getElementById('backtestStatus');
|
||||
|
||||
// Update metrics
|
||||
document.getElementById('backtestReturn').textContent =
|
||||
results.total_return.toFixed(2) + '%';
|
||||
document.getElementById('backtestReturn').style.color =
|
||||
results.total_return >= 0 ? 'var(--neon-green)' : 'var(--neon-pink)';
|
||||
|
||||
document.getElementById('backtestTrades').textContent = results.total_trades;
|
||||
document.getElementById('backtestWinRate').textContent =
|
||||
results.win_rate.toFixed(1) + '%';
|
||||
document.getElementById('backtestSharpe').textContent =
|
||||
results.sharpe_ratio.toFixed(2);
|
||||
document.getElementById('backtestDrawdown').textContent =
|
||||
results.max_drawdown.toFixed(2) + '%';
|
||||
document.getElementById('backtestEquity').textContent =
|
||||
'$' + results.final_equity.toFixed(2);
|
||||
|
||||
// Draw equity curve chart
|
||||
drawEquityChart(results.equity_curve);
|
||||
|
||||
// Show results
|
||||
statusDiv.style.display = 'none';
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
// Re-enable button
|
||||
const btn = document.getElementById('runBacktestBtn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>▶ RUN BACKTEST</span>';
|
||||
|
||||
showNotification('BACKTEST COMPLETE', 'success');
|
||||
}
|
||||
|
||||
function drawEquityChart(equityCurve) {
|
||||
const canvas = document.getElementById('backtestChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!equityCurve || equityCurve.length === 0) {
|
||||
ctx.fillStyle = 'var(--text-secondary)';
|
||||
ctx.font = '14px Orbitron';
|
||||
ctx.fillText('No data available', 10, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Setup
|
||||
const padding = 40;
|
||||
const width = canvas.width - padding * 2;
|
||||
const height = canvas.height - padding * 2;
|
||||
|
||||
// Find min/max for scaling
|
||||
const equities = equityCurve.map(p => p.equity);
|
||||
const minEquity = Math.min(...equities);
|
||||
const maxEquity = Math.max(...equities);
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = 'rgba(0, 255, 255, 0.2)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = padding + (height / 5) * i;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padding, y);
|
||||
ctx.lineTo(canvas.width - padding, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw equity curve
|
||||
ctx.strokeStyle = 'var(--neon-cyan)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
equityCurve.forEach((point, index) => {
|
||||
const x = padding + (width / (equityCurve.length - 1)) * index;
|
||||
const y = padding + height - ((point.equity - minEquity) / range) * height;
|
||||
|
||||
if (index === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw glow effect
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.shadowColor = 'var(--neon-cyan)';
|
||||
ctx.stroke();
|
||||
|
||||
// Draw labels
|
||||
ctx.fillStyle = 'var(--text-secondary)';
|
||||
ctx.font = '10px Orbitron';
|
||||
ctx.fillText('$' + minEquity.toFixed(0), 5, canvas.height - padding);
|
||||
ctx.fillText('$' + maxEquity.toFixed(0), 5, padding + 10);
|
||||
}
|
||||
|
||||
// Add animations
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -0,0 +1,866 @@
|
||||
/* Cyberpunk Theme Styles */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;400;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--neon-cyan: #00ffff;
|
||||
--neon-pink: #ff00ff;
|
||||
--neon-green: #00ff00;
|
||||
--neon-yellow: #ffff00;
|
||||
--dark-bg: #0a0a0a;
|
||||
--darker-bg: #050505;
|
||||
--panel-bg: rgba(10, 10, 20, 0.8);
|
||||
--border-color: #00ffff;
|
||||
--text-primary: #00ffff;
|
||||
--text-secondary: #00ff88;
|
||||
--glow-intensity: 0 0 10px, 0 0 20px, 0 0 30px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
background: var(--dark-bg);
|
||||
color: var(--text-primary);
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.cyberpunk-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Grid Background */
|
||||
.grid-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 255, 255, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 255, 255, 0.1) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
z-index: 0;
|
||||
opacity: 0.3;
|
||||
animation: gridMove 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
}
|
||||
|
||||
/* Particles Effect */
|
||||
.particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20% 30%, var(--neon-cyan), transparent),
|
||||
radial-gradient(2px 2px at 60% 70%, var(--neon-pink), transparent),
|
||||
radial-gradient(1px 1px at 50% 50%, var(--neon-green), transparent);
|
||||
background-size: 200% 200%;
|
||||
animation: particles 15s ease infinite;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@keyframes particles {
|
||||
0%, 100% { background-position: 0% 0%, 100% 100%, 50% 50%; }
|
||||
50% { background-position: 100% 0%, 0% 100%, 50% 50%; }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.cyberpunk-header {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.glitch {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
color: var(--neon-cyan);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
text-shadow:
|
||||
0 0 10px var(--neon-cyan),
|
||||
0 0 20px var(--neon-cyan),
|
||||
0 0 30px var(--neon-cyan),
|
||||
0 0 40px var(--neon-cyan);
|
||||
animation: glitch 2s infinite;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.glitch::before,
|
||||
.glitch::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.glitch::before {
|
||||
left: 2px;
|
||||
text-shadow: -2px 0 var(--neon-pink);
|
||||
clip: rect(44px, 450px, 56px, 0);
|
||||
animation: glitch-anim 5s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
.glitch::after {
|
||||
left: -2px;
|
||||
text-shadow: 2px 0 var(--neon-green);
|
||||
clip: rect(44px, 450px, 56px, 0);
|
||||
animation: glitch-anim 1s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0%, 100% { transform: translate(0); }
|
||||
20% { transform: translate(-2px, 2px); }
|
||||
40% { transform: translate(-2px, -2px); }
|
||||
60% { transform: translate(2px, 2px); }
|
||||
80% { transform: translate(2px, -2px); }
|
||||
}
|
||||
|
||||
@keyframes glitch-anim {
|
||||
0% { clip: rect(31px, 9999px, 94px, 0); }
|
||||
5% { clip: rect(14px, 9999px, 29px, 0); }
|
||||
10% { clip: rect(95px, 9999px, 96px, 0); }
|
||||
15% { clip: rect(9px, 9999px, 97px, 0); }
|
||||
20% { clip: rect(43px, 9999px, 27px, 0); }
|
||||
25% { clip: rect(87px, 9999px, 3px, 0); }
|
||||
30% { clip: rect(80px, 9999px, 94px, 0); }
|
||||
35% { clip: rect(66px, 9999px, 28px, 0); }
|
||||
40% { clip: rect(68px, 9999px, 100px, 0); }
|
||||
45% { clip: rect(14px, 9999px, 33px, 0); }
|
||||
50% { clip: rect(60px, 9999px, 85px, 0); }
|
||||
55% { clip: rect(75px, 9999px, 5px, 0); }
|
||||
60% { clip: rect(1px, 9999px, 80px, 0); }
|
||||
65% { clip: rect(79px, 9999px, 63px, 0); }
|
||||
70% { clip: rect(17px, 9999px, 79px, 0); }
|
||||
75% { clip: rect(85px, 9999px, 65px, 0); }
|
||||
80% { clip: rect(60px, 9999px, 27px, 0); }
|
||||
85% { clip: rect(38px, 9999px, 73px, 0); }
|
||||
90% { clip: rect(50px, 9999px, 29px, 0); }
|
||||
95% { clip: rect(3px, 9999px, 14px, 0); }
|
||||
100% { clip: rect(88px, 9999px, 53px, 0); }
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.2rem;
|
||||
color: var(--neon-pink);
|
||||
letter-spacing: 0.3em;
|
||||
margin-top: 10px;
|
||||
text-shadow: 0 0 10px var(--neon-pink);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--neon-pink);
|
||||
box-shadow: 0 0 10px var(--neon-pink);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: var(--neon-green);
|
||||
box-shadow: 0 0 10px var(--neon-green);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Dashboard Grid */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.backtest-panel.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
background: var(--panel-bg);
|
||||
border: 2px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 0 10px rgba(0, 255, 255, 0.3),
|
||||
inset 0 0 20px rgba(0, 255, 255, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
padding: 15px 20px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.2rem;
|
||||
color: var(--neon-cyan);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: 0 0 10px var(--neon-cyan);
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
var(--neon-cyan),
|
||||
transparent);
|
||||
animation: scan 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.control-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.control-group input[type="range"] {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(0, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--neon-cyan);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 10px var(--neon-cyan);
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--neon-cyan);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 0 10px var(--neon-cyan);
|
||||
}
|
||||
|
||||
.control-group input[type="number"],
|
||||
.control-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-group input[type="number"]:focus,
|
||||
.control-group select:focus {
|
||||
box-shadow: 0 0 10px var(--neon-cyan);
|
||||
}
|
||||
|
||||
.control-group span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
color: var(--neon-cyan);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.cyber-button {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin-top: 10px;
|
||||
background: transparent;
|
||||
border: 2px solid var(--neon-cyan);
|
||||
color: var(--neon-cyan);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.cyber-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--neon-cyan);
|
||||
transition: left 0.3s;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.cyber-button:hover::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.cyber-button:hover {
|
||||
color: var(--dark-bg);
|
||||
box-shadow: 0 0 20px var(--neon-cyan);
|
||||
}
|
||||
|
||||
.cyber-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
border-color: var(--neon-green);
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.start-btn::before {
|
||||
background: var(--neon-green);
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
border-color: var(--neon-pink);
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.stop-btn::before {
|
||||
background: var(--neon-pink);
|
||||
}
|
||||
|
||||
/* Markets List */
|
||||
.markets-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.market-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||
border-left: 4px solid var(--neon-cyan);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.market-item:hover {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
border-color: var(--neon-cyan);
|
||||
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.market-question {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.market-prices {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.price-yes {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.price-no {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Metrics */
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--neon-cyan);
|
||||
text-shadow: 0 0 10px var(--neon-cyan);
|
||||
}
|
||||
|
||||
/* Positions */
|
||||
.positions-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.position-card {
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||
padding: 15px;
|
||||
border-left: 4px solid var(--neon-cyan);
|
||||
}
|
||||
|
||||
.position-card.profit {
|
||||
border-left-color: var(--neon-green);
|
||||
}
|
||||
|
||||
.position-card.loss {
|
||||
border-left-color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.position-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.position-outcome {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.position-pnl {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.position-pnl.positive {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.position-pnl.negative {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.position-details {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Loading & Empty States */
|
||||
.loading,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--neon-cyan);
|
||||
box-shadow: 0 0 10px var(--neon-cyan);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--neon-green);
|
||||
}
|
||||
|
||||
/* Backtesting */
|
||||
.backtest-controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.backtest-btn {
|
||||
margin-top: 20px;
|
||||
border-color: var(--neon-yellow);
|
||||
color: var(--neon-yellow);
|
||||
}
|
||||
|
||||
.backtest-btn::before {
|
||||
background: var(--neon-yellow);
|
||||
}
|
||||
|
||||
.backtest-results {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.backtest-metrics {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
|
||||
.metric-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-row .metric-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.metric-row .metric-value {
|
||||
color: var(--neon-cyan);
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#backtestChart {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.backtest-status {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Update grid for backtesting panel */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.positions-panel {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.backtest-panel {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
/* Terminal Panel */
|
||||
.terminal-panel {
|
||||
margin-top: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 2px solid var(--neon-cyan);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
padding: 8px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--neon-cyan);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
color: var(--neon-cyan);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.terminal-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--neon-pink);
|
||||
color: var(--neon-pink);
|
||||
padding: 4px 10px;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.terminal-btn:hover {
|
||||
background: var(--neon-pink);
|
||||
color: var(--dark-bg);
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
background: #000;
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 4px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.terminal-line.info {
|
||||
color: var(--neon-cyan);
|
||||
}
|
||||
|
||||
.terminal-line.success {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.terminal-line.warning {
|
||||
color: var(--neon-yellow);
|
||||
}
|
||||
|
||||
.terminal-line.error {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.terminal-line.trade {
|
||||
color: var(--neon-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-line.market {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Real-time Chart */
|
||||
.chart-container {
|
||||
margin-top: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 2px solid var(--neon-cyan);
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--neon-cyan);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.chart-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.chart-stats span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pnl-positive {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.pnl-negative {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
#realtimeChart {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
background: #000;
|
||||
border: 1px solid var(--neon-cyan);
|
||||
}
|
||||
|
||||
/* Trades Panel */
|
||||
.trades-panel {
|
||||
margin-top: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 2px solid var(--neon-pink);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.trades-header {
|
||||
background: rgba(255, 0, 255, 0.1);
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--neon-pink);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
color: var(--neon-pink);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.trades-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.trade-item {
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left: 3px solid var(--neon-cyan);
|
||||
border-radius: 2px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trade-item.buy {
|
||||
border-left-color: var(--neon-green);
|
||||
}
|
||||
|
||||
.trade-item.sell {
|
||||
border-left-color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.trade-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trade-action {
|
||||
font-weight: 700;
|
||||
color: var(--neon-cyan);
|
||||
}
|
||||
|
||||
.trade-item.buy .trade-action {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.trade-item.sell .trade-action {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
.trade-details {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.trade-pnl {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.trade-pnl.positive {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.trade-pnl.negative {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.positions-panel,
|
||||
.backtest-panel {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user