fix pivots display
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -604,6 +604,10 @@ class DashboardLayoutManager:
|
||||
html.Div([
|
||||
dcc.Graph(id="price-chart", style={"height": "500px"})
|
||||
]),
|
||||
html.Div(
|
||||
id="williams-trend-legend",
|
||||
className="text-muted small mb-2"
|
||||
),
|
||||
html.Hr(className="my-2"),
|
||||
html.Div([
|
||||
html.H6([
|
||||
|
||||
Reference in New Issue
Block a user