min pivot distance only applies to L1 pivots ,

charts minimize addons,
delete buttons WIP
This commit is contained in:
Dobromir Popov
2025-10-24 13:49:16 +03:00
parent 42bf91b735
commit b970c4ca4d
8 changed files with 532 additions and 169 deletions

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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);
});
}

View File

@@ -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
*/

View File

@@ -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 %}

View File

@@ -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.');
}
});

View File

@@ -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>

View File

@@ -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: