525 lines
21 KiB
HTML
525 lines
21 KiB
HTML
{% extends "base_layout.html" %}
|
|
|
|
{% block title %}Trade Annotation Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Live Mode Banner -->
|
|
<div id="live-mode-banner" class="alert alert-success mb-0" style="display: none; border-radius: 0;">
|
|
<div class="container-fluid">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<span class="badge bg-danger me-2">🔴 LIVE</span>
|
|
<strong>Real-Time Inference Active</strong>
|
|
<span class="ms-3 small">Charts updating with live data every second</span>
|
|
</div>
|
|
<div>
|
|
<span class="badge bg-light text-dark" id="live-update-count">0 updates</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-3">
|
|
<!-- Left Sidebar - Controls -->
|
|
<div class="col-md-2">
|
|
{% include 'components/control_panel.html' %}
|
|
</div>
|
|
|
|
<!-- Main Chart Area -->
|
|
<div class="col-md-8">
|
|
{% include 'components/chart_panel.html' %}
|
|
</div>
|
|
|
|
<!-- Right Sidebar - Annotations & Training -->
|
|
<div class="col-md-2">
|
|
{% include 'components/annotation_list.html' %}
|
|
{% include 'components/training_panel.html' %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inference Simulation Modal -->
|
|
<div class="modal fade" id="inferenceModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-brain"></i>
|
|
Inference Simulation
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{% include 'components/inference_panel.html' %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Initialize application state
|
|
window.appState = {
|
|
currentSymbol: '{{ current_symbol }}',
|
|
currentTimeframes: {{ timeframes | tojson }},
|
|
|
|
// IMPORTANT!!! DO NOT CHANGE {{ annotations | tojson }} to { { annotations | tojson } }
|
|
annotations: {{ annotations | tojson }},
|
|
pendingAnnotation: null,
|
|
chartManager: null,
|
|
annotationManager: null,
|
|
timeNavigator: null,
|
|
trainingController: null
|
|
};
|
|
|
|
// Initialize components when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Initialize chart manager
|
|
window.appState.chartManager = new ChartManager('chart-container', window.appState.currentTimeframes);
|
|
|
|
// Initialize annotation manager
|
|
window.appState.annotationManager = new AnnotationManager(window.appState.chartManager);
|
|
|
|
// Initialize time navigator
|
|
window.appState.timeNavigator = new TimeNavigator(window.appState.chartManager);
|
|
|
|
// Initialize training controller
|
|
window.appState.trainingController = new TrainingController();
|
|
|
|
// Setup global functions FIRST (before loading data)
|
|
setupGlobalFunctions();
|
|
|
|
// Load initial data (may call renderAnnotationsList which needs deleteAnnotation)
|
|
loadInitialData();
|
|
|
|
// Setup keyboard shortcuts
|
|
setupKeyboardShortcuts();
|
|
});
|
|
|
|
function loadInitialData() {
|
|
console.log('Loading initial chart data...');
|
|
|
|
// Fetch initial chart data with 2500 candles for training
|
|
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,
|
|
limit: 2500 // Load 2500 candles initially for training
|
|
})
|
|
})
|
|
.then(response => {
|
|
console.log('Chart data response status:', response.status);
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Chart data received:', data);
|
|
|
|
if (data.success) {
|
|
console.log('Initializing charts with data...');
|
|
window.appState.chartManager.initializeCharts(data.chart_data, data.pivot_bounds);
|
|
|
|
// Show pivot bounds info if available
|
|
if (data.pivot_bounds) {
|
|
const pivotInfo = data.pivot_bounds;
|
|
console.log(`Loaded ${pivotInfo.total_levels} pivot levels (${pivotInfo.support_levels.length} support, ${pivotInfo.resistance_levels.length} resistance) from ${pivotInfo.timeframe} data over ${pivotInfo.period}`);
|
|
}
|
|
|
|
// Load existing annotations
|
|
console.log('Loading', window.appState.annotations.length, 'existing annotations');
|
|
window.appState.annotations.forEach(annotation => {
|
|
window.appState.chartManager.addAnnotation(annotation);
|
|
});
|
|
|
|
// Update annotation list
|
|
if (typeof renderAnnotationsList === 'function') {
|
|
renderAnnotationsList(window.appState.annotations);
|
|
}
|
|
|
|
// DISABLED: Live updates were causing data corruption (red wall issue)
|
|
// Use manual refresh button instead
|
|
// startLiveChartUpdates();
|
|
|
|
console.log('Initial data load complete');
|
|
} else {
|
|
console.error('Chart data load failed:', data.error);
|
|
showError('Failed to load chart data: ' + data.error.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Chart data fetch error:', error);
|
|
showError('Network error: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Live chart update mechanism
|
|
let liveUpdateInterval = null;
|
|
|
|
function startLiveChartUpdates() {
|
|
// Clear any existing interval
|
|
if (liveUpdateInterval) {
|
|
clearInterval(liveUpdateInterval);
|
|
}
|
|
|
|
console.log('Starting live chart updates (1s interval)');
|
|
|
|
// Update every second for 1s chart
|
|
liveUpdateInterval = setInterval(() => {
|
|
updateLiveChartData();
|
|
}, 1000);
|
|
}
|
|
|
|
function updateLiveChartData() {
|
|
// Only update if we have a chart manager
|
|
if (!window.appState || !window.appState.chartManager) {
|
|
return;
|
|
}
|
|
|
|
// Fetch latest 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 && window.appState.chartManager) {
|
|
// Update charts with new data and pivot bounds
|
|
window.appState.chartManager.updateCharts(data.chart_data, data.pivot_bounds);
|
|
|
|
// Show pivot bounds info if available
|
|
if (data.pivot_bounds) {
|
|
const pivotInfo = data.pivot_bounds;
|
|
console.log(`Loaded ${pivotInfo.total_levels} pivot levels (${pivotInfo.support_levels.length} support, ${pivotInfo.resistance_levels.length} resistance) from ${pivotInfo.timeframe} data over ${pivotInfo.period}`);
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.debug('Live update error:', error);
|
|
// Don't show error to user for live updates
|
|
});
|
|
}
|
|
|
|
// Clean up on page unload
|
|
window.addEventListener('beforeunload', function () {
|
|
if (liveUpdateInterval) {
|
|
clearInterval(liveUpdateInterval);
|
|
}
|
|
});
|
|
|
|
function setupKeyboardShortcuts() {
|
|
document.addEventListener('keydown', function (e) {
|
|
// Arrow left - navigate backward
|
|
if (e.key === 'ArrowLeft') {
|
|
e.preventDefault();
|
|
if (window.appState.timeNavigator) {
|
|
window.appState.timeNavigator.scrollBackward();
|
|
}
|
|
}
|
|
// Arrow right - navigate forward
|
|
else if (e.key === 'ArrowRight') {
|
|
e.preventDefault();
|
|
if (window.appState.timeNavigator) {
|
|
window.appState.timeNavigator.scrollForward();
|
|
}
|
|
}
|
|
// Space - mark point (if chart is focused)
|
|
else if (e.key === ' ' && e.target.tagName !== 'INPUT') {
|
|
e.preventDefault();
|
|
// Trigger mark at current crosshair position
|
|
if (window.appState.annotationManager) {
|
|
window.appState.annotationManager.markCurrentPosition();
|
|
}
|
|
}
|
|
// Escape - cancel pending annotation
|
|
else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
if (window.appState.annotationManager) {
|
|
window.appState.annotationManager.pendingAnnotation = null;
|
|
document.getElementById('pending-annotation-status').style.display = 'none';
|
|
showSuccess('Annotation cancelled');
|
|
}
|
|
}
|
|
// Enter - complete annotation (if pending)
|
|
else if (e.key === 'Enter' && e.target.tagName !== 'INPUT') {
|
|
e.preventDefault();
|
|
if (window.appState.annotationManager && window.appState.annotationManager.pendingAnnotation) {
|
|
showSuccess('Click on chart to mark exit point');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
// Create toast notification
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast align-items-center text-white bg-danger border-0';
|
|
toast.setAttribute('role', 'alert');
|
|
toast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
${message}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
// Add to page and show
|
|
document.body.appendChild(toast);
|
|
const bsToast = new bootstrap.Toast(toast);
|
|
bsToast.show();
|
|
|
|
// Remove after hidden
|
|
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast align-items-center text-white bg-success border-0';
|
|
toast.setAttribute('role', 'alert');
|
|
toast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="fas fa-check-circle"></i>
|
|
${message}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
const bsToast = new bootstrap.Toast(toast);
|
|
bsToast.show();
|
|
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
|
}
|
|
|
|
function showWarning(message) {
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast align-items-center text-white bg-warning border-0';
|
|
toast.setAttribute('role', 'alert');
|
|
toast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
${message}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
const bsToast = new bootstrap.Toast(toast);
|
|
bsToast.show();
|
|
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
|
}
|
|
|
|
function deleteAnnotation(annotationId) {
|
|
console.log('=== deleteAnnotation called ===');
|
|
console.log('Annotation ID:', annotationId);
|
|
console.log('window.appState:', window.appState);
|
|
console.log('window.appState.annotations:', window.appState?.annotations);
|
|
|
|
if (!annotationId) {
|
|
console.error('No annotation ID provided');
|
|
showError('No annotation ID provided');
|
|
return;
|
|
}
|
|
|
|
if (!window.appState || !window.appState.annotations) {
|
|
console.error('appState not initialized');
|
|
showError('Application state not initialized. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
// Check if annotation exists
|
|
const annotation = window.appState.annotations.find(a => a.annotation_id === annotationId);
|
|
if (!annotation) {
|
|
console.error('Annotation not found in appState:', annotationId);
|
|
showError('Annotation not found');
|
|
return;
|
|
}
|
|
|
|
console.log('Found annotation:', annotation);
|
|
console.log('Current annotations count:', window.appState.annotations.length);
|
|
|
|
if (!confirm('Delete this annotation?')) {
|
|
console.log('Delete cancelled by user');
|
|
return;
|
|
}
|
|
|
|
console.log('Sending delete request to API...');
|
|
fetch('/api/delete-annotation', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ annotation_id: annotationId })
|
|
})
|
|
.then(response => {
|
|
console.log('Delete response status:', response.status);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Delete response data:', data);
|
|
|
|
if (data.success) {
|
|
console.log('Delete successful, updating UI...');
|
|
|
|
// Remove from app state
|
|
const originalCount = window.appState.annotations.length;
|
|
window.appState.annotations = window.appState.annotations.filter(
|
|
a => a.annotation_id !== annotationId
|
|
);
|
|
console.log(`Removed from appState: ${originalCount} -> ${window.appState.annotations.length}`);
|
|
|
|
// Update UI
|
|
if (typeof renderAnnotationsList === 'function') {
|
|
renderAnnotationsList(window.appState.annotations);
|
|
console.log('UI updated with renderAnnotationsList');
|
|
} else {
|
|
console.error('renderAnnotationsList function not found');
|
|
// Try to reload the page as fallback
|
|
location.reload();
|
|
return;
|
|
}
|
|
|
|
// Remove from chart
|
|
if (window.appState && window.appState.chartManager) {
|
|
window.appState.chartManager.removeAnnotation(annotationId);
|
|
console.log('Removed from chart');
|
|
} else {
|
|
console.warn('Chart manager not available');
|
|
}
|
|
|
|
showSuccess('Annotation deleted successfully');
|
|
console.log('=== deleteAnnotation completed successfully ===');
|
|
} else {
|
|
console.error('Delete failed:', data.error);
|
|
showError('Failed to delete annotation: ' + (data.error ? data.error.message : 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Delete error:', error);
|
|
showError('Network error: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function highlightAnnotation(annotationId) {
|
|
if (window.appState && window.appState.chartManager) {
|
|
window.appState.chartManager.highlightAnnotation(annotationId);
|
|
}
|
|
}
|
|
|
|
function setupGlobalFunctions() {
|
|
console.log('=== setupGlobalFunctions called ===');
|
|
console.log('deleteAnnotation function exists:', typeof deleteAnnotation);
|
|
console.log('highlightAnnotation function exists:', typeof highlightAnnotation);
|
|
console.log('renderAnnotationsList function exists:', typeof renderAnnotationsList);
|
|
console.log('showError function exists:', typeof showError);
|
|
console.log('showSuccess function exists:', typeof showSuccess);
|
|
console.log('showWarning function exists:', typeof showWarning);
|
|
|
|
// Make functions globally available
|
|
window.showError = showError;
|
|
window.showSuccess = showSuccess;
|
|
window.showWarning = showWarning;
|
|
window.renderAnnotationsList = renderAnnotationsList;
|
|
window.deleteAnnotation = deleteAnnotation;
|
|
window.highlightAnnotation = highlightAnnotation;
|
|
|
|
// Verify functions are set
|
|
console.log('Global functions setup complete:');
|
|
console.log(' - window.deleteAnnotation:', typeof window.deleteAnnotation);
|
|
console.log(' - window.renderAnnotationsList:', typeof window.renderAnnotationsList);
|
|
console.log(' - window.showError:', typeof window.showError);
|
|
console.log(' - window.showSuccess:', typeof window.showSuccess);
|
|
console.log(' - window.showWarning:', typeof window.showWarning);
|
|
console.log(' - window.highlightAnnotation:', typeof window.highlightAnnotation);
|
|
|
|
// Test call
|
|
console.log('Testing window.deleteAnnotation availability...');
|
|
if (typeof window.deleteAnnotation === 'function') {
|
|
console.log('✓ window.deleteAnnotation is ready');
|
|
} else {
|
|
console.error('✗ window.deleteAnnotation is NOT a function!');
|
|
}
|
|
}
|
|
|
|
function renderAnnotationsList(annotations) {
|
|
const listElement = document.getElementById('annotations-list');
|
|
if (!listElement) return;
|
|
|
|
listElement.innerHTML = '';
|
|
|
|
annotations.forEach(annotation => {
|
|
const item = document.createElement('div');
|
|
item.className = 'annotation-item mb-2 p-2 border rounded';
|
|
item.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<small class="text-muted">${annotation.timeframe}</small>
|
|
<div class="fw-bold ${annotation.profit_loss_pct >= 0 ? 'text-success' : 'text-danger'}">
|
|
${annotation.direction} ${annotation.profit_loss_pct >= 0 ? '+' : ''}${annotation.profit_loss_pct.toFixed(2)}%
|
|
</div>
|
|
<small class="text-muted">
|
|
${new Date(annotation.entry.timestamp).toLocaleString()}
|
|
</small>
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary btn-sm highlight-btn" title="Highlight">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger btn-sm delete-btn" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
item.querySelector('.highlight-btn').addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
console.log('Highlight button clicked for:', annotation.annotation_id);
|
|
if (typeof window.highlightAnnotation === 'function') {
|
|
window.highlightAnnotation(annotation.annotation_id);
|
|
}
|
|
});
|
|
|
|
item.querySelector('.delete-btn').addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
console.log('=== Delete button clicked ===');
|
|
console.log('Annotation ID:', annotation.annotation_id);
|
|
console.log('window.deleteAnnotation type:', typeof window.deleteAnnotation);
|
|
console.log('window object keys containing delete:', Object.keys(window).filter(k => k.includes('delete')));
|
|
|
|
if (typeof window.deleteAnnotation === 'function') {
|
|
console.log('Calling window.deleteAnnotation...');
|
|
try {
|
|
window.deleteAnnotation(annotation.annotation_id);
|
|
} catch (error) {
|
|
console.error('Error calling deleteAnnotation:', error);
|
|
showError('Error calling delete function: ' + error.message);
|
|
}
|
|
} else {
|
|
console.error('window.deleteAnnotation is not a function:', typeof window.deleteAnnotation);
|
|
console.log('Available window functions:', Object.keys(window).filter(k => typeof window[k] === 'function'));
|
|
showError('Delete function not available. Please refresh the page.');
|
|
}
|
|
});
|
|
|
|
listElement.appendChild(item);
|
|
});
|
|
}
|
|
|
|
</script>
|
|
{% endblock %} |