diff --git a/ANNOTATE/web/app.py b/ANNOTATE/web/app.py index 11f6739..5421ed2 100644 --- a/ANNOTATE/web/app.py +++ b/ANNOTATE/web/app.py @@ -19,16 +19,19 @@ import logging from datetime import datetime import json import pandas as pd +import numpy as np # Import core components from main system try: from core.data_provider import DataProvider from core.orchestrator import TradingOrchestrator from core.config import get_config + from core.williams_market_structure import WilliamsMarketStructure except ImportError as e: print(f"Warning: Could not import main system components: {e}") print("Running in standalone mode with limited functionality") DataProvider = None + WilliamsMarketStructure = None TradingOrchestrator = None get_config = lambda: {} @@ -200,66 +203,90 @@ class AnnotationDashboard: def _get_pivot_markers_for_timeframe(self, symbol: str, timeframe: str, df: pd.DataFrame) -> dict: """ - Get pivot markers for a specific timeframe - Returns dict with indices mapping to pivot info for that candle + Get pivot markers for a specific timeframe using WilliamsMarketStructure directly + Returns dict with all pivot points and identifies which are the last high/low per level """ try: - if not self.data_provider: + if WilliamsMarketStructure is None: + logger.warning("WilliamsMarketStructure not available") return {} - # Get Williams pivot levels for this timeframe - pivot_levels = self.data_provider.get_williams_pivot_levels( - symbol=symbol, - base_timeframe=timeframe, - limit=len(df) + if df is None or len(df) < 10: + logger.warning(f"Insufficient data for pivot calculation: {len(df) if df is not None else 0} bars") + return {} + + # Convert DataFrame to numpy array format expected by Williams Market Structure + ohlcv_array = df[['open', 'high', 'low', 'close', 'volume']].copy() + + # Add timestamp as first column (convert to milliseconds) + timestamps = df.index.astype(np.int64) // 10**6 # pandas index is ns -> convert to ms + ohlcv_array.insert(0, 'timestamp', timestamps) + ohlcv_array = ohlcv_array.to_numpy() + + # Initialize Williams Market Structure with default distance + # We'll override it in the calculation call + williams = WilliamsMarketStructure(min_pivot_distance=1) + + # Calculate recursive pivot points with min_pivot_distance=2 + # This ensures 5 candles per pivot (tip + 2 prev + 2 next) + pivot_levels = williams.calculate_recursive_pivot_points( + ohlcv_array, + min_pivot_distance=2 ) if not pivot_levels: + logger.debug(f"No pivot levels found for {symbol} {timeframe}") return {} # Build a map of timestamp -> pivot info + # Also track last high/low per level for drawing horizontal lines pivot_map = {} + last_pivots = {} # {level: {'high': (ts_str, idx), 'low': (ts_str, idx)}} - # For each level (1-5), find the last high and last low pivot + # For each level (1-5), collect ALL pivot points for level_num, trend_level in pivot_levels.items(): if not hasattr(trend_level, 'pivot_points') or not trend_level.pivot_points: continue - # Find last high and last low for this level - last_high = None - last_low = None + last_pivots[level_num] = {'high': None, 'low': None} + # Add ALL pivot points to the map for pivot in trend_level.pivot_points: + ts_str = pivot.timestamp.strftime('%Y-%m-%d %H:%M:%S') + + if ts_str not in pivot_map: + pivot_map[ts_str] = {'highs': [], 'lows': []} + + pivot_info = { + 'level': level_num, + 'price': pivot.price, + 'strength': pivot.strength, + 'is_last': False # Will be updated below + } + if pivot.pivot_type == 'high': - last_high = pivot + pivot_map[ts_str]['highs'].append(pivot_info) + last_pivots[level_num]['high'] = (ts_str, len(pivot_map[ts_str]['highs']) - 1) elif pivot.pivot_type == 'low': - last_low = pivot - - # Add to pivot map - if last_high: - ts_str = last_high.timestamp.strftime('%Y-%m-%d %H:%M:%S') - if ts_str not in pivot_map: - pivot_map[ts_str] = {'highs': [], 'lows': []} - pivot_map[ts_str]['highs'].append({ - 'level': level_num, - 'price': last_high.price, - 'strength': last_high.strength - }) - - if last_low: - ts_str = last_low.timestamp.strftime('%Y-%m-%d %H:%M:%S') - if ts_str not in pivot_map: - pivot_map[ts_str] = {'highs': [], 'lows': []} - pivot_map[ts_str]['lows'].append({ - 'level': level_num, - 'price': last_low.price, - 'strength': last_low.strength - }) + pivot_map[ts_str]['lows'].append(pivot_info) + last_pivots[level_num]['low'] = (ts_str, len(pivot_map[ts_str]['lows']) - 1) + # Mark the last high and last low for each level + for level_num, last_info in last_pivots.items(): + if last_info['high']: + ts_str, idx = last_info['high'] + pivot_map[ts_str]['highs'][idx]['is_last'] = True + if last_info['low']: + ts_str, idx = last_info['low'] + pivot_map[ts_str]['lows'][idx]['is_last'] = True + + logger.info(f"Found {len(pivot_map)} pivot candles for {symbol} {timeframe} (from {len(df)} candles)") return pivot_map except Exception as e: logger.error(f"Error getting pivot markers for {timeframe}: {e}") + import traceback + logger.error(traceback.format_exc()) return {} def _setup_routes(self): diff --git a/ANNOTATE/web/static/js/chart_manager.js b/ANNOTATE/web/static/js/chart_manager.js index 4c9757c..793c48e 100644 --- a/ANNOTATE/web/static/js/chart_manager.js +++ b/ANNOTATE/web/static/js/chart_manager.js @@ -149,87 +149,117 @@ class ChartManager { // Prepare chart data with pivot bounds const chartData = [candlestickTrace, volumeTrace]; - // Add pivot markers from chart data (last high/low for each level L1-L5) + // Add pivot markers from chart data const shapes = []; const annotations = []; - + const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false }; + if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) { const xMin = data.timestamps[0]; const xMax = data.timestamps[data.timestamps.length - 1]; - + // Process each timestamp that has pivot markers Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { - // Draw horizontal lines for last high pivots (resistance) + // Process high pivots if (pivots.highs && pivots.highs.length > 0) { pivots.highs.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'high'); - shapes.push({ - type: 'line', - x0: xMin, - y0: pivot.price, - x1: xMax, - y1: pivot.price, - line: { - color: color, - width: 1, - dash: 'dash' - }, - layer: 'below' - }); - - // Add label for the level - annotations.push({ - x: xMax, - y: pivot.price, - text: `L${pivot.level}H`, - showarrow: false, - xanchor: 'left', - font: { - size: 9, - color: color - }, - bgcolor: '#1f2937', - borderpad: 2 - }); + + // Draw dot on the pivot candle (above the high) + pivotDots.x.push(timestamp); + pivotDots.y.push(pivot.price); + pivotDots.text.push(`L${pivot.level} High Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); + pivotDots.marker.color.push(color); + pivotDots.marker.size.push(8); + pivotDots.marker.symbol.push('triangle-down'); + + // Draw horizontal line ONLY for last pivot of this level + if (pivot.is_last) { + shapes.push({ + type: 'line', + x0: xMin, + y0: pivot.price, + x1: xMax, + y1: pivot.price, + line: { + color: color, + width: 1, + dash: 'dash' + }, + layer: 'below' + }); + + // Add label for the level + annotations.push({ + x: xMax, + y: pivot.price, + text: `L${pivot.level}H`, + showarrow: false, + xanchor: 'left', + font: { + size: 9, + color: color + }, + bgcolor: '#1f2937', + borderpad: 2 + }); + } }); } - - // Draw horizontal lines for last low pivots (support) + + // Process low pivots if (pivots.lows && pivots.lows.length > 0) { pivots.lows.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'low'); - shapes.push({ - type: 'line', - x0: xMin, - y0: pivot.price, - x1: xMax, - y1: pivot.price, - line: { - color: color, - width: 1, - dash: 'dash' - }, - layer: 'below' - }); - - // Add label for the level - annotations.push({ - x: xMax, - y: pivot.price, - text: `L${pivot.level}L`, - showarrow: false, - xanchor: 'left', - font: { - size: 9, - color: color - }, - bgcolor: '#1f2937', - borderpad: 2 - }); + + // Draw dot on the pivot candle (below the low) + pivotDots.x.push(timestamp); + pivotDots.y.push(pivot.price); + pivotDots.text.push(`L${pivot.level} Low Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); + pivotDots.marker.color.push(color); + pivotDots.marker.size.push(8); + pivotDots.marker.symbol.push('triangle-up'); + + // Draw horizontal line ONLY for last pivot of this level + if (pivot.is_last) { + shapes.push({ + type: 'line', + x0: xMin, + y0: pivot.price, + x1: xMax, + y1: pivot.price, + line: { + color: color, + width: 1, + dash: 'dash' + }, + layer: 'below' + }); + + // Add label for the level + annotations.push({ + x: xMax, + y: pivot.price, + text: `L${pivot.level}L`, + showarrow: false, + xanchor: 'left', + font: { + size: 9, + color: color + }, + bgcolor: '#1f2937', + borderpad: 2 + }); + } }); } }); - + + // Add pivot dots trace if we have any + if (pivotDots.x.length > 0) { + chartData.push(pivotDots); + } + console.log(`Added ${shapes.length} pivot levels to ${timeframe} chart`); } @@ -370,85 +400,115 @@ class ChartManager { // Add pivot markers from chart data const shapes = []; const annotations = []; - + const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false }; + if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) { const xMin = data.timestamps[0]; const xMax = data.timestamps[data.timestamps.length - 1]; - + // Process each timestamp that has pivot markers Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { - // Draw horizontal lines for last high pivots + // Process high pivots if (pivots.highs && pivots.highs.length > 0) { pivots.highs.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'high'); - shapes.push({ - type: 'line', - x0: xMin, - y0: pivot.price, - x1: xMax, - y1: pivot.price, - line: { - color: color, - width: 1, - dash: 'dash' - }, - layer: 'below' - }); - - annotations.push({ - x: xMax, - y: pivot.price, - text: `L${pivot.level}H`, - showarrow: false, - xanchor: 'left', - font: { - size: 9, - color: color - }, - bgcolor: '#1f2937', - borderpad: 2 - }); + + // Draw dot on the pivot candle + pivotDots.x.push(timestamp); + pivotDots.y.push(pivot.price); + pivotDots.text.push(`L${pivot.level} High Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); + pivotDots.marker.color.push(color); + pivotDots.marker.size.push(8); + pivotDots.marker.symbol.push('triangle-down'); + + // Draw horizontal line ONLY for last pivot + if (pivot.is_last) { + shapes.push({ + type: 'line', + x0: xMin, + y0: pivot.price, + x1: xMax, + y1: pivot.price, + line: { + color: color, + width: 1, + dash: 'dash' + }, + layer: 'below' + }); + + annotations.push({ + x: xMax, + y: pivot.price, + text: `L${pivot.level}H`, + showarrow: false, + xanchor: 'left', + font: { + size: 9, + color: color + }, + bgcolor: '#1f2937', + borderpad: 2 + }); + } }); } - - // Draw horizontal lines for last low pivots + + // Process low pivots if (pivots.lows && pivots.lows.length > 0) { pivots.lows.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'low'); - shapes.push({ - type: 'line', - x0: xMin, - y0: pivot.price, - x1: xMax, - y1: pivot.price, - line: { - color: color, - width: 1, - dash: 'dash' - }, - layer: 'below' - }); - - annotations.push({ - x: xMax, - y: pivot.price, - text: `L${pivot.level}L`, - showarrow: false, - xanchor: 'left', - font: { - size: 9, - color: color - }, - bgcolor: '#1f2937', - borderpad: 2 - }); + + // Draw dot on the pivot candle + pivotDots.x.push(timestamp); + pivotDots.y.push(pivot.price); + pivotDots.text.push(`L${pivot.level} Low Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); + pivotDots.marker.color.push(color); + pivotDots.marker.size.push(8); + pivotDots.marker.symbol.push('triangle-up'); + + // Draw horizontal line ONLY for last pivot + if (pivot.is_last) { + shapes.push({ + type: 'line', + x0: xMin, + y0: pivot.price, + x1: xMax, + y1: pivot.price, + line: { + color: color, + width: 1, + dash: 'dash' + }, + layer: 'below' + }); + + annotations.push({ + x: xMax, + y: pivot.price, + text: `L${pivot.level}L`, + showarrow: false, + xanchor: 'left', + font: { + size: 9, + color: color + }, + bgcolor: '#1f2937', + borderpad: 2 + }); + } }); } }); + + // Add pivot dots trace if we have any + if (pivotDots.x.length > 0) { + chartData.push(pivotDots); + } } // Use Plotly.react for efficient updates - const update = { + const update = { shapes: shapes, annotations: annotations }; @@ -739,11 +799,11 @@ class ChartManager { // Different colors for different levels const highColors = ['#dc3545', '#ff6b6b', '#ff8787', '#ffa8a8', '#ffc9c9']; const lowColors = ['#28a745', '#51cf66', '#69db7c', '#8ce99a', '#b2f2bb']; - + const colors = type === 'high' ? highColors : lowColors; return colors[Math.min(level - 1, colors.length - 1)]; } - + /** * Enable crosshair cursor */ diff --git a/core/williams_market_structure.py b/core/williams_market_structure.py index 494b956..9b401a7 100644 --- a/core/williams_market_structure.py +++ b/core/williams_market_structure.py @@ -65,18 +65,22 @@ class WilliamsMarketStructure: logger.info(f"Williams Market Structure initialized with {self.max_levels} levels") - def calculate_recursive_pivot_points(self, ohlcv_data: np.ndarray) -> Dict[int, TrendLevel]: + def calculate_recursive_pivot_points(self, ohlcv_data: np.ndarray, min_pivot_distance: int = None) -> Dict[int, TrendLevel]: """ Calculate recursive pivot points following Williams Market Structure methodology Args: ohlcv_data: OHLCV data array with shape (N, 6) [timestamp, O, H, L, C, V] + min_pivot_distance: Override the instance's min_pivot_distance for this calculation (default: None uses instance value) Returns: Dictionary of trend levels with pivot points """ try: - if len(ohlcv_data) < self.min_pivot_distance * 2 + 1: + # Use provided min_pivot_distance or fall back to instance default + pivot_distance = min_pivot_distance if min_pivot_distance is not None else self.min_pivot_distance + + if len(ohlcv_data) < pivot_distance * 2 + 1: logger.warning(f"Insufficient data for pivot calculation: {len(ohlcv_data)} bars") return {} @@ -87,6 +91,10 @@ class WilliamsMarketStructure: # Initialize pivot levels self.pivot_levels = {} + # Temporarily set the pivot distance for this calculation + original_distance = self.min_pivot_distance + self.min_pivot_distance = pivot_distance + # Level 1: Calculate pivot points from raw OHLCV data level_1_pivots = self._calculate_level_1_pivots(df) if level_1_pivots: @@ -110,6 +118,9 @@ class WilliamsMarketStructure: else: break # No more higher level pivots possible + # Restore original pivot distance + self.min_pivot_distance = original_distance + logger.debug(f"Calculated {len(self.pivot_levels)} pivot levels") return self.pivot_levels