fix pivots display

This commit is contained in:
Dobromir Popov
2025-10-20 16:17:43 +03:00
parent e993bc2831
commit a8ea9b24c0
6 changed files with 1314 additions and 139 deletions

View File

@@ -1805,6 +1805,19 @@ class CleanTradingDashboard:
trade_count = len(self.closed_trades)
trade_str = f"{trade_count} Trades"
# Portfolio value
portfolio_value = self._get_live_account_balance()
portfolio_str = f"${portfolio_value:.2f}"
# Profitability multiplier
if portfolio_value > 0 and self.starting_balance > 0:
multiplier = portfolio_value / self.starting_balance
multiplier_str = f"{multiplier:.1f}x"
else:
multiplier_str = "1.0x"
# Exchange status
mexc_status = "Connected" if self._check_exchange_connection() else "Disconnected"
# COB WebSocket status with update rate
cob_status = self.get_cob_websocket_status()
@@ -1823,37 +1836,54 @@ class CleanTradingDashboard:
else:
cob_status_str = f"Error ({update_rate:.1f}/s)"
# Open Interest (multi-source via report crawler)
# Open Interest (multi-source via report crawler) - CACHED to reduce API calls
try:
oi_display = "Loading..."
if hasattr(self, 'data_provider') and self.data_provider:
# Prefer BTC/USDT for OI if trading BTC, else ETH/USDT
oi_symbol = 'BTC/USDT' if 'BTC' in (self.trading_symbol if hasattr(self, 'trading_symbol') else 'BTC/USDT') else 'ETH/USDT'
# Lazy import to avoid circulars
from core.report_data_crawler import ReportDataCrawler
if not hasattr(self, '_report_crawler') or self._report_crawler is None:
self._report_crawler = ReportDataCrawler(self.data_provider)
report = self._report_crawler.crawl_report_data(oi_symbol)
if report and report.open_interest_data:
# Show first two sources compactly
parts = []
for oi in report.open_interest_data[:2]:
try:
val = float(oi.open_interest) if oi.open_interest else 0
parts.append(f"{oi.source.upper()}: {val:,.0f}")
except Exception:
continue
if parts:
oi_display = " | ".join(parts)
# Cache OI data for 30 seconds to avoid over-scanning
import time
current_time = time.time()
cache_key = '_oi_cache'
cache_ttl = 30 # seconds
if not hasattr(self, cache_key) or (current_time - self._oi_cache_time > cache_ttl):
oi_display = "Loading..."
if hasattr(self, 'data_provider') and self.data_provider:
# Prefer BTC/USDT for OI if trading BTC, else ETH/USDT
oi_symbol = 'BTC/USDT' if 'BTC' in (self.trading_symbol if hasattr(self, 'trading_symbol') else 'BTC/USDT') else 'ETH/USDT'
# Lazy import to avoid circulars
from core.report_data_crawler import ReportDataCrawler
if not hasattr(self, '_report_crawler') or self._report_crawler is None:
self._report_crawler = ReportDataCrawler(self.data_provider)
report = self._report_crawler.crawl_report_data(oi_symbol)
if report and report.open_interest_data:
# Show first two sources compactly
parts = []
for oi in report.open_interest_data[:2]:
try:
val = float(oi.open_interest) if oi.open_interest else 0
parts.append(f"{oi.source.upper()}: {val:,.0f}")
except Exception:
continue
if parts:
oi_display = " | ".join(parts)
else:
oi_display = "N/A"
else:
oi_display = "N/A"
else:
oi_display = "N/A"
# Update cache
self._oi_cache = oi_display
self._oi_cache_time = current_time
else:
oi_display = "N/A"
# Use cached value
oi_display = self._oi_cache
except Exception as e:
logger.debug(f"Open Interest display error: {e}")
oi_display = "N/A"
# Set cache to avoid repeated errors
if not hasattr(self, '_oi_cache_time'):
self._oi_cache_time = 0
return price_str, session_pnl_str, position_str, oi_display, trade_str, portfolio_str, multiplier_str, cob_status_str, mexc_status
@@ -1918,7 +1948,8 @@ class CleanTradingDashboard:
return [html.P(f"Error: {str(e)}", className="text-danger")]
@self.app.callback(
Output('price-chart', 'figure'),
[Output('price-chart', 'figure'),
Output('williams-trend-legend', 'children')],
[Input('interval-component', 'n_intervals'),
Input('show-pivots-switch', 'value')],
[State('price-chart', 'relayoutData')]
@@ -1927,7 +1958,7 @@ class CleanTradingDashboard:
"""Update price chart every second, persisting user zoom/pan"""
try:
show_pivots = bool(pivots_value and 'enabled' in pivots_value)
fig = self._create_price_chart('ETH/USDT', show_pivots=show_pivots)
fig, legend_children = self._create_price_chart('ETH/USDT', show_pivots=show_pivots, return_legend=True)
if relayout_data:
if 'xaxis.range[0]' in relayout_data and 'xaxis.range[1]' in relayout_data:
@@ -1935,7 +1966,7 @@ class CleanTradingDashboard:
if 'yaxis.range[0]' in relayout_data and 'yaxis.range[1]' in relayout_data:
fig.update_yaxes(range=[relayout_data['yaxis.range[0]'], relayout_data['yaxis.range[1]']])
return fig
return fig, legend_children
except Exception as e:
logger.error(f"Error updating chart: {e}")
return go.Figure().add_annotation(text=f"Chart Error: {str(e)}",
@@ -2026,6 +2057,9 @@ class CleanTradingDashboard:
# Determine COB data source mode
cob_mode = self._get_cob_mode()
# Build COB components (placeholder for now to fix the error)
eth_components = html.Div("ETH COB: Loading...", className="text-muted")
btc_components = html.Div("BTC COB: Loading...", className="text-muted")
return eth_components, btc_components
@@ -2879,8 +2913,10 @@ class CleanTradingDashboard:
# Return None if absolutely nothing available
return None
def _create_price_chart(self, symbol: str, show_pivots: bool = True) -> go.Figure:
"""Create 1-minute main chart with 1-second mini chart - Updated every second"""
def _create_price_chart(self, symbol: str, show_pivots: bool = True, return_legend: bool = False):
"""Create 1-minute main chart with 1-second mini chart - Updated every second
If return_legend is True, returns (figure, legend_children) and keeps legend out of chart to avoid scale issues.
"""
try:
# FIXED: Always get fresh data on startup to avoid gaps
# 1. Get historical 1-minute data as base (180 candles = 3 hours) - FORCE REFRESH on first load
@@ -3049,67 +3085,11 @@ class CleanTradingDashboard:
self._add_trades_to_chart(fig, symbol, df_main, row=1)
# ADD PIVOT POINTS TO MAIN CHART
self._add_pivot_points_to_chart(fig, symbol, df_main, row=1)
# ADD PIVOT POINTS TO MAIN CHART (overlay on 1m)
legend_children = None
if show_pivots:
try:
pivots_input = None
if hasattr(self.data_provider, 'get_base_data_input'):
bdi = self.data_provider.get_base_data_input(symbol)
if bdi and getattr(bdi, 'pivot_points', None):
pivots_input = bdi.pivot_points
if pivots_input:
# Filter pivots within the visible time range of df_main
start_ts = df_main.index.min()
end_ts = df_main.index.max()
xs_high = []
ys_high = []
xs_low = []
ys_low = []
for p in pivots_input:
ts = getattr(p, 'timestamp', None)
price = getattr(p, 'price', None)
ptype = getattr(p, 'type', 'low')
if ts is None or price is None:
continue
# Convert pivot timestamp to local tz and make tz-naive to match chart axes
try:
if hasattr(ts, 'tzinfo') and ts.tzinfo is not None:
pt = ts.astimezone(_local_tz) if _local_tz else ts
else:
# Assume UTC then convert
pt = ts.replace(tzinfo=timezone.utc)
pt = pt.astimezone(_local_tz) if _local_tz else pt
# Drop tzinfo for plotting
try:
pt = pt.replace(tzinfo=None)
except Exception:
pass
except Exception:
pt = ts
if start_ts <= pt <= end_ts:
if str(ptype).lower() == 'high':
xs_high.append(pt)
ys_high.append(price)
else:
xs_low.append(pt)
ys_low.append(price)
if xs_high or xs_low:
fig.add_trace(
go.Scatter(x=xs_high, y=ys_high, mode='markers', name='Pivot High',
marker=dict(color='#ff7043', size=7, symbol='triangle-up'),
hoverinfo='skip'),
row=1, col=1
)
fig.add_trace(
go.Scatter(x=xs_low, y=ys_low, mode='markers', name='Pivot Low',
marker=dict(color='#42a5f5', size=7, symbol='triangle-down'),
hoverinfo='skip'),
row=1, col=1
)
except Exception as e:
logger.debug(f"Error overlaying pivot points: {e}")
legend_children = self._add_pivot_points_to_chart(fig, symbol, df_main, row=1)
# Remove old inline overlay system (now handled in _add_pivot_points_to_chart with external legend)
# Mini 1-second chart (if available)
if has_mini_chart and ws_data_1s is not None:
@@ -3189,6 +3169,8 @@ class CleanTradingDashboard:
chart_info += f", 1s ticks: {len(ws_data_1s)}"
logger.debug(f"[CHART] Created combined chart - {chart_info}")
if return_legend:
return fig, (legend_children or [])
return fig
except Exception as e:
@@ -4338,14 +4320,22 @@ class CleanTradingDashboard:
logger.warning(f"Error adding trades to chart: {e}")
def _add_pivot_points_to_chart(self, fig: go.Figure, symbol: str, df_main: pd.DataFrame, row: int = 1):
"""Add Williams Market Structure pivot points (all 5 levels) to the chart"""
"""Add Williams Market Structure pivot points (all 5 levels) to the chart
Returns HTML children for external legend display.
"""
try:
# Get pivot bounds from data provider
if not hasattr(self, 'data_provider') or not self.data_provider:
logger.debug("No data provider available for pivots")
return
# Get Williams pivot levels with trend analysis
pivot_levels = self.data_provider.get_williams_pivot_levels(symbol)
try:
pivot_levels = self.data_provider.get_williams_pivot_levels(symbol)
except Exception as e:
logger.warning(f"Error getting Williams pivot levels: {e}")
return
if not pivot_levels:
logger.debug(f"No Williams pivot levels available for {symbol}")
return
@@ -4457,55 +4447,30 @@ class CleanTradingDashboard:
row=row, col=1
)
# Add multi-level trend analysis annotation
if pivot_levels:
# Build trend summary from all levels
trend_lines = []
for level_num in sorted(pivot_levels.keys()):
trend_level = pivot_levels[level_num]
if hasattr(trend_level, 'trend_direction') and hasattr(trend_level, 'trend_strength'):
direction = trend_level.trend_direction
strength = trend_level.trend_strength
# Format direction
direction_emoji = {
'up': '',
'down': '',
'sideways': ''
}.get(direction, '?')
trend_lines.append(f"L{level_num}: {direction_emoji} {strength:.0%}")
if trend_lines:
# Determine overall trend color from Level 5 (longest-term)
overall_trend = 'sideways'
if 5 in pivot_levels and hasattr(pivot_levels[5], 'trend_direction'):
overall_trend = pivot_levels[5].trend_direction
trend_color = {
'up': 'rgba(0, 255, 0, 0.8)',
'down': 'rgba(255, 0, 0, 0.8)',
'sideways': 'rgba(255, 165, 0, 0.8)'
}.get(overall_trend, 'rgba(128, 128, 128, 0.8)')
fig.add_annotation(
xref="paper", yref="paper",
x=0.02, y=0.98,
text="<br>".join(["Williams Trends:"] + trend_lines),
showarrow=False,
bgcolor="rgba(0,0,0,0.85)",
bordercolor=trend_color,
borderwidth=2,
borderpad=6,
font=dict(color="white", size=9, family="monospace"),
align="left",
row=row, col=1
)
# Build external legend HTML (no annotation on chart to avoid scale distortion)
legend_children = []
try:
if pivot_levels:
from dash import html
trend_rows = []
for level_num in sorted(pivot_levels.keys()):
tl = pivot_levels[level_num]
direction = getattr(tl, 'trend_direction', 'sideways')
strength = getattr(tl, 'trend_strength', 0.0)
arrow = {'up': '', 'down': '', 'sideways': ''}.get(direction, '?')
color = {'up': 'text-success', 'down': 'text-danger', 'sideways': 'text-warning'}.get(direction, 'text-muted')
trend_rows.append(html.Span([f"L{level_num}: ", html.Span(f"{arrow} {strength:.0%}", className=color)], className="me-3"))
if trend_rows:
legend_children = [html.Div([html.Strong("Williams Trends:"), html.Span(" "), *trend_rows])]
except Exception:
legend_children = []
logger.debug(f"Added {len(pivot_levels)} Williams pivot levels to chart")
return legend_children
except Exception as e:
logger.warning(f"Error adding pivot points to chart: {e}")
return []
def _get_price_at_time(self, df: pd.DataFrame, timestamp) -> Optional[float]:
"""Get price from dataframe at specific timestamp"""