min pivot distance only applies to L1 pivots ,
charts minimize addons, delete buttons WIP
This commit is contained in:
@@ -68,10 +68,33 @@
|
||||
"entry_state": {},
|
||||
"exit_state": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"annotation_id": "a16f92c0-7228-4293-a188-baf74ca9b284",
|
||||
"symbol": "ETH/USDT",
|
||||
"timeframe": "1m",
|
||||
"entry": {
|
||||
"timestamp": "2025-10-24 08:31",
|
||||
"price": 3930.54,
|
||||
"index": 101
|
||||
},
|
||||
"exit": {
|
||||
"timestamp": "2025-10-24 08:46",
|
||||
"price": 3965,
|
||||
"index": 104
|
||||
},
|
||||
"direction": "LONG",
|
||||
"profit_loss_pct": 0.8767243177782196,
|
||||
"notes": "",
|
||||
"created_at": "2025-10-24T12:56:59.390345",
|
||||
"market_context": {
|
||||
"entry_state": {},
|
||||
"exit_state": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"total_annotations": 3,
|
||||
"last_updated": "2025-10-23T18:36:05.809750"
|
||||
"total_annotations": 4,
|
||||
"last_updated": "2025-10-24T12:56:59.390345"
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,12 @@
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#chart-container.no-scroll {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.timeframe-chart {
|
||||
@@ -28,6 +34,17 @@
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-tertiary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.timeframe-chart.minimized {
|
||||
margin-bottom: 0.25rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.timeframe-chart.minimized .chart-header {
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
@@ -39,6 +56,25 @@
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chart-header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.minimize-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.minimize-btn:hover {
|
||||
background-color: var(--accent-primary);
|
||||
border-color: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.timeframe-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
|
||||
@@ -93,34 +93,52 @@ class AnnotationManager {
|
||||
* Delete old annotation and save updated one
|
||||
*/
|
||||
deleteAndSaveAnnotation(oldId, newAnnotation) {
|
||||
console.log('Updating annotation:', oldId, newAnnotation);
|
||||
|
||||
// Delete old
|
||||
fetch('/api/delete-annotation', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({annotation_id: oldId})
|
||||
})
|
||||
.then(() => {
|
||||
// Save new
|
||||
return fetch('/api/save-annotation', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(newAnnotation)
|
||||
});
|
||||
.then(response => response.json())
|
||||
.then(deleteData => {
|
||||
console.log('Delete response:', deleteData);
|
||||
if (deleteData.success) {
|
||||
// Save new
|
||||
return fetch('/api/save-annotation', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(newAnnotation)
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to delete old annotation: ' + deleteData.error.message);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Save response:', data);
|
||||
if (data.success) {
|
||||
// Update app state
|
||||
window.appState.annotations = window.appState.annotations.filter(a => a.annotation_id !== oldId);
|
||||
window.appState.annotations.push(data.annotation);
|
||||
|
||||
// Update UI
|
||||
window.renderAnnotationsList(window.appState.annotations);
|
||||
if (typeof window.renderAnnotationsList === 'function') {
|
||||
window.renderAnnotationsList(window.appState.annotations);
|
||||
}
|
||||
|
||||
// Update chart
|
||||
this.chartManager.removeAnnotation(oldId);
|
||||
this.chartManager.addAnnotation(data.annotation);
|
||||
|
||||
window.showSuccess('Annotation updated successfully');
|
||||
} else {
|
||||
throw new Error('Failed to save updated annotation: ' + data.error.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Update annotation error:', error);
|
||||
window.showError('Failed to update annotation: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ class ChartManager {
|
||||
pivotDots.y.push(pivot.price);
|
||||
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
|
||||
pivotDots.marker.color.push(color);
|
||||
pivotDots.marker.size.push(8);
|
||||
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
|
||||
pivotDots.marker.symbol.push('triangle-down');
|
||||
|
||||
// Draw horizontal line ONLY for last pivot of this level
|
||||
@@ -217,7 +217,7 @@ class ChartManager {
|
||||
pivotDots.y.push(pivot.price);
|
||||
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
|
||||
pivotDots.marker.color.push(color);
|
||||
pivotDots.marker.size.push(8);
|
||||
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
|
||||
pivotDots.marker.symbol.push('triangle-up');
|
||||
|
||||
// Draw horizontal line ONLY for last pivot of this level
|
||||
@@ -285,24 +285,42 @@ class ChartManager {
|
||||
annotations: []
|
||||
};
|
||||
|
||||
// Add click handler for chart
|
||||
// Add click handler for chart and annotations
|
||||
plotElement.on('plotly_click', (eventData) => {
|
||||
// Check if this is an annotation click by looking at the clicked element
|
||||
if (eventData.points && eventData.points.length > 0) {
|
||||
const point = eventData.points[0];
|
||||
// If it's a shape click (annotation line), try to find the annotation
|
||||
if (point.data && point.data.name && point.data.name.startsWith('line_')) {
|
||||
const annotationId = point.data.name.replace('line_', '');
|
||||
console.log('Line annotation clicked:', annotationId);
|
||||
this.handleAnnotationClick(annotationId, 'edit');
|
||||
return; // Don't process as regular chart click
|
||||
}
|
||||
}
|
||||
|
||||
// Regular chart click for annotation marking
|
||||
this.handleChartClick(timeframe, eventData);
|
||||
});
|
||||
|
||||
// Add click handler for annotations
|
||||
plotElement.on('plotly_clickannotation', (eventData) => {
|
||||
console.log('Annotation clicked:', eventData);
|
||||
const annotationName = eventData.annotation.name;
|
||||
if (annotationName) {
|
||||
const parts = annotationName.split('_');
|
||||
const action = parts[0]; // 'entry', 'exit', or 'delete'
|
||||
const annotationId = parts[1];
|
||||
|
||||
console.log(`Annotation action: ${action}, ID: ${annotationId}`);
|
||||
|
||||
if (action === 'delete') {
|
||||
this.handleAnnotationClick(annotationId, 'delete');
|
||||
} else {
|
||||
this.handleAnnotationClick(annotationId, 'edit');
|
||||
}
|
||||
} else {
|
||||
console.log('No annotation name found in click event');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -418,7 +436,7 @@ class ChartManager {
|
||||
pivotDots.y.push(pivot.price);
|
||||
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
|
||||
pivotDots.marker.color.push(color);
|
||||
pivotDots.marker.size.push(8);
|
||||
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
|
||||
pivotDots.marker.symbol.push('triangle-down');
|
||||
|
||||
// Draw horizontal line ONLY for last pivot
|
||||
@@ -464,7 +482,7 @@ class ChartManager {
|
||||
pivotDots.y.push(pivot.price);
|
||||
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
|
||||
pivotDots.marker.color.push(color);
|
||||
pivotDots.marker.size.push(8);
|
||||
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
|
||||
pivotDots.marker.symbol.push('triangle-up');
|
||||
|
||||
// Draw horizontal line ONLY for last pivot
|
||||
@@ -644,13 +662,32 @@ class ChartManager {
|
||||
});
|
||||
});
|
||||
|
||||
// Update chart layout with annotations
|
||||
// Get existing pivot annotations (they have specific names like L1H, L2L)
|
||||
const existingLayout = chart.element.layout || {};
|
||||
const existingAnnotations = existingLayout.annotations || [];
|
||||
const pivotAnnotations = existingAnnotations.filter(ann =>
|
||||
ann.text && ann.text.match(/^L\d+[HL]$/)
|
||||
);
|
||||
|
||||
// Merge pivot annotations with trade annotations
|
||||
const allAnnotations = [...pivotAnnotations, ...plotlyAnnotations];
|
||||
|
||||
// Get existing pivot shapes
|
||||
const existingShapes = existingLayout.shapes || [];
|
||||
const pivotShapes = existingShapes.filter(shape =>
|
||||
shape.layer === 'below' && shape.line && shape.line.dash === 'dash'
|
||||
);
|
||||
|
||||
// Merge pivot shapes with trade annotation shapes
|
||||
const allShapes = [...pivotShapes, ...plotlyShapes];
|
||||
|
||||
// Update chart layout with merged annotations and shapes
|
||||
Plotly.relayout(chart.plotId, {
|
||||
annotations: plotlyAnnotations,
|
||||
shapes: plotlyShapes
|
||||
annotations: allAnnotations,
|
||||
shapes: allShapes
|
||||
});
|
||||
|
||||
console.log(`Updated ${timeframeAnnotations.length} annotations for ${timeframe}`);
|
||||
console.log(`Updated ${timeframeAnnotations.length} trade annotations for ${timeframe} (preserved ${pivotAnnotations.length} pivot annotations)`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -713,83 +750,217 @@ class ChartManager {
|
||||
const annotation = this.annotations[annotationId];
|
||||
if (!annotation) return;
|
||||
|
||||
// Show edit dialog
|
||||
const action = prompt(
|
||||
'Edit annotation:\n' +
|
||||
'1 - Move entry point\n' +
|
||||
'2 - Move exit point\n' +
|
||||
'3 - Delete annotation\n' +
|
||||
'Enter choice (1-3):',
|
||||
'1'
|
||||
);
|
||||
// Create a better edit dialog using Bootstrap modal
|
||||
this.showEditDialog(annotationId, annotation);
|
||||
}
|
||||
|
||||
if (action === '1') {
|
||||
// Move entry point
|
||||
window.showSuccess('Click on chart to set new entry point');
|
||||
/**
|
||||
* Show edit dialog for annotation
|
||||
*/
|
||||
showEditDialog(annotationId, annotation) {
|
||||
// Create modal HTML
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="editAnnotationModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Annotation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<strong>Current Annotation:</strong>
|
||||
<div class="mt-2">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Entry:</small><br>
|
||||
<span class="badge bg-success">▲</span>
|
||||
${new Date(annotation.entry.timestamp).toLocaleString()}<br>
|
||||
<small>$${annotation.entry.price.toFixed(2)}</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Exit:</small><br>
|
||||
<span class="badge bg-danger">▼</span>
|
||||
${new Date(annotation.exit.timestamp).toLocaleString()}<br>
|
||||
<small>$${annotation.exit.price.toFixed(2)}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">P&L:</small>
|
||||
<span class="badge ${annotation.profit_loss_pct >= 0 ? 'bg-success' : 'bg-danger'}">
|
||||
${annotation.profit_loss_pct >= 0 ? '+' : ''}${annotation.profit_loss_pct.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">What would you like to edit?</label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button type="button" class="btn btn-outline-primary" id="edit-entry-btn">
|
||||
<i class="fas fa-arrow-up"></i> Move Entry
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="edit-exit-btn">
|
||||
<i class="fas fa-arrow-down"></i> Move Exit
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" id="delete-annotation-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store annotation for editing
|
||||
if (window.appState && window.appState.annotationManager) {
|
||||
window.appState.annotationManager.editingAnnotation = {
|
||||
annotation_id: annotationId,
|
||||
original: annotation,
|
||||
editMode: 'entry'
|
||||
};
|
||||
|
||||
// Remove current annotation from display
|
||||
this.removeAnnotation(annotationId);
|
||||
|
||||
// Show exit marker as reference
|
||||
const chart = this.charts[annotation.timeframe];
|
||||
if (chart) {
|
||||
Plotly.relayout(chart.plotId, {
|
||||
annotations: [{
|
||||
x: annotation.exit.timestamp,
|
||||
y: annotation.exit.price,
|
||||
text: '▼ (exit)',
|
||||
showarrow: true,
|
||||
arrowhead: 2,
|
||||
ax: 0,
|
||||
ay: 40,
|
||||
font: { size: 14, color: '#9ca3af' }
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (action === '2') {
|
||||
// Move exit point
|
||||
window.showSuccess('Click on chart to set new exit point');
|
||||
|
||||
if (window.appState && window.appState.annotationManager) {
|
||||
window.appState.annotationManager.editingAnnotation = {
|
||||
annotation_id: annotationId,
|
||||
original: annotation,
|
||||
editMode: 'exit'
|
||||
};
|
||||
|
||||
// Remove current annotation from display
|
||||
this.removeAnnotation(annotationId);
|
||||
|
||||
// Show entry marker as reference
|
||||
const chart = this.charts[annotation.timeframe];
|
||||
if (chart) {
|
||||
Plotly.relayout(chart.plotId, {
|
||||
annotations: [{
|
||||
x: annotation.entry.timestamp,
|
||||
y: annotation.entry.price,
|
||||
text: '▲ (entry)',
|
||||
showarrow: true,
|
||||
arrowhead: 2,
|
||||
ax: 0,
|
||||
ay: -40,
|
||||
font: { size: 14, color: '#9ca3af' }
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (action === '3') {
|
||||
// Delete
|
||||
this.handleAnnotationClick(annotationId, 'delete');
|
||||
// Remove existing modal if any
|
||||
const existingModal = document.getElementById('editAnnotationModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Add modal to page
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('editAnnotationModal'));
|
||||
modal.show();
|
||||
|
||||
// Add event listeners
|
||||
document.getElementById('edit-entry-btn').addEventListener('click', () => {
|
||||
modal.hide();
|
||||
this.startEditMode(annotationId, annotation, 'entry');
|
||||
});
|
||||
|
||||
document.getElementById('edit-exit-btn').addEventListener('click', () => {
|
||||
modal.hide();
|
||||
this.startEditMode(annotationId, annotation, 'exit');
|
||||
});
|
||||
|
||||
document.getElementById('delete-annotation-btn').addEventListener('click', () => {
|
||||
modal.hide();
|
||||
this.handleAnnotationClick(annotationId, 'delete');
|
||||
});
|
||||
|
||||
// Clean up modal when hidden
|
||||
document.getElementById('editAnnotationModal').addEventListener('hidden.bs.modal', () => {
|
||||
document.getElementById('editAnnotationModal').remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start edit mode for annotation
|
||||
*/
|
||||
startEditMode(annotationId, annotation, editMode) {
|
||||
const message = editMode === 'entry' ? 'Click on chart to set new entry point' : 'Click on chart to set new exit point';
|
||||
window.showSuccess(message);
|
||||
|
||||
// Store annotation for editing
|
||||
if (window.appState && window.appState.annotationManager) {
|
||||
window.appState.annotationManager.editingAnnotation = {
|
||||
annotation_id: annotationId,
|
||||
original: annotation,
|
||||
editMode: editMode
|
||||
};
|
||||
|
||||
// Remove current annotation from display
|
||||
this.removeAnnotation(annotationId);
|
||||
|
||||
// Show reference marker
|
||||
const chart = this.charts[annotation.timeframe];
|
||||
if (chart) {
|
||||
const referencePoint = editMode === 'entry' ? annotation.exit : annotation.entry;
|
||||
const markerText = editMode === 'entry' ? '▼ (exit)' : '▲ (entry)';
|
||||
const arrowDirection = editMode === 'entry' ? 40 : -40;
|
||||
|
||||
Plotly.relayout(chart.plotId, {
|
||||
annotations: [{
|
||||
x: referencePoint.timestamp,
|
||||
y: referencePoint.price,
|
||||
text: markerText,
|
||||
showarrow: true,
|
||||
arrowhead: 2,
|
||||
ax: 0,
|
||||
ay: arrowDirection,
|
||||
font: { size: 14, color: '#9ca3af' },
|
||||
bgcolor: 'rgba(31, 41, 55, 0.8)',
|
||||
borderpad: 4
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all annotations from charts
|
||||
*/
|
||||
clearAllAnnotations() {
|
||||
console.log('Clearing all annotations from charts');
|
||||
|
||||
// Clear from memory
|
||||
this.annotations = {};
|
||||
|
||||
// Update all charts
|
||||
Object.keys(this.charts).forEach(timeframe => {
|
||||
this.updateChartAnnotations(timeframe);
|
||||
});
|
||||
|
||||
console.log('All annotations cleared from charts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update chart layout when charts are minimized/maximized
|
||||
*/
|
||||
updateChartLayout() {
|
||||
const chartContainer = document.getElementById('chart-container');
|
||||
const visibleCharts = document.querySelectorAll('.timeframe-chart:not(.minimized)');
|
||||
const minimizedCharts = document.querySelectorAll('.timeframe-chart.minimized');
|
||||
|
||||
// Remove scroll if all charts are visible or if some are minimized
|
||||
if (minimizedCharts.length > 0) {
|
||||
chartContainer.classList.add('no-scroll');
|
||||
|
||||
// Calculate available height for visible charts
|
||||
const containerHeight = chartContainer.clientHeight;
|
||||
const headerHeight = 50; // Approximate header height
|
||||
const availableHeight = containerHeight - (visibleCharts.length * headerHeight);
|
||||
const chartHeight = Math.max(200, availableHeight / visibleCharts.length);
|
||||
|
||||
// Update visible chart heights
|
||||
visibleCharts.forEach(chart => {
|
||||
const plotElement = chart.querySelector('.chart-plot');
|
||||
if (plotElement) {
|
||||
plotElement.style.height = `${chartHeight}px`;
|
||||
|
||||
// Trigger Plotly resize
|
||||
const plotId = plotElement.id;
|
||||
if (plotId) {
|
||||
Plotly.Plots.resize(plotId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chartContainer.classList.remove('no-scroll');
|
||||
|
||||
// Reset to default heights
|
||||
visibleCharts.forEach(chart => {
|
||||
const plotElement = chart.querySelector('.chart-plot');
|
||||
if (plotElement) {
|
||||
plotElement.style.height = '300px';
|
||||
|
||||
// Trigger Plotly resize
|
||||
const plotId = plotElement.id;
|
||||
if (plotId) {
|
||||
Plotly.Plots.resize(plotId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Updated chart layout: ${visibleCharts.length} visible, ${minimizedCharts.length} minimized`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -804,6 +975,15 @@ class ChartManager {
|
||||
return colors[Math.min(level - 1, colors.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get marker size for pivot level
|
||||
* L1 = smallest (6px), L5 = largest (14px)
|
||||
*/
|
||||
_getPivotMarkerSize(level) {
|
||||
const sizes = [6, 8, 10, 12, 14]; // L1 to L5
|
||||
return sizes[Math.min(level - 1, sizes.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable crosshair cursor
|
||||
*/
|
||||
|
||||
@@ -299,59 +299,16 @@
|
||||
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
||||
}
|
||||
|
||||
function setupGlobalFunctions() {
|
||||
// Make functions globally available
|
||||
window.showError = showError;
|
||||
window.showSuccess = showSuccess;
|
||||
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);
|
||||
}
|
||||
|
||||
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" onclick="highlightAnnotation('${annotation.annotation_id}')" title="Highlight">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteAnnotation('${annotation.annotation_id}')" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
listElement.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAnnotation(annotationId) {
|
||||
console.log('deleteAnnotation called with ID:', annotationId);
|
||||
|
||||
if (!window.appState || !window.appState.annotations) {
|
||||
console.error('appState not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Current annotations:', window.appState.annotations.map(a => a.annotation_id));
|
||||
|
||||
if (!confirm('Delete this annotation?')) {
|
||||
console.log('Delete cancelled by user');
|
||||
return;
|
||||
@@ -406,9 +363,84 @@
|
||||
}
|
||||
|
||||
function highlightAnnotation(annotationId) {
|
||||
if (window.appState.chartManager) {
|
||||
if (window.appState && window.appState.chartManager) {
|
||||
window.appState.chartManager.highlightAnnotation(annotationId);
|
||||
}
|
||||
}
|
||||
|
||||
function setupGlobalFunctions() {
|
||||
// Make functions globally available
|
||||
window.showError = showError;
|
||||
window.showSuccess = showSuccess;
|
||||
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);
|
||||
}
|
||||
|
||||
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 for:', annotation.annotation_id);
|
||||
console.log('window.deleteAnnotation type:', typeof window.deleteAnnotation);
|
||||
|
||||
if (typeof window.deleteAnnotation === 'function') {
|
||||
console.log('Calling window.deleteAnnotation...');
|
||||
window.deleteAnnotation(annotation.annotation_id);
|
||||
} else {
|
||||
console.error('window.deleteAnnotation is not a function:', typeof window.deleteAnnotation);
|
||||
alert('Delete function not available. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
|
||||
listElement.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -166,12 +166,16 @@
|
||||
item.querySelector('.delete-annotation-btn').addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
console.log('Delete button clicked for:', annotation.annotation_id);
|
||||
console.log('window.deleteAnnotation type:', typeof window.deleteAnnotation);
|
||||
console.log('window object keys:', Object.keys(window).filter(k => k.includes('delete')));
|
||||
|
||||
// Use window.deleteAnnotation to ensure we get the global function
|
||||
if (typeof window.deleteAnnotation === 'function') {
|
||||
console.log('Calling window.deleteAnnotation...');
|
||||
window.deleteAnnotation(annotation.annotation_id);
|
||||
} else {
|
||||
console.error('window.deleteAnnotation is not a function:', typeof window.deleteAnnotation);
|
||||
console.log('Available functions:', Object.keys(window).filter(k => typeof window[k] === 'function'));
|
||||
alert('Delete function not available. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,7 +26,13 @@
|
||||
<div class="timeframe-chart" id="chart-1s">
|
||||
<div class="chart-header">
|
||||
<span class="timeframe-label">1 Second</span>
|
||||
<span class="chart-info" id="info-1s"></span>
|
||||
<div class="chart-header-controls">
|
||||
<span class="chart-info" id="info-1s"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-light minimize-btn" data-timeframe="1s"
|
||||
title="Minimize Chart">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-plot" id="plot-1s"></div>
|
||||
</div>
|
||||
@@ -34,7 +40,13 @@
|
||||
<div class="timeframe-chart" id="chart-1m">
|
||||
<div class="chart-header">
|
||||
<span class="timeframe-label">1 Minute</span>
|
||||
<span class="chart-info" id="info-1m"></span>
|
||||
<div class="chart-header-controls">
|
||||
<span class="chart-info" id="info-1m"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-light minimize-btn" data-timeframe="1m"
|
||||
title="Minimize Chart">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-plot" id="plot-1m"></div>
|
||||
</div>
|
||||
@@ -42,7 +54,13 @@
|
||||
<div class="timeframe-chart" id="chart-1h">
|
||||
<div class="chart-header">
|
||||
<span class="timeframe-label">1 Hour</span>
|
||||
<span class="chart-info" id="info-1h"></span>
|
||||
<div class="chart-header-controls">
|
||||
<span class="chart-info" id="info-1h"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-light minimize-btn" data-timeframe="1h"
|
||||
title="Minimize Chart">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-plot" id="plot-1h"></div>
|
||||
</div>
|
||||
@@ -50,7 +68,13 @@
|
||||
<div class="timeframe-chart" id="chart-1d">
|
||||
<div class="chart-header">
|
||||
<span class="timeframe-label">1 Day</span>
|
||||
<span class="chart-info" id="info-1d"></span>
|
||||
<div class="chart-header-controls">
|
||||
<span class="chart-info" id="info-1d"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-light minimize-btn" data-timeframe="1d"
|
||||
title="Minimize Chart">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-plot" id="plot-1d"></div>
|
||||
</div>
|
||||
@@ -68,25 +92,25 @@
|
||||
|
||||
<script>
|
||||
// Chart panel controls
|
||||
document.getElementById('zoom-in-btn').addEventListener('click', function() {
|
||||
document.getElementById('zoom-in-btn').addEventListener('click', function () {
|
||||
if (appState.chartManager) {
|
||||
appState.chartManager.handleZoom(1.5);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('zoom-out-btn').addEventListener('click', function() {
|
||||
document.getElementById('zoom-out-btn').addEventListener('click', function () {
|
||||
if (appState.chartManager) {
|
||||
appState.chartManager.handleZoom(0.67);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('reset-zoom-btn').addEventListener('click', function() {
|
||||
document.getElementById('reset-zoom-btn').addEventListener('click', function () {
|
||||
if (appState.chartManager) {
|
||||
appState.chartManager.resetZoom();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('fullscreen-btn').addEventListener('click', function() {
|
||||
document.getElementById('fullscreen-btn').addEventListener('click', function () {
|
||||
const chartContainer = document.getElementById('chart-container');
|
||||
if (chartContainer.requestFullscreen) {
|
||||
chartContainer.requestFullscreen();
|
||||
@@ -96,4 +120,37 @@
|
||||
chartContainer.msRequestFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// Minimize button functionality
|
||||
document.querySelectorAll('.minimize-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const timeframe = this.getAttribute('data-timeframe');
|
||||
const chartElement = document.getElementById(`chart-${timeframe}`);
|
||||
const plotElement = document.getElementById(`plot-${timeframe}`);
|
||||
|
||||
if (chartElement.classList.contains('minimized')) {
|
||||
// Restore chart
|
||||
chartElement.classList.remove('minimized');
|
||||
plotElement.style.display = 'block';
|
||||
this.innerHTML = '<i class="fas fa-minus"></i>';
|
||||
this.title = 'Minimize Chart';
|
||||
|
||||
// Update chart layout
|
||||
if (window.appState && window.appState.chartManager) {
|
||||
window.appState.chartManager.updateChartLayout();
|
||||
}
|
||||
} else {
|
||||
// Minimize chart
|
||||
chartElement.classList.add('minimized');
|
||||
plotElement.style.display = 'none';
|
||||
this.innerHTML = '<i class="fas fa-plus"></i>';
|
||||
this.title = 'Restore Chart';
|
||||
|
||||
// Update chart layout
|
||||
if (window.appState && window.appState.chartManager) {
|
||||
window.appState.chartManager.updateChartLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -121,7 +121,10 @@ class WilliamsMarketStructure:
|
||||
# Restore original pivot distance
|
||||
self.min_pivot_distance = original_distance
|
||||
|
||||
logger.debug(f"Calculated {len(self.pivot_levels)} pivot levels")
|
||||
logger.info(f"Calculated {len(self.pivot_levels)} pivot levels from {len(ohlcv_data)} candles")
|
||||
for level_num, level_data in self.pivot_levels.items():
|
||||
logger.info(f" L{level_num}: {len(level_data.pivot_points)} pivots")
|
||||
|
||||
return self.pivot_levels
|
||||
|
||||
except Exception as e:
|
||||
@@ -182,7 +185,7 @@ class WilliamsMarketStructure:
|
||||
)
|
||||
pivots.append(pivot)
|
||||
|
||||
logger.debug(f"Level 1: Found {len(pivots)} pivot points")
|
||||
logger.info(f"Level 1: Found {len(pivots)} pivot points from {len(df)} candles")
|
||||
return pivots
|
||||
|
||||
except Exception as e:
|
||||
@@ -197,10 +200,18 @@ class WilliamsMarketStructure:
|
||||
pivot points from the previous level as if they were OHLCV candles
|
||||
"""
|
||||
if level - 1 not in self.pivot_levels:
|
||||
logger.debug(f"Level {level}: Previous level {level-1} not found")
|
||||
return []
|
||||
|
||||
previous_level_pivots = self.pivot_levels[level - 1].pivot_points
|
||||
if len(previous_level_pivots) < self.min_pivot_distance * 2 + 1:
|
||||
|
||||
# For higher levels (L2+), use distance=1 to allow more pivots
|
||||
# Only L1 uses the strict min_pivot_distance requirement
|
||||
pivot_distance = 1
|
||||
required_pivots = pivot_distance * 2 + 1 # 3 pivots minimum
|
||||
|
||||
if len(previous_level_pivots) < required_pivots:
|
||||
logger.debug(f"Level {level}: Insufficient pivots from L{level-1} ({len(previous_level_pivots)} < {required_pivots} required)")
|
||||
return []
|
||||
|
||||
pivots = []
|
||||
@@ -210,13 +221,15 @@ class WilliamsMarketStructure:
|
||||
highs = [p for p in previous_level_pivots if p.pivot_type == 'high']
|
||||
lows = [p for p in previous_level_pivots if p.pivot_type == 'low']
|
||||
|
||||
# Find swing highs among the high pivots
|
||||
for i in range(self.min_pivot_distance, len(highs) - self.min_pivot_distance):
|
||||
logger.debug(f"Level {level}: Processing {len(highs)} highs and {len(lows)} lows from L{level-1}")
|
||||
|
||||
# Find swing highs among the high pivots (using distance=1 for L2+)
|
||||
for i in range(pivot_distance, len(highs) - pivot_distance):
|
||||
current_pivot = highs[i]
|
||||
|
||||
# Check if this high is surrounded by lower highs
|
||||
is_swing_high = True
|
||||
for j in range(i - self.min_pivot_distance, i + self.min_pivot_distance + 1):
|
||||
for j in range(i - pivot_distance, i + pivot_distance + 1):
|
||||
if j != i and j < len(highs) and highs[j].price >= current_pivot.price:
|
||||
is_swing_high = False
|
||||
break
|
||||
@@ -234,12 +247,12 @@ class WilliamsMarketStructure:
|
||||
pivots.append(pivot)
|
||||
|
||||
# Find swing lows among the low pivots
|
||||
for i in range(self.min_pivot_distance, len(lows) - self.min_pivot_distance):
|
||||
for i in range(pivot_distance, len(lows) - pivot_distance):
|
||||
current_pivot = lows[i]
|
||||
|
||||
# Check if this low is surrounded by higher lows
|
||||
is_swing_low = True
|
||||
for j in range(i - self.min_pivot_distance, i + self.min_pivot_distance + 1):
|
||||
for j in range(i - pivot_distance, i + pivot_distance + 1):
|
||||
if j != i and j < len(lows) and lows[j].price <= current_pivot.price:
|
||||
is_swing_low = False
|
||||
break
|
||||
@@ -259,7 +272,7 @@ class WilliamsMarketStructure:
|
||||
# Sort pivots by timestamp
|
||||
pivots.sort(key=lambda x: x.timestamp)
|
||||
|
||||
logger.debug(f"Level {level}: Found {len(pivots)} pivot points")
|
||||
logger.info(f"Level {level}: Found {len(pivots)} pivot points (from {len(highs)} highs, {len(lows)} lows)")
|
||||
return pivots
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user