Files
gogo2/ANNOTATE/web/templates/components/training_panel.html
2025-11-17 19:13:30 +02:00

1002 lines
41 KiB
HTML

<div class="card training-panel">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-graduation-cap"></i>
Training
</h6>
</div>
<div class="card-body p-2">
<!-- Model Selection -->
<div class="mb-3">
<label for="model-select" class="form-label small">Model</label>
<select class="form-select form-select-sm" id="model-select">
<option value="">Loading models...</option>
</select>
</div>
<!-- Training Controls -->
<div class="mb-3">
<button class="btn btn-primary btn-sm w-100" id="train-model-btn" style="display: none;">
<i class="fas fa-play"></i>
Train Model
</button>
<button class="btn btn-success btn-sm w-100" id="load-model-btn" style="display: none;">
<i class="fas fa-download"></i>
Load Model
</button>
</div>
<!-- Training Status -->
<div id="training-status" style="display: none;">
<div class="alert alert-info py-2 px-2 mb-2">
<div class="d-flex align-items-center mb-1">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Training...</span>
</div>
<strong class="small">Training in progress</strong>
</div>
<div class="progress mb-1" style="height: 10px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="training-progress-bar"
role="progressbar" style="width: 0%"></div>
</div>
<div class="small">
<div>Epoch: <span id="training-epoch">0</span>/<span id="training-total-epochs">0</span></div>
<div>Loss: <span id="training-loss">--</span></div>
</div>
</div>
</div>
<!-- Training Results -->
<div id="training-results" style="display: none;">
<div class="alert alert-success py-2 px-2 mb-2">
<strong class="small">
<i class="fas fa-check-circle"></i>
Training Complete
</strong>
<div class="small mt-1">
<div>Final Loss: <span id="result-loss">--</span></div>
<div>Accuracy: <span id="result-accuracy">--</span></div>
<div>Duration: <span id="result-duration">--</span></div>
</div>
</div>
</div>
<!-- Real-Time Inference -->
<div class="mb-3">
<label class="form-label small">Real-Time Inference</label>
<!-- Primary Timeframe Selector -->
<div class="mb-2">
<label for="primary-timeframe-select" class="form-label small text-muted">Primary Timeframe</label>
<select class="form-select form-select-sm" id="primary-timeframe-select">
<option value="1s">1 Second</option>
<option value="1m" selected>1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minutes</option>
<option value="1h">1 Hour</option>
</select>
</div>
<button class="btn btn-success btn-sm w-100" id="start-inference-btn">
<i class="fas fa-play"></i>
Start Live Inference
</button>
<button class="btn btn-danger btn-sm w-100 mt-1" id="stop-inference-btn" style="display: none;">
<i class="fas fa-stop"></i>
Stop Inference
</button>
</div>
<!-- Backtest on Visible Chart -->
<div class="mb-3">
<label class="form-label small">Backtest on Visible Data</label>
<button class="btn btn-warning btn-sm w-100" id="start-backtest-btn">
<i class="fas fa-history"></i>
Backtest Visible Chart
</button>
<button class="btn btn-danger btn-sm w-100 mt-1" id="stop-backtest-btn" style="display: none;">
<i class="fas fa-stop"></i>
Stop Backtest
</button>
<!-- Backtest Results -->
<div id="backtest-results" style="display: none;" class="mt-2">
<div class="alert alert-success py-2 px-2 mb-0">
<strong class="small">Backtest Results</strong>
<div class="small mt-1">
<div>PnL: <span id="backtest-pnl" class="fw-bold">--</span></div>
<div>Trades: <span id="backtest-trades">--</span></div>
<div>Win Rate: <span id="backtest-winrate">--</span></div>
<div>Progress: <span id="backtest-progress">0</span>/<span id="backtest-total">0</span></div>
</div>
</div>
</div>
</div>
<!-- Multi-Step Inference Control -->
<div class="mb-3" id="inference-controls" style="display: none;">
<label for="prediction-steps-slider" class="form-label small text-muted">
Prediction Steps: <span id="prediction-steps-value">1</span>
</label>
<input type="range" class="form-range" id="prediction-steps-slider"
min="1" max="15" value="1" step="1">
<div class="small text-muted" style="font-size: 0.7rem;">
Chain predictions (each feeds back as last candle)
</div>
</div>
<!-- Inference Status -->
<div id="inference-status" style="display: none;">
<div class="alert alert-success py-2 px-2 mb-2">
<div class="d-flex align-items-center mb-1">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Running...</span>
</div>
<strong class="small">🔴 LIVE</strong>
</div>
<div class="small">
<div>Signal: <span id="latest-signal" class="fw-bold">--</span></div>
<div>Confidence: <span id="latest-confidence">--</span></div>
<div class="text-muted" style="font-size: 0.7rem;">Predicting <span id="active-steps">1</span> step(s) ahead</div>
</div>
<!-- Last 5 Predictions -->
<div class="mt-2 pt-2 border-top">
<div class="small fw-bold mb-1">Last 5 Predictions:</div>
<div id="prediction-history" class="small" style="font-size: 0.7rem; max-height: 120px; overflow-y: auto;">
<div class="text-muted">No predictions yet...</div>
</div>
</div>
</div>
</div>
<!-- Test Case Stats -->
<div class="small text-muted">
<div class="d-flex justify-content-between">
<span>Test Cases:</span>
<span id="testcase-count">0</span>
</div>
<div class="d-flex justify-content-between">
<span>Last Training:</span>
<span id="last-training-time">Never</span>
</div>
</div>
</div>
</div>
<script>
// Track model states
let modelStates = [];
let selectedModel = null;
let activeTrainingId = null; // Track active training session
function checkActiveTraining() {
/**
* Check if there's an active training session on page load
* This allows resuming progress tracking after page reload
*/
fetch('/api/active-training')
.then(response => response.json())
.then(data => {
if (data.success && data.active && data.session) {
console.log('Active training session found:', data.session);
// Resume tracking
activeTrainingId = data.session.training_id;
showTrainingStatus();
pollTrainingProgress(activeTrainingId);
} else {
console.log('No active training session');
}
})
.catch(error => {
console.error('Error checking active training:', error);
});
}
function loadAvailableModels() {
fetch('/api/available-models')
.then(response => response.json())
.then(data => {
console.log('📊 Available models API response:', JSON.stringify(data, null, 2));
const modelSelect = document.getElementById('model-select');
if (data.success && data.models && Array.isArray(data.models)) {
modelStates = data.models;
modelSelect.innerHTML = '';
// Add placeholder option
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = 'Select a model...';
modelSelect.appendChild(placeholder);
// Add model options with load status and checkpoint info
data.models.forEach((model, index) => {
console.log(` Model ${index}:`, model, 'Type:', typeof model);
// Ensure model is an object with name property
const modelName = (model && typeof model === 'object' && model.name) ? model.name : String(model);
const isLoaded = (model && typeof model === 'object' && 'loaded' in model) ? model.loaded : false;
const checkpoint = (model && typeof model === 'object' && model.checkpoint) ? model.checkpoint : null;
console.log(` → Name: "${modelName}", Loaded: ${isLoaded}`, checkpoint ? `Checkpoint: epoch ${checkpoint.epoch}` : '');
const option = document.createElement('option');
option.value = modelName;
// Build option text with checkpoint info (simplified for safety)
let optionText = modelName;
try {
if (isLoaded) {
optionText += ' ✓';
if (checkpoint && checkpoint.epoch) {
// Show full metrics if available (from loaded model)
if (checkpoint.loss != null && checkpoint.accuracy != null) {
optionText += ` (E${checkpoint.epoch}, L:${checkpoint.loss.toFixed(3)}, A:${(checkpoint.accuracy * 100).toFixed(1)}%)`;
} else {
// Show just epoch if metrics not available (from filename)
optionText += ` (E${checkpoint.epoch})`;
}
}
} else {
optionText += ' (not loaded)';
// Optionally show checkpoint exists
if (checkpoint && checkpoint.epoch) {
optionText += ` [E${checkpoint.epoch}]`;
}
}
} catch (e) {
console.error('Error building option text:', e);
// Fallback to simple text
optionText = modelName + (isLoaded ? ' ✓' : ' (not loaded)');
}
option.textContent = optionText;
option.dataset.loaded = isLoaded;
if (checkpoint) {
option.dataset.checkpoint = JSON.stringify(checkpoint);
}
modelSelect.appendChild(option);
});
console.log(`✓ Models available: ${data.available_count}, loaded: ${data.loaded_count}`);
// Update button state for currently selected model
updateButtonState();
} else {
console.error('❌ Invalid response format:', data);
modelSelect.innerHTML = '<option value="">No models available</option>';
}
})
.catch(error => {
console.error('❌ Error loading models:', error);
const modelSelect = document.getElementById('model-select');
modelSelect.innerHTML = '<option value="">Error loading models</option>';
});
}
function updateButtonState() {
const modelSelect = document.getElementById('model-select');
const trainBtn = document.getElementById('train-model-btn');
const loadBtn = document.getElementById('load-model-btn');
const inferenceBtn = document.getElementById('start-inference-btn');
selectedModel = modelSelect.value;
if (!selectedModel) {
// No model selected
trainBtn.style.display = 'none';
loadBtn.style.display = 'none';
inferenceBtn.disabled = true;
return;
}
// Find model state
const modelState = modelStates.find(m => m.name === selectedModel);
if (modelState && modelState.loaded) {
// Model is loaded - show train/inference buttons
trainBtn.style.display = 'block';
loadBtn.style.display = 'none';
inferenceBtn.disabled = false;
} else {
// Model not loaded - show load button
trainBtn.style.display = 'none';
loadBtn.style.display = 'block';
inferenceBtn.disabled = true;
}
}
// Update button state when model selection changes
document.getElementById('model-select').addEventListener('change', updateButtonState);
// Load models when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadAvailableModels);
} else {
loadAvailableModels();
}
// Load model button handler
document.getElementById('load-model-btn').addEventListener('click', function () {
const modelName = document.getElementById('model-select').value;
if (!modelName) {
showError('Please select a model first');
return;
}
// Disable button and show loading
const loadBtn = this;
loadBtn.disabled = true;
loadBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Loading...';
// Load the model
fetch('/api/load-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_name: modelName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccess(`${modelName} loaded successfully`);
// Refresh model list to update states
loadAvailableModels();
// AUTO-SELECT: Keep the loaded model selected in dropdown
setTimeout(() => {
const modelSelect = document.getElementById('model-select');
modelSelect.value = modelName;
updateButtonState();
}, 100);
} else {
showError(`Failed to load ${modelName}: ${data.error}`);
loadBtn.disabled = false;
loadBtn.innerHTML = '<i class="fas fa-download"></i> Load Model';
}
})
.catch(error => {
showError('Network error: ' + error.message);
loadBtn.disabled = false;
loadBtn.innerHTML = '<i class="fas fa-download"></i> Load Model';
});
});
// Train model button
document.getElementById('train-model-btn').addEventListener('click', function () {
const modelName = document.getElementById('model-select').value;
if (appState.annotations.length === 0) {
showError('No annotations available for training');
return;
}
// Get annotation IDs
const annotationIds = appState.annotations.map(a => a.annotation_id);
// Start training
startTraining(modelName, annotationIds);
});
function showTrainingStatus() {
// Show training status UI
document.getElementById('training-status').style.display = 'block';
document.getElementById('training-results').style.display = 'none';
document.getElementById('train-model-btn').disabled = true;
}
function startTraining(modelName, annotationIds) {
// Show training status
showTrainingStatus();
// Reset progress
document.getElementById('training-progress-bar').style.width = '0%';
document.getElementById('training-epoch').textContent = '0';
document.getElementById('training-loss').textContent = '--';
// Start training request
fetch('/api/train-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_name: modelName,
annotation_ids: annotationIds,
symbol: appState.currentSymbol // CRITICAL: Filter by current symbol
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Store active training ID for persistence across reloads
activeTrainingId = data.training_id;
// Start polling for training progress
pollTrainingProgress(data.training_id);
} else {
showError('Failed to start training: ' + data.error.message);
document.getElementById('training-status').style.display = 'none';
document.getElementById('train-model-btn').disabled = false;
activeTrainingId = null;
}
})
.catch(error => {
showError('Network error: ' + error.message);
document.getElementById('training-status').style.display = 'none';
document.getElementById('train-model-btn').disabled = false;
activeTrainingId = null;
});
}
function pollTrainingProgress(trainingId) {
const pollInterval = setInterval(function () {
fetch('/api/training-progress', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ training_id: trainingId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const progress = data.progress;
// Update progress bar
const percentage = (progress.current_epoch / progress.total_epochs) * 100;
document.getElementById('training-progress-bar').style.width = percentage + '%';
document.getElementById('training-epoch').textContent = progress.current_epoch;
document.getElementById('training-total-epochs').textContent = progress.total_epochs;
document.getElementById('training-loss').textContent = progress.current_loss.toFixed(4);
// Check if complete
if (progress.status === 'completed') {
clearInterval(pollInterval);
activeTrainingId = null; // Clear active training
showTrainingResults(progress);
} else if (progress.status === 'failed') {
clearInterval(pollInterval);
activeTrainingId = null; // Clear active training
showError('Training failed: ' + progress.error);
document.getElementById('training-status').style.display = 'none';
document.getElementById('train-model-btn').disabled = false;
}
}
})
.catch(error => {
clearInterval(pollInterval);
// Don't clear activeTrainingId on network error - training might still be running
showError('Failed to get training progress: ' + error.message);
document.getElementById('training-status').style.display = 'none';
document.getElementById('train-model-btn').disabled = false;
});
}, 1000); // Poll every second
}
function showTrainingResults(results) {
// Hide training status
document.getElementById('training-status').style.display = 'none';
// Show results
document.getElementById('training-results').style.display = 'block';
document.getElementById('result-loss').textContent = results.final_loss.toFixed(4);
document.getElementById('result-accuracy').textContent = (results.accuracy * 100).toFixed(2) + '%';
document.getElementById('result-duration').textContent = results.duration_seconds.toFixed(1) + 's';
// Update last training time
document.getElementById('last-training-time').textContent = new Date().toLocaleTimeString();
// Re-enable train button
document.getElementById('train-model-btn').disabled = false;
showSuccess('Training completed successfully');
}
// Real-time inference controls
let currentInferenceId = null;
let signalPollInterval = null;
let predictionHistory = []; // Store last 5 predictions
// Prediction steps slider handler
document.getElementById('prediction-steps-slider').addEventListener('input', function() {
const steps = this.value;
document.getElementById('prediction-steps-value').textContent = steps;
document.getElementById('active-steps').textContent = steps;
});
document.getElementById('start-inference-btn').addEventListener('click', function () {
const modelName = document.getElementById('model-select').value;
if (!modelName) {
showError('Please select a model first');
return;
}
// Get primary timeframe and prediction steps
const primaryTimeframe = document.getElementById('primary-timeframe-select').value;
const predictionSteps = parseInt(document.getElementById('prediction-steps-slider').value);
// Start real-time inference
fetch('/api/realtime-inference/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_name: modelName,
symbol: appState.currentSymbol,
primary_timeframe: primaryTimeframe,
prediction_steps: predictionSteps
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
currentInferenceId = data.inference_id;
// Update UI
document.getElementById('start-inference-btn').style.display = 'none';
document.getElementById('stop-inference-btn').style.display = 'block';
document.getElementById('inference-status').style.display = 'block';
document.getElementById('inference-controls').style.display = 'block';
// Clear prediction history
predictionHistory = [];
updatePredictionHistory();
// Show live mode banner
const banner = document.getElementById('live-mode-banner');
if (banner) {
banner.style.display = 'block';
}
// Start polling for signals
startSignalPolling();
showSuccess('Real-time inference started - Charts now updating live');
} else {
showError('Failed to start inference: ' + data.error.message);
}
})
.catch(error => {
showError('Network error: ' + error.message);
});
});
document.getElementById('stop-inference-btn').addEventListener('click', function () {
if (!currentInferenceId) return;
// Stop real-time inference
fetch('/api/realtime-inference/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inference_id: currentInferenceId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update UI
document.getElementById('start-inference-btn').style.display = 'block';
document.getElementById('stop-inference-btn').style.display = 'none';
document.getElementById('inference-status').style.display = 'none';
document.getElementById('inference-controls').style.display = 'none';
// Hide live mode banner
const banner = document.getElementById('live-mode-banner');
if (banner) {
banner.style.display = 'none';
}
// Stop polling
stopSignalPolling();
currentInferenceId = null;
showSuccess('Real-time inference stopped');
}
})
.catch(error => {
showError('Network error: ' + error.message);
});
});
// Backtest controls
let currentBacktestId = null;
let backtestPollInterval = null;
let backtestMarkers = []; // Store markers to clear later
document.getElementById('start-backtest-btn').addEventListener('click', function () {
const modelName = document.getElementById('model-select').value;
if (!modelName) {
showError('Please select a model first');
return;
}
// Get current chart state
const primaryTimeframe = document.getElementById('primary-timeframe-select').value;
const symbol = appState.currentSymbol;
// Get visible chart range from the chart (if available)
const chart = document.getElementById('main-chart');
let startTime = null;
let endTime = null;
// Try to get visible range from chart's x-axis
if (chart && chart.layout && chart.layout.xaxis) {
const xaxis = chart.layout.xaxis;
if (xaxis.range) {
startTime = xaxis.range[0];
endTime = xaxis.range[1];
}
}
// Clear previous backtest markers
if (backtestMarkers.length > 0) {
clearBacktestMarkers();
}
// Start backtest
fetch('/api/backtest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_name: modelName,
symbol: symbol,
timeframe: primaryTimeframe,
start_time: startTime,
end_time: endTime
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
currentBacktestId = data.backtest_id;
// Update UI
document.getElementById('start-backtest-btn').style.display = 'none';
document.getElementById('stop-backtest-btn').style.display = 'block';
document.getElementById('backtest-results').style.display = 'block';
// Reset results
document.getElementById('backtest-pnl').textContent = '$0.00';
document.getElementById('backtest-trades').textContent = '0';
document.getElementById('backtest-winrate').textContent = '0%';
document.getElementById('backtest-progress').textContent = '0';
document.getElementById('backtest-total').textContent = data.total_candles || '?';
// Start polling for backtest progress
startBacktestPolling();
showSuccess('Backtest started');
} else {
showError('Failed to start backtest: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
showError('Network error: ' + error.message);
});
});
document.getElementById('stop-backtest-btn').addEventListener('click', function () {
if (!currentBacktestId) return;
// Stop backtest
fetch('/api/backtest/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ backtest_id: currentBacktestId })
})
.then(response => response.json())
.then(data => {
// Update UI
document.getElementById('start-backtest-btn').style.display = 'block';
document.getElementById('stop-backtest-btn').style.display = 'none';
// Stop polling
stopBacktestPolling();
currentBacktestId = null;
showSuccess('Backtest stopped');
})
.catch(error => {
showError('Network error: ' + error.message);
});
});
function startBacktestPolling() {
if (backtestPollInterval) {
clearInterval(backtestPollInterval);
}
backtestPollInterval = setInterval(() => {
if (!currentBacktestId) {
stopBacktestPolling();
return;
}
fetch(`/api/backtest/progress/${currentBacktestId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
updateBacktestUI(data);
// If complete, stop polling
if (data.status === 'complete' || data.status === 'error') {
stopBacktestPolling();
document.getElementById('start-backtest-btn').style.display = 'block';
document.getElementById('stop-backtest-btn').style.display = 'none';
currentBacktestId = null;
if (data.status === 'complete') {
showSuccess('Backtest complete');
} else {
showError('Backtest error: ' + (data.error || 'Unknown'));
}
}
}
})
.catch(error => {
console.error('Backtest polling error:', error);
});
}, 500); // Poll every 500ms for backtest progress
}
function stopBacktestPolling() {
if (backtestPollInterval) {
clearInterval(backtestPollInterval);
backtestPollInterval = null;
}
}
function updateBacktestUI(data) {
// Update progress
document.getElementById('backtest-progress').textContent = data.candles_processed || 0;
document.getElementById('backtest-total').textContent = data.total_candles || 0;
// Update PnL
const pnl = data.pnl || 0;
const pnlElement = document.getElementById('backtest-pnl');
pnlElement.textContent = `$${pnl.toFixed(2)}`;
pnlElement.className = pnl >= 0 ? 'fw-bold text-success' : 'fw-bold text-danger';
// Update trades
document.getElementById('backtest-trades').textContent = data.total_trades || 0;
// Update win rate
const winRate = data.win_rate || 0;
document.getElementById('backtest-winrate').textContent = `${(winRate * 100).toFixed(1)}%`;
// Add new predictions to chart
if (data.new_predictions && data.new_predictions.length > 0) {
addBacktestMarkersToChart(data.new_predictions);
}
}
function addBacktestMarkersToChart(predictions) {
// Store markers for later clearing
predictions.forEach(pred => {
backtestMarkers.push(pred);
});
// Trigger chart update with new markers
if (window.updateBacktestMarkers) {
window.updateBacktestMarkers(backtestMarkers);
}
}
function clearBacktestMarkers() {
backtestMarkers = [];
if (window.clearBacktestMarkers) {
window.clearBacktestMarkers();
}
}
function updatePredictionHistory() {
const historyDiv = document.getElementById('prediction-history');
if (predictionHistory.length === 0) {
historyDiv.innerHTML = '<div class="text-muted">No predictions yet...</div>';
return;
}
// Display last 5 predictions (most recent first)
const html = predictionHistory.slice(0, 5).map(pred => {
const time = new Date(pred.timestamp).toLocaleTimeString();
const actionColor = pred.action === 'BUY' ? 'text-success' :
pred.action === 'SELL' ? 'text-danger' : 'text-secondary';
const confidence = (pred.confidence * 100).toFixed(1);
const price = pred.predicted_price ? pred.predicted_price.toFixed(2) : '--';
return `
<div class="d-flex justify-content-between align-items-center mb-1 pb-1 border-bottom">
<div>
<span class="${actionColor} fw-bold">${pred.action}</span>
<span class="text-muted ms-1">${time}</span>
</div>
<div class="text-end">
<div>${confidence}%</div>
<div class="text-muted" style="font-size: 0.65rem;">$${price}</div>
</div>
</div>
`;
}).join('');
historyDiv.innerHTML = html;
}
function startSignalPolling() {
signalPollInterval = setInterval(function () {
// Poll for signals
fetch('/api/realtime-inference/signals')
.then(response => response.json())
.then(data => {
if (data.success && data.signals.length > 0) {
const latest = data.signals[0];
document.getElementById('latest-signal').textContent = latest.action;
document.getElementById('latest-confidence').textContent =
(latest.confidence * 100).toFixed(1) + '%';
// Add to prediction history (keep last 5)
predictionHistory.unshift({
timestamp: latest.timestamp || new Date().toISOString(),
action: latest.action,
confidence: latest.confidence,
predicted_price: latest.predicted_price
});
if (predictionHistory.length > 5) {
predictionHistory = predictionHistory.slice(0, 5);
}
updatePredictionHistory();
// Update chart with signal markers
if (appState.chartManager) {
displaySignalOnChart(latest);
}
}
})
.catch(error => {
console.error('Error polling signals:', error);
});
// Update charts with latest data
updateChartsWithLiveData();
}, 1000); // Poll every second
}
function updateChartsWithLiveData() {
// Fetch latest chart data
fetch('/api/chart-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: appState.currentSymbol,
timeframes: appState.currentTimeframes,
start_time: null,
end_time: null
})
})
.then(response => response.json())
.then(data => {
if (data.success && appState.chartManager) {
// Update each chart with new data
Object.keys(data.chart_data).forEach(timeframe => {
const chartData = data.chart_data[timeframe];
if (appState.chartManager.charts[timeframe]) {
updateSingleChart(timeframe, chartData);
}
});
}
})
.catch(error => {
console.error('Error updating charts:', error);
});
}
let liveUpdateCount = 0;
function updateSingleChart(timeframe, newData) {
const chart = appState.chartManager.charts[timeframe];
if (!chart) return;
try {
// Update candlestick data
Plotly.update(chart.plotId, {
x: [newData.timestamps],
open: [newData.open],
high: [newData.high],
low: [newData.low],
close: [newData.close]
}, {}, [0]);
// Update volume data
const volumeColors = newData.close.map((close, i) => {
if (i === 0) return '#3b82f6';
return close >= newData.open[i] ? '#10b981' : '#ef4444';
});
Plotly.update(chart.plotId, {
x: [newData.timestamps],
y: [newData.volume],
'marker.color': [volumeColors]
}, {}, [1]);
// Update counter
liveUpdateCount++;
const counterEl = document.getElementById('live-update-count');
if (counterEl) {
counterEl.textContent = liveUpdateCount + ' updates';
}
} catch (error) {
console.error('Error updating chart:', timeframe, error);
}
}
function stopSignalPolling() {
if (signalPollInterval) {
clearInterval(signalPollInterval);
signalPollInterval = null;
}
}
function displaySignalOnChart(signal) {
// Add signal marker to chart
if (!appState.chartManager || !appState.chartManager.charts) return;
// Add marker to all timeframe charts
Object.keys(appState.chartManager.charts).forEach(timeframe => {
const chart = appState.chartManager.charts[timeframe];
if (!chart) return;
// Get current annotations
const currentAnnotations = chart.element.layout.annotations || [];
// Determine marker based on signal
let markerText = '';
let markerColor = '#9ca3af';
if (signal.action === 'BUY') {
markerText = '🔵 BUY';
markerColor = '#10b981';
} else if (signal.action === 'SELL') {
markerText = '🔴 SELL';
markerColor = '#ef4444';
} else {
return; // Don't show HOLD signals
}
// Add new signal marker
const newAnnotation = {
x: signal.timestamp,
y: signal.price,
text: markerText,
showarrow: true,
arrowhead: 2,
ax: 0,
ay: -40,
font: {
size: 12,
color: markerColor
},
bgcolor: '#1f2937',
bordercolor: markerColor,
borderwidth: 2,
borderpad: 4,
opacity: 0.8
};
// Keep only last 10 signal markers
const signalAnnotations = currentAnnotations.filter(ann =>
ann.text && (ann.text.includes('BUY') || ann.text.includes('SELL'))
).slice(-9);
// Combine with existing non-signal annotations
const otherAnnotations = currentAnnotations.filter(ann =>
!ann.text || (!ann.text.includes('BUY') && !ann.text.includes('SELL'))
);
const allAnnotations = [...otherAnnotations, ...signalAnnotations, newAnnotation];
// Update chart
Plotly.relayout(chart.plotId, {
annotations: allAnnotations
});
});
console.log('Signal displayed:', signal.action, '@', signal.price);
}
</script>