use common williams market structure calcs

This commit is contained in:
Dobromir Popov
2025-10-24 12:30:17 +03:00
parent bd95ff610c
commit 42bf91b735
3 changed files with 260 additions and 162 deletions

View File

@@ -19,16 +19,19 @@ import logging
from datetime import datetime from datetime import datetime
import json import json
import pandas as pd import pandas as pd
import numpy as np
# Import core components from main system # Import core components from main system
try: try:
from core.data_provider import DataProvider from core.data_provider import DataProvider
from core.orchestrator import TradingOrchestrator from core.orchestrator import TradingOrchestrator
from core.config import get_config from core.config import get_config
from core.williams_market_structure import WilliamsMarketStructure
except ImportError as e: except ImportError as e:
print(f"Warning: Could not import main system components: {e}") print(f"Warning: Could not import main system components: {e}")
print("Running in standalone mode with limited functionality") print("Running in standalone mode with limited functionality")
DataProvider = None DataProvider = None
WilliamsMarketStructure = None
TradingOrchestrator = None TradingOrchestrator = None
get_config = lambda: {} get_config = lambda: {}
@@ -200,66 +203,90 @@ class AnnotationDashboard:
def _get_pivot_markers_for_timeframe(self, symbol: str, timeframe: str, df: pd.DataFrame) -> dict: def _get_pivot_markers_for_timeframe(self, symbol: str, timeframe: str, df: pd.DataFrame) -> dict:
""" """
Get pivot markers for a specific timeframe Get pivot markers for a specific timeframe using WilliamsMarketStructure directly
Returns dict with indices mapping to pivot info for that candle Returns dict with all pivot points and identifies which are the last high/low per level
""" """
try: try:
if not self.data_provider: if WilliamsMarketStructure is None:
logger.warning("WilliamsMarketStructure not available")
return {} return {}
# Get Williams pivot levels for this timeframe if df is None or len(df) < 10:
pivot_levels = self.data_provider.get_williams_pivot_levels( logger.warning(f"Insufficient data for pivot calculation: {len(df) if df is not None else 0} bars")
symbol=symbol, return {}
base_timeframe=timeframe,
limit=len(df) # 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: if not pivot_levels:
logger.debug(f"No pivot levels found for {symbol} {timeframe}")
return {} return {}
# Build a map of timestamp -> pivot info # Build a map of timestamp -> pivot info
# Also track last high/low per level for drawing horizontal lines
pivot_map = {} 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(): for level_num, trend_level in pivot_levels.items():
if not hasattr(trend_level, 'pivot_points') or not trend_level.pivot_points: if not hasattr(trend_level, 'pivot_points') or not trend_level.pivot_points:
continue continue
# Find last high and last low for this level last_pivots[level_num] = {'high': None, 'low': None}
last_high = None
last_low = None
# Add ALL pivot points to the map
for pivot in trend_level.pivot_points: 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': 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': elif pivot.pivot_type == 'low':
last_low = pivot pivot_map[ts_str]['lows'].append(pivot_info)
last_pivots[level_num]['low'] = (ts_str, len(pivot_map[ts_str]['lows']) - 1)
# Add to pivot map # Mark the last high and last low for each level
if last_high: for level_num, last_info in last_pivots.items():
ts_str = last_high.timestamp.strftime('%Y-%m-%d %H:%M:%S') if last_info['high']:
if ts_str not in pivot_map: ts_str, idx = last_info['high']
pivot_map[ts_str] = {'highs': [], 'lows': []} pivot_map[ts_str]['highs'][idx]['is_last'] = True
pivot_map[ts_str]['highs'].append({ if last_info['low']:
'level': level_num, ts_str, idx = last_info['low']
'price': last_high.price, pivot_map[ts_str]['lows'][idx]['is_last'] = True
'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
})
logger.info(f"Found {len(pivot_map)} pivot candles for {symbol} {timeframe} (from {len(df)} candles)")
return pivot_map return pivot_map
except Exception as e: except Exception as e:
logger.error(f"Error getting pivot markers for {timeframe}: {e}") logger.error(f"Error getting pivot markers for {timeframe}: {e}")
import traceback
logger.error(traceback.format_exc())
return {} return {}
def _setup_routes(self): def _setup_routes(self):

View File

@@ -149,9 +149,10 @@ class ChartManager {
// Prepare chart data with pivot bounds // Prepare chart data with pivot bounds
const chartData = [candlestickTrace, volumeTrace]; 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 shapes = [];
const annotations = []; 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) { if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) {
const xMin = data.timestamps[0]; const xMin = data.timestamps[0];
@@ -159,10 +160,21 @@ class ChartManager {
// Process each timestamp that has pivot markers // Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { 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) { if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => { pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high'); const color = this._getPivotColor(pivot.level, 'high');
// 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<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.symbol.push('triangle-down');
// Draw horizontal line ONLY for last pivot of this level
if (pivot.is_last) {
shapes.push({ shapes.push({
type: 'line', type: 'line',
x0: xMin, x0: xMin,
@@ -191,13 +203,25 @@ class ChartManager {
bgcolor: '#1f2937', bgcolor: '#1f2937',
borderpad: 2 borderpad: 2
}); });
}
}); });
} }
// Draw horizontal lines for last low pivots (support) // Process low pivots
if (pivots.lows && pivots.lows.length > 0) { if (pivots.lows && pivots.lows.length > 0) {
pivots.lows.forEach(pivot => { pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low'); const color = this._getPivotColor(pivot.level, 'low');
// 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<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.symbol.push('triangle-up');
// Draw horizontal line ONLY for last pivot of this level
if (pivot.is_last) {
shapes.push({ shapes.push({
type: 'line', type: 'line',
x0: xMin, x0: xMin,
@@ -226,10 +250,16 @@ class ChartManager {
bgcolor: '#1f2937', bgcolor: '#1f2937',
borderpad: 2 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`); console.log(`Added ${shapes.length} pivot levels to ${timeframe} chart`);
} }
@@ -370,6 +400,7 @@ class ChartManager {
// Add pivot markers from chart data // Add pivot markers from chart data
const shapes = []; const shapes = [];
const annotations = []; 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) { if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) {
const xMin = data.timestamps[0]; const xMin = data.timestamps[0];
@@ -377,10 +408,21 @@ class ChartManager {
// Process each timestamp that has pivot markers // Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { 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) { if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => { pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high'); const color = this._getPivotColor(pivot.level, 'high');
// Draw dot on the pivot candle
pivotDots.x.push(timestamp);
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.symbol.push('triangle-down');
// Draw horizontal line ONLY for last pivot
if (pivot.is_last) {
shapes.push({ shapes.push({
type: 'line', type: 'line',
x0: xMin, x0: xMin,
@@ -408,13 +450,25 @@ class ChartManager {
bgcolor: '#1f2937', bgcolor: '#1f2937',
borderpad: 2 borderpad: 2
}); });
}
}); });
} }
// Draw horizontal lines for last low pivots // Process low pivots
if (pivots.lows && pivots.lows.length > 0) { if (pivots.lows && pivots.lows.length > 0) {
pivots.lows.forEach(pivot => { pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low'); const color = this._getPivotColor(pivot.level, 'low');
// Draw dot on the pivot candle
pivotDots.x.push(timestamp);
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.symbol.push('triangle-up');
// Draw horizontal line ONLY for last pivot
if (pivot.is_last) {
shapes.push({ shapes.push({
type: 'line', type: 'line',
x0: xMin, x0: xMin,
@@ -442,9 +496,15 @@ class ChartManager {
bgcolor: '#1f2937', bgcolor: '#1f2937',
borderpad: 2 borderpad: 2
}); });
}
}); });
} }
}); });
// Add pivot dots trace if we have any
if (pivotDots.x.length > 0) {
chartData.push(pivotDots);
}
} }
// Use Plotly.react for efficient updates // Use Plotly.react for efficient updates

View File

@@ -65,18 +65,22 @@ class WilliamsMarketStructure:
logger.info(f"Williams Market Structure initialized with {self.max_levels} levels") 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 Calculate recursive pivot points following Williams Market Structure methodology
Args: Args:
ohlcv_data: OHLCV data array with shape (N, 6) [timestamp, O, H, L, C, V] 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: Returns:
Dictionary of trend levels with pivot points Dictionary of trend levels with pivot points
""" """
try: 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") logger.warning(f"Insufficient data for pivot calculation: {len(ohlcv_data)} bars")
return {} return {}
@@ -87,6 +91,10 @@ class WilliamsMarketStructure:
# Initialize pivot levels # Initialize pivot levels
self.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: Calculate pivot points from raw OHLCV data
level_1_pivots = self._calculate_level_1_pivots(df) level_1_pivots = self._calculate_level_1_pivots(df)
if level_1_pivots: if level_1_pivots:
@@ -110,6 +118,9 @@ class WilliamsMarketStructure:
else: else:
break # No more higher level pivots possible 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") logger.debug(f"Calculated {len(self.pivot_levels)} pivot levels")
return self.pivot_levels return self.pivot_levels