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