1419 lines
63 KiB
HTML
1419 lines
63 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>Annotations: <span id="training-annotation-count" class="fw-bold text-primary">--</span></div>
|
|
<div>Timeframe: <span id="training-timeframe" class="fw-bold text-info">--</span></div>
|
|
<div class="mt-1 pt-1 border-top">
|
|
<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>GPU: <span id="training-gpu-util">--</span>% | CPU: <span id="training-cpu-util">--</span>%</div>
|
|
</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 (No Training)
|
|
</button>
|
|
<button class="btn btn-info btn-sm w-100 mt-1" id="start-inference-pivot-btn">
|
|
<i class="fas fa-chart-line"></i>
|
|
Live Inference + Pivot Training
|
|
</button>
|
|
<button class="btn btn-primary btn-sm w-100 mt-1" id="start-inference-candle-btn">
|
|
<i class="fas fa-graduation-cap"></i>
|
|
Live Inference + Per-Candle Training
|
|
</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 justify-content-between mb-1">
|
|
<div class="d-flex align-items-center">
|
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
|
<span class="visually-hidden">Running...</span>
|
|
</div>
|
|
<strong class="small">🔴 LIVE</strong>
|
|
</div>
|
|
<!-- Model Performance -->
|
|
<div class="small text-end">
|
|
<div style="font-size: 0.65rem;">Acc: <span id="live-accuracy" class="fw-bold text-success">--</span></div>
|
|
<div style="font-size: 0.65rem;">Loss: <span id="live-loss" class="fw-bold text-warning">--</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Position & PnL Status -->
|
|
<div class="mb-2 p-2" style="background-color: rgba(0,0,0,0.1); border-radius: 4px;">
|
|
<div class="small">
|
|
<div class="d-flex justify-content-between">
|
|
<span>Position:</span>
|
|
<span id="position-status" class="fw-bold text-info">NO POSITION</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between" id="floating-pnl-row" style="display: none !important;">
|
|
<span>Floating PnL:</span>
|
|
<span id="floating-pnl" class="fw-bold">--</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between">
|
|
<span>Session PnL:</span>
|
|
<span id="session-pnl" class="fw-bold text-success">+$0.00</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between" style="font-size: 0.7rem; color: #9ca3af;">
|
|
<span>Win Rate:</span>
|
|
<span id="win-rate">0% (0/0)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="small">
|
|
<div>Timeframe: <span id="active-timeframe" class="fw-bold text-primary">--</span></div>
|
|
<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();
|
|
|
|
// Populate annotation count and timeframe if available
|
|
if (data.session.annotation_count) {
|
|
document.getElementById('training-annotation-count').textContent = data.session.annotation_count;
|
|
}
|
|
if (data.session.timeframe) {
|
|
document.getElementById('training-timeframe').textContent = data.session.timeframe.toUpperCase();
|
|
}
|
|
|
|
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}`);
|
|
|
|
// Auto-select Transformer (or any loaded model) if available
|
|
let modelToSelect = null;
|
|
// First try to find Transformer
|
|
const transformerModel = data.models.find(m => {
|
|
const modelName = (m && typeof m === 'object' && m.name) ? m.name : String(m);
|
|
const isLoaded = (m && typeof m === 'object' && 'loaded' in m) ? m.loaded : false;
|
|
return modelName === 'Transformer' && isLoaded;
|
|
});
|
|
|
|
if (transformerModel) {
|
|
modelToSelect = 'Transformer';
|
|
} else {
|
|
// If Transformer not loaded, find any loaded model
|
|
const loadedModel = data.models.find(m => {
|
|
const isLoaded = (m && typeof m === 'object' && 'loaded' in m) ? m.loaded : false;
|
|
return isLoaded;
|
|
});
|
|
if (loadedModel) {
|
|
const modelName = (loadedModel && typeof loadedModel === 'object' && loadedModel.name) ? loadedModel.name : String(loadedModel);
|
|
modelToSelect = modelName;
|
|
}
|
|
}
|
|
|
|
// Auto-select if found
|
|
if (modelToSelect) {
|
|
modelSelect.value = modelToSelect;
|
|
selectedModel = modelToSelect;
|
|
console.log(`✓ Auto-selected loaded model: ${modelToSelect}`);
|
|
}
|
|
|
|
// 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 (Click to retry)</option>';
|
|
|
|
// Allow retry by clicking
|
|
modelSelect.addEventListener('click', function() {
|
|
if (modelSelect.value === "") {
|
|
loadAvailableModels();
|
|
}
|
|
}, { once: true });
|
|
});
|
|
}
|
|
|
|
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();
|
|
|
|
// Get primary timeframe for training
|
|
const primaryTimeframe = document.getElementById('primary-timeframe-select').value;
|
|
|
|
// Reset progress
|
|
document.getElementById('training-progress-bar').style.width = '0%';
|
|
document.getElementById('training-epoch').textContent = '0';
|
|
document.getElementById('training-loss').textContent = '--';
|
|
|
|
// Set annotation count and timeframe
|
|
document.getElementById('training-annotation-count').textContent = annotationIds.length;
|
|
document.getElementById('training-timeframe').textContent = primaryTimeframe.toUpperCase();
|
|
|
|
// 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
|
|
timeframe: primaryTimeframe // Primary timeframe for display
|
|
})
|
|
})
|
|
.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);
|
|
|
|
// Update GPU/CPU utilization
|
|
const gpuUtil = progress.gpu_utilization !== null && progress.gpu_utilization !== undefined
|
|
? progress.gpu_utilization.toFixed(1) : '--';
|
|
const cpuUtil = progress.cpu_utilization !== null && progress.cpu_utilization !== undefined
|
|
? progress.cpu_utilization.toFixed(1) : '--';
|
|
document.getElementById('training-gpu-util').textContent = gpuUtil;
|
|
document.getElementById('training-cpu-util').textContent = cpuUtil;
|
|
|
|
// 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 15 predictions
|
|
|
|
// PnL tracking for simulated trading ($100 position size)
|
|
let pnlTracker = {
|
|
positions: [], // Open positions: {action, entryPrice, entryTime, size}
|
|
closedTrades: [], // Completed trades with PnL
|
|
totalPnL: 0,
|
|
winRate: 0,
|
|
positionSize: 100 // $100 per trade
|
|
};
|
|
|
|
// 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;
|
|
});
|
|
|
|
// Helper function to start inference with different modes
|
|
function startInference(enableLiveTraining, trainEveryCandle) {
|
|
const modelName = document.getElementById('model-select').value;
|
|
|
|
if (!modelName) {
|
|
showError('Please select a model first');
|
|
return;
|
|
}
|
|
|
|
// Get timeframe
|
|
const timeframe = document.getElementById('primary-timeframe-select').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,
|
|
timeframe: timeframe,
|
|
enable_live_training: enableLiveTraining,
|
|
train_every_candle: trainEveryCandle
|
|
})
|
|
})
|
|
.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('start-inference-pivot-btn').style.display = 'none';
|
|
document.getElementById('start-inference-candle-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';
|
|
|
|
// Display active timeframe
|
|
document.getElementById('active-timeframe').textContent = timeframe.toUpperCase();
|
|
|
|
// Clear prediction history and reset PnL tracker
|
|
predictionHistory = [];
|
|
pnlTracker = {
|
|
positions: [],
|
|
closedTrades: [],
|
|
totalPnL: 0,
|
|
winRate: 0,
|
|
positionSize: 100
|
|
};
|
|
updatePredictionHistory();
|
|
updatePnLDisplay();
|
|
|
|
// Show live mode banner
|
|
const banner = document.getElementById('live-mode-banner');
|
|
if (banner) {
|
|
banner.style.display = 'block';
|
|
}
|
|
|
|
// Start polling for signals
|
|
startSignalPolling();
|
|
|
|
// Start chart auto-update
|
|
if (window.appState && window.appState.chartManager) {
|
|
window.appState.chartManager.startAutoUpdate();
|
|
}
|
|
|
|
const trainingMode = data.training_mode || 'inference-only';
|
|
const modeText = trainingMode === 'per-candle' ? ' with per-candle training' :
|
|
(trainingMode === 'pivot-based' ? ' with pivot training' : '');
|
|
showSuccess('Real-time inference started' + modeText);
|
|
} else {
|
|
showError('Failed to start inference: ' + data.error.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showError('Network error: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Button handlers for different inference modes
|
|
document.getElementById('start-inference-btn').addEventListener('click', function () {
|
|
startInference(false, false); // No training
|
|
});
|
|
|
|
document.getElementById('start-inference-pivot-btn').addEventListener('click', function () {
|
|
startInference(true, false); // Pivot-based training
|
|
});
|
|
|
|
document.getElementById('start-inference-candle-btn').addEventListener('click', function () {
|
|
startInference(false, true); // Per-candle training
|
|
});
|
|
|
|
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('start-inference-pivot-btn').style.display = 'block';
|
|
document.getElementById('start-inference-candle-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();
|
|
|
|
// Stop chart auto-update and remove metrics overlay
|
|
if (window.appState && window.appState.chartManager) {
|
|
window.appState.chartManager.stopAutoUpdate();
|
|
window.appState.chartManager.removeLiveMetrics();
|
|
}
|
|
|
|
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 updatePnLTracking(action, currentPrice, timestamp) {
|
|
// Simple trading simulation: BUY opens long, SELL opens short, HOLD closes positions
|
|
if (action === 'BUY' && pnlTracker.positions.length === 0) {
|
|
// Open long position
|
|
pnlTracker.positions.push({
|
|
action: 'BUY',
|
|
entryPrice: currentPrice,
|
|
entryTime: timestamp,
|
|
size: pnlTracker.positionSize
|
|
});
|
|
} else if (action === 'SELL' && pnlTracker.positions.length === 0) {
|
|
// Open short position
|
|
pnlTracker.positions.push({
|
|
action: 'SELL',
|
|
entryPrice: currentPrice,
|
|
entryTime: timestamp,
|
|
size: pnlTracker.positionSize
|
|
});
|
|
} else if (action === 'HOLD' && pnlTracker.positions.length > 0) {
|
|
// Close all positions
|
|
pnlTracker.positions.forEach(pos => {
|
|
let pnl = 0;
|
|
if (pos.action === 'BUY') {
|
|
// Long: profit if price went up
|
|
pnl = (currentPrice - pos.entryPrice) / pos.entryPrice * pos.size;
|
|
} else if (pos.action === 'SELL') {
|
|
// Short: profit if price went down
|
|
pnl = (pos.entryPrice - currentPrice) / pos.entryPrice * pos.size;
|
|
}
|
|
|
|
pnlTracker.closedTrades.push({
|
|
entryPrice: pos.entryPrice,
|
|
exitPrice: currentPrice,
|
|
pnl: pnl,
|
|
entryTime: pos.entryTime,
|
|
exitTime: timestamp
|
|
});
|
|
|
|
pnlTracker.totalPnL += pnl;
|
|
});
|
|
|
|
pnlTracker.positions = [];
|
|
|
|
// Calculate win rate
|
|
const wins = pnlTracker.closedTrades.filter(t => t.pnl > 0).length;
|
|
pnlTracker.winRate = pnlTracker.closedTrades.length > 0
|
|
? (wins / pnlTracker.closedTrades.length * 100)
|
|
: 0;
|
|
}
|
|
|
|
// Update PnL display
|
|
updatePnLDisplay();
|
|
}
|
|
|
|
function updatePnLDisplay() {
|
|
const pnlColor = pnlTracker.totalPnL >= 0 ? 'text-success' : 'text-danger';
|
|
const pnlSign = pnlTracker.totalPnL >= 0 ? '+' : '';
|
|
|
|
// Update PnL metric
|
|
const pnlElement = document.getElementById('metric-pnl');
|
|
if (pnlElement) {
|
|
pnlElement.textContent = `${pnlSign}$${pnlTracker.totalPnL.toFixed(2)}`;
|
|
pnlElement.className = `h4 mb-0 ${pnlColor}`;
|
|
}
|
|
|
|
// Update Win Rate
|
|
const winrateElement = document.getElementById('metric-winrate');
|
|
if (winrateElement) {
|
|
winrateElement.textContent = pnlTracker.closedTrades.length > 0
|
|
? `${pnlTracker.winRate.toFixed(1)}%`
|
|
: '--';
|
|
}
|
|
|
|
// Update Total Trades
|
|
const tradesElement = document.getElementById('metric-trades');
|
|
if (tradesElement) {
|
|
tradesElement.textContent = pnlTracker.closedTrades.length;
|
|
}
|
|
|
|
// Update Open Positions
|
|
const positionsElement = document.getElementById('metric-positions');
|
|
if (positionsElement) {
|
|
positionsElement.textContent = pnlTracker.positions.length;
|
|
}
|
|
|
|
// Update in live banner if exists
|
|
const banner = document.getElementById('inference-status');
|
|
if (banner) {
|
|
let pnlDiv = document.getElementById('live-banner-pnl');
|
|
if (!pnlDiv) {
|
|
const metricsDiv = document.getElementById('live-banner-metrics');
|
|
if (metricsDiv) {
|
|
pnlDiv = document.createElement('span');
|
|
pnlDiv.id = 'live-banner-pnl';
|
|
metricsDiv.appendChild(pnlDiv);
|
|
}
|
|
}
|
|
if (pnlDiv) {
|
|
pnlDiv.innerHTML = `<span class="${pnlColor}">PnL: ${pnlSign}$${pnlTracker.totalPnL.toFixed(2)}</span>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updatePositionStateDisplay(positionState, sessionMetrics) {
|
|
/**
|
|
* Update live trading panel with current position and PnL info
|
|
*/
|
|
try {
|
|
// Update position status
|
|
const positionStatusEl = document.getElementById('position-status');
|
|
const floatingPnlRow = document.getElementById('floating-pnl-row');
|
|
const floatingPnlEl = document.getElementById('floating-pnl');
|
|
|
|
if (positionState.has_position) {
|
|
const posType = positionState.position_type.toUpperCase();
|
|
const entryPrice = positionState.entry_price.toFixed(2);
|
|
positionStatusEl.textContent = `${posType} @ $${entryPrice}`;
|
|
positionStatusEl.className = posType === 'LONG' ? 'fw-bold text-success' : 'fw-bold text-danger';
|
|
|
|
// Show floating PnL
|
|
if (floatingPnlRow) {
|
|
floatingPnlRow.style.display = 'flex !important';
|
|
floatingPnlRow.classList.remove('d-none');
|
|
}
|
|
const unrealizedPnl = positionState.unrealized_pnl || 0;
|
|
const pnlColor = unrealizedPnl >= 0 ? 'text-success' : 'text-danger';
|
|
const pnlSign = unrealizedPnl >= 0 ? '+' : '';
|
|
floatingPnlEl.textContent = `${pnlSign}${unrealizedPnl.toFixed(2)}%`;
|
|
floatingPnlEl.className = `fw-bold ${pnlColor}`;
|
|
} else {
|
|
positionStatusEl.textContent = 'NO POSITION';
|
|
positionStatusEl.className = 'fw-bold text-secondary';
|
|
|
|
// Hide floating PnL row
|
|
if (floatingPnlRow) {
|
|
floatingPnlRow.style.display = 'none !important';
|
|
floatingPnlRow.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
// Update session PnL
|
|
const sessionPnlEl = document.getElementById('session-pnl');
|
|
if (sessionPnlEl && sessionMetrics) {
|
|
const totalPnl = sessionMetrics.total_pnl || 0;
|
|
const pnlColor = totalPnl >= 0 ? 'text-success' : 'text-danger';
|
|
const pnlSign = totalPnl >= 0 ? '+' : '';
|
|
sessionPnlEl.textContent = `${pnlSign}$${totalPnl.toFixed(2)}`;
|
|
sessionPnlEl.className = `fw-bold ${pnlColor}`;
|
|
|
|
// Update win rate
|
|
const winRateEl = document.getElementById('win-rate');
|
|
if (winRateEl) {
|
|
const winRate = sessionMetrics.win_rate || 0;
|
|
const winCount = sessionMetrics.win_count || 0;
|
|
const totalTrades = sessionMetrics.total_trades || 0;
|
|
winRateEl.textContent = `${winRate.toFixed(1)}% (${winCount}/${totalTrades})`;
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error updating position state display:', error);
|
|
}
|
|
}
|
|
|
|
// Make function globally accessible for WebSocket handler
|
|
window.updatePositionStateDisplay = updatePositionStateDisplay;
|
|
|
|
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 15 predictions (most recent first)
|
|
const html = predictionHistory.slice(0, 15).map(pred => {
|
|
// Safely parse timestamp
|
|
let timeStr = '--:--:--';
|
|
try {
|
|
if (pred.timestamp) {
|
|
const date = new Date(pred.timestamp);
|
|
if (!isNaN(date.getTime())) {
|
|
timeStr = date.toLocaleTimeString();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing timestamp:', e);
|
|
}
|
|
|
|
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 && !isNaN(pred.predicted_price)) ? pred.predicted_price.toFixed(2) : '--';
|
|
const timeframe = pred.timeframe || '1m';
|
|
|
|
return `
|
|
<div class="d-flex justify-content-between align-items-center mb-1 pb-1 border-bottom">
|
|
<div>
|
|
<span class="badge bg-dark text-light me-1" style="font-size: 0.6rem;">${timeframe}</span>
|
|
<span class="${actionColor} fw-bold">${pred.action}</span>
|
|
<span class="text-muted ms-1" style="font-size: 0.75rem;">${timeStr}</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) {
|
|
// Update Accuracy Metrics in Header
|
|
if (data.metrics) {
|
|
document.getElementById('metric-accuracy').textContent = (data.metrics.accuracy * 100).toFixed(1) + '%';
|
|
// If you want to show these in the live banner too:
|
|
const banner = document.getElementById('inference-status');
|
|
if (banner) {
|
|
// Check if we already have a metrics div, if not create one
|
|
let metricsDiv = document.getElementById('live-banner-metrics');
|
|
if (!metricsDiv) {
|
|
metricsDiv = document.createElement('div');
|
|
metricsDiv.id = 'live-banner-metrics';
|
|
metricsDiv.className = 'mt-1 pt-1 border-top small text-muted d-flex justify-content-between';
|
|
banner.querySelector('.small').appendChild(metricsDiv);
|
|
}
|
|
metricsDiv.innerHTML = `
|
|
<span>Acc: ${(data.metrics.accuracy * 100).toFixed(1)}%</span>
|
|
<span>Loss: ${data.metrics.loss ? data.metrics.loss.toFixed(4) : '--'}</span>
|
|
`;
|
|
}
|
|
}
|
|
|
|
const latest = data.signals[0];
|
|
console.log('[Signal Polling] Latest signal:', latest);
|
|
console.log('[Signal Polling] predicted_candle:', latest.predicted_candle);
|
|
|
|
document.getElementById('latest-signal').textContent = latest.action;
|
|
document.getElementById('latest-confidence').textContent =
|
|
(latest.confidence * 100).toFixed(1) + '%';
|
|
|
|
// Handle prediction price and timestamp safely
|
|
const predPrice = (latest.predicted_price && !isNaN(latest.predicted_price))
|
|
? latest.predicted_price.toFixed(2)
|
|
: '--';
|
|
|
|
// Format timestamp carefully
|
|
let timestamp = new Date().toISOString();
|
|
if (latest.timestamp) {
|
|
timestamp = latest.timestamp;
|
|
}
|
|
|
|
// Get current price from signal (backend uses 'price' field)
|
|
const currentPrice = latest.price || latest.current_price;
|
|
|
|
// Add to prediction history (keep last 15)
|
|
const newPrediction = {
|
|
timestamp: timestamp,
|
|
action: latest.action,
|
|
confidence: latest.confidence,
|
|
predicted_price: latest.predicted_price,
|
|
current_price: currentPrice,
|
|
timeframe: appState.currentTimeframes ? appState.currentTimeframes[0] : '1m'
|
|
};
|
|
|
|
// Strengthen filter: only add valid signals
|
|
const validActions = ['BUY', 'SELL', 'HOLD'];
|
|
if (latest.action &&
|
|
validActions.includes(latest.action) &&
|
|
!isNaN(latest.confidence) &&
|
|
latest.confidence > 0 &&
|
|
currentPrice &&
|
|
!isNaN(currentPrice)) {
|
|
|
|
// Update PnL tracking
|
|
updatePnLTracking(latest.action, currentPrice, timestamp);
|
|
|
|
predictionHistory.unshift(newPrediction);
|
|
if (predictionHistory.length > 15) {
|
|
predictionHistory = predictionHistory.slice(0, 15);
|
|
}
|
|
updatePredictionHistory();
|
|
} else {
|
|
console.warn('Signal filtered out:', {
|
|
action: latest.action,
|
|
confidence: latest.confidence,
|
|
price: currentPrice,
|
|
reason: !latest.action ? 'no action' :
|
|
!validActions.includes(latest.action) ? 'invalid action' :
|
|
isNaN(latest.confidence) ? 'NaN confidence' :
|
|
latest.confidence <= 0 ? 'zero confidence' :
|
|
!currentPrice ? 'no price' :
|
|
isNaN(currentPrice) ? 'NaN price' : 'unknown'
|
|
});
|
|
}
|
|
|
|
// Update chart with signal markers and predictions
|
|
if (window.appState && window.appState.chartManager) {
|
|
displaySignalOnChart(latest);
|
|
|
|
// Update ghost candles and other predictions
|
|
const predictions = {};
|
|
const modelKey = latest.model ? latest.model.toLowerCase() : 'transformer';
|
|
predictions[modelKey] = latest;
|
|
|
|
window.appState.chartManager.updatePredictions(predictions);
|
|
|
|
// Display live metrics on the active chart
|
|
if (data.metrics) {
|
|
window.appState.chartManager.updateLiveMetrics(data.metrics);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error polling signals:', error);
|
|
});
|
|
}, 1000); // Poll every second
|
|
}
|
|
|
|
// REMOVED: updateChartsWithLiveData() - Full chart refresh is deprecated in favor of incremental updates
|
|
|
|
/* DEPRECATED: Old update logic
|
|
// 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);
|
|
});
|
|
}
|
|
*/
|
|
|
|
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++;
|
|
// Note: The element ID is 'live-updates-count' in the header, not 'live-update-count'
|
|
const counterEl = document.getElementById('live-updates-count') || 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> |