chart rewrite - better and working

This commit is contained in:
Dobromir Popov
2025-06-24 22:39:23 +03:00
parent ab8c94d735
commit 9d843b7550
5 changed files with 515 additions and 24 deletions

View File

@ -309,7 +309,9 @@ class TradingDashboard:
self.closed_trades = [] # List of all closed trades with full details
# Load existing closed trades from file
logger.info("DASHBOARD: Loading closed trades from file...")
self._load_closed_trades_from_file()
logger.info(f"DASHBOARD: Loaded {len(self.closed_trades)} closed trades")
# Signal execution settings for scalping - REMOVED FREQUENCY LIMITS
self.min_confidence_threshold = 0.30 # Start lower to allow learning
@ -840,6 +842,7 @@ class TradingDashboard:
], className="card-body text-center p-2")
], className="card bg-light", style={"height": "60px"}),
], style={"display": "grid", "gridTemplateColumns": "repeat(4, 1fr)", "gap": "8px", "width": "60%"}),
# Right side - Merged: Recent Signals & Model Training - 2 columns
html.Div([
@ -869,13 +872,28 @@ class TradingDashboard:
# Charts row - Now full width since training moved up
html.Div([
# Price chart - Full width
# Price chart - Full width with manual trading buttons
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-chart-candlestick me-2"),
"Live 1s Price & Volume Chart (WebSocket Stream)"
], className="card-title mb-2"),
# Chart header with manual trading buttons
html.Div([
html.H6([
html.I(className="fas fa-chart-candlestick me-2"),
"Live 1s Price & Volume Chart (WebSocket Stream)"
], className="card-title mb-0"),
html.Div([
html.Button([
html.I(className="fas fa-arrow-up me-1"),
"BUY"
], id="manual-buy-btn", className="btn btn-success btn-sm me-2",
style={"fontSize": "10px", "padding": "2px 8px"}),
html.Button([
html.I(className="fas fa-arrow-down me-1"),
"SELL"
], id="manual-sell-btn", className="btn btn-danger btn-sm",
style={"fontSize": "10px", "padding": "2px 8px"})
], className="d-flex")
], className="d-flex justify-content-between align-items-center mb-2"),
dcc.Graph(id="price-chart", style={"height": "400px"})
], className="card-body p-2")
], className="card", style={"width": "100%"}),
@ -1172,25 +1190,30 @@ class TradingDashboard:
]
position_class = "fw-bold mb-0 small"
else:
position_text = "No Position"
position_class = "text-muted mb-0 small"
# Show HOLD when no position is open
from dash import html
position_text = [
html.Span("[HOLD] ", className="text-warning fw-bold"),
html.Span("No Position - Waiting for Signal", className="text-muted")
]
position_class = "fw-bold mb-0 small"
# MEXC status (simple)
mexc_status = "LIVE" if (self.trading_executor and self.trading_executor.trading_enabled and not self.trading_executor.simulation_mode) else "SIM"
# CHART OPTIMIZATION - Real-time chart updates every 1 second
# OPTIMIZED CHART - Using new optimized version with trade caching
if is_chart_update:
try:
if hasattr(self, '_cached_chart_data_time'):
cache_time = self._cached_chart_data_time
if time.time() - cache_time < 5: # Use cached chart if < 5s old for faster updates
if time.time() - cache_time < 3: # Use cached chart if < 3s old for faster updates
price_chart = getattr(self, '_cached_price_chart', None)
else:
price_chart = self._create_price_chart_optimized(symbol, current_price)
price_chart = self._create_price_chart_optimized_v2(symbol)
self._cached_price_chart = price_chart
self._cached_chart_data_time = time.time()
else:
price_chart = self._create_price_chart_optimized(symbol, current_price)
price_chart = self._create_price_chart_optimized_v2(symbol)
self._cached_price_chart = price_chart
self._cached_chart_data_time = time.time()
except Exception as e:
@ -1382,6 +1405,83 @@ class TradingDashboard:
except Exception as e:
logger.error(f"Error updating leverage: {e}")
return f"{self.leverage_multiplier:.0f}x", "Error", "badge bg-secondary"
# Manual Buy button callback
@self.app.callback(
Output('recent-decisions', 'children', allow_duplicate=True),
[Input('manual-buy-btn', 'n_clicks')],
prevent_initial_call=True
)
def manual_buy(n_clicks):
"""Execute manual buy order"""
if n_clicks and n_clicks > 0:
try:
symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT"
current_price = self.get_realtime_price(symbol) or 2434.0
# Create manual trading decision
manual_decision = {
'action': 'BUY',
'symbol': symbol,
'price': current_price,
'size': 0.001, # Small test size (max 1 lot)
'confidence': 1.0, # Manual trades have 100% confidence
'timestamp': datetime.now(),
'source': 'MANUAL_BUY',
'mexc_executed': False, # Mark as manual/test trade
'usd_size': current_price * 0.001
}
# Process the trading decision
self._process_trading_decision(manual_decision)
logger.info(f"MANUAL: BUY executed at ${current_price:.2f}")
return dash.no_update
except Exception as e:
logger.error(f"Error executing manual buy: {e}")
return dash.no_update
return dash.no_update
# Manual Sell button callback
@self.app.callback(
Output('recent-decisions', 'children', allow_duplicate=True),
[Input('manual-sell-btn', 'n_clicks')],
prevent_initial_call=True
)
def manual_sell(n_clicks):
"""Execute manual sell order"""
if n_clicks and n_clicks > 0:
try:
symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT"
current_price = self.get_realtime_price(symbol) or 2434.0
# Create manual trading decision
manual_decision = {
'action': 'SELL',
'symbol': symbol,
'price': current_price,
'size': 0.001, # Small test size (max 1 lot)
'confidence': 1.0, # Manual trades have 100% confidence
'timestamp': datetime.now(),
'source': 'MANUAL_SELL',
'mexc_executed': False, # Mark as manual/test trade
'usd_size': current_price * 0.001
}
# Process the trading decision
self._process_trading_decision(manual_decision)
logger.info(f"MANUAL: SELL executed at ${current_price:.2f}")
return dash.no_update
except Exception as e:
logger.error(f"Error executing manual sell: {e}")
return dash.no_update
return dash.no_update
def _simulate_price_update(self, symbol: str, base_price: float) -> float:
"""
@ -1439,19 +1539,18 @@ class TradingDashboard:
"""Create price chart with volume and Williams pivot points from cached data"""
try:
# For Williams Market Structure, we need 1s data for proper recursive analysis
# Get 5 minutes (300 seconds) of 1s data for accurate pivot calculation
# Get 4 hours (240 minutes) of 1m data for better trade visibility
df_1s = None
df_1m = None
# Try to get 1s data first for Williams analysis
# Try to get 1s data first for Williams analysis (reduced to 10 minutes for performance)
try:
df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=300, refresh=False)
df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=600, refresh=False)
if df_1s is None or df_1s.empty:
logger.warning("[CHART] No 1s cached data available, trying fresh 1s data")
df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=300, refresh=True)
if df_1s is not None and not df_1s.empty:
logger.debug(f"[CHART] Using {len(df_1s)} 1s bars for Williams analysis")
# Aggregate 1s data to 1m for chart display (cleaner visualization)
df = self._aggregate_1s_to_1m(df_1s)
actual_timeframe = '1s→1m'
@ -1461,14 +1560,14 @@ class TradingDashboard:
logger.warning(f"[CHART] Error getting 1s data: {e}")
df_1s = None
# Fallback to 1m data if 1s not available
# Fallback to 1m data if 1s not available (4 hours for historical trades)
if df_1s is None:
df = self.data_provider.get_historical_data(symbol, '1m', limit=30, refresh=False)
df = self.data_provider.get_historical_data(symbol, '1m', limit=240, refresh=False)
if df is None or df.empty:
logger.warning("[CHART] No cached 1m data available, trying fresh 1m data")
try:
df = self.data_provider.get_historical_data(symbol, '1m', limit=30, refresh=True)
df = self.data_provider.get_historical_data(symbol, '1m', limit=240, refresh=True)
if df is not None and not df.empty:
# Ensure timezone consistency for fresh data
df = self._ensure_timezone_consistency(df)
@ -1491,7 +1590,6 @@ class TradingDashboard:
# Ensure timezone consistency for cached data
df = self._ensure_timezone_consistency(df)
actual_timeframe = '1m'
logger.debug(f"[CHART] Using {len(df)} 1m bars from cached data in {self.timezone}")
# Final check: ensure we have valid data with proper index
if df is None or df.empty:
@ -1542,9 +1640,7 @@ class TradingDashboard:
pivot_points = self._get_williams_pivot_points_for_chart(williams_data, chart_df=df)
if pivot_points:
self._add_williams_pivot_points_to_chart(fig, pivot_points, row=1)
logger.info(f"[CHART] Added Williams pivot points using {actual_timeframe} data")
else:
logger.debug("[CHART] No Williams pivot points calculated")
logger.debug(f"[CHART] Added Williams pivot points using {actual_timeframe} data")
except Exception as e:
logger.debug(f"Error adding Williams pivot points to chart: {e}")
@ -1632,7 +1728,7 @@ class TradingDashboard:
elif decision['action'] == 'SELL':
sell_decisions.append((decision, signal_type))
logger.debug(f"[CHART] Showing {len(buy_decisions)} BUY and {len(sell_decisions)} SELL signals in chart timeframe")
# Add BUY markers with different styles for executed vs ignored
executed_buys = [d[0] for d in buy_decisions if d[1] == 'EXECUTED']
@ -1766,7 +1862,9 @@ class TradingDashboard:
if (chart_start_utc <= entry_time_pd <= chart_end_utc) or (chart_start_utc <= exit_time_pd <= chart_end_utc):
chart_trades.append(trade)
logger.debug(f"[CHART] Showing {len(chart_trades)} closed trades on chart")
# Minimal logging - only show count
if len(chart_trades) > 0:
logger.debug(f"[CHART] Showing {len(chart_trades)} trades on chart")
# Plot closed trades with profit/loss styling
profitable_entries_x = []
@ -2926,9 +3024,12 @@ class TradingDashboard:
import json
from pathlib import Path
logger.info("LOAD_TRADES: Checking for closed_trades_history.json...")
if Path('closed_trades_history.json').exists():
logger.info("LOAD_TRADES: File exists, loading...")
with open('closed_trades_history.json', 'r') as f:
trades_data = json.load(f)
logger.info(f"LOAD_TRADES: Raw data loaded: {len(trades_data)} trades")
# Convert string dates back to datetime objects
for trade in trades_data:
@ -6035,6 +6136,188 @@ class TradingDashboard:
except Exception as e:
logger.debug(f"Signal processing error: {e}")
def _create_price_chart_optimized_v2(self, symbol: str) -> go.Figure:
"""OPTIMIZED: Create price chart with cached trade filtering and minimal logging"""
try:
chart_start = time.time()
# STEP 1: Get chart data with minimal API calls
df = None
actual_timeframe = '1m'
# Try cached 1m data first (fastest)
df = self.data_provider.get_historical_data(symbol, '1m', limit=120, refresh=False)
if df is None or df.empty:
# Fallback to fresh data only if needed
df = self.data_provider.get_historical_data(symbol, '1m', limit=120, refresh=True)
if df is None or df.empty:
return self._create_empty_chart(f"{symbol} Chart", "No data available")
# STEP 2: Ensure proper timezone (cached result)
if not hasattr(self, '_tz_cache_time') or time.time() - self._tz_cache_time > 300: # 5min cache
df = self._ensure_timezone_consistency(df)
self._tz_cache_time = time.time()
# STEP 3: Create base chart quickly
fig = make_subplots(
rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1,
subplot_titles=(f'{symbol} Price ({actual_timeframe.upper()})', 'Volume'),
row_heights=[0.7, 0.3]
)
# STEP 4: Add price line (main trace)
fig.add_trace(
go.Scatter(
x=df.index, y=df['close'], mode='lines', name=f"{symbol} Price",
line=dict(color='#00ff88', width=2),
hovertemplate='<b>$%{y:.2f}</b><br>%{x}<extra></extra>'
), row=1, col=1
)
# STEP 5: Add volume (if available)
if 'volume' in df.columns:
fig.add_trace(
go.Bar(x=df.index, y=df['volume'], name='Volume',
marker_color='rgba(158, 158, 158, 0.6)'), row=2, col=1
)
# STEP 6: OPTIMIZED TRADE VISUALIZATION - with caching
if self.closed_trades:
# Cache trade filtering results for 30 seconds
cache_key = f"trades_{len(self.closed_trades)}_{df.index.min()}_{df.index.max()}"
if (not hasattr(self, '_trade_cache') or
self._trade_cache.get('key') != cache_key or
time.time() - self._trade_cache.get('time', 0) > 30):
# Filter trades to chart timeframe (expensive operation)
chart_start_utc = df.index.min().tz_localize(None) if df.index.min().tz else df.index.min()
chart_end_utc = df.index.max().tz_localize(None) if df.index.max().tz else df.index.max()
chart_trades = []
for trade in self.closed_trades:
if not isinstance(trade, dict):
continue
entry_time = trade.get('entry_time')
exit_time = trade.get('exit_time')
if not entry_time or not exit_time:
continue
# Quick timezone conversion
try:
if isinstance(entry_time, datetime):
entry_utc = entry_time.replace(tzinfo=None) if not entry_time.tzinfo else entry_time.astimezone(timezone.utc).replace(tzinfo=None)
else:
continue
if isinstance(exit_time, datetime):
exit_utc = exit_time.replace(tzinfo=None) if not exit_time.tzinfo else exit_time.astimezone(timezone.utc).replace(tzinfo=None)
else:
continue
# Check if trade overlaps with chart
entry_pd = pd.to_datetime(entry_utc)
exit_pd = pd.to_datetime(exit_utc)
if (chart_start_utc <= entry_pd <= chart_end_utc) or (chart_start_utc <= exit_pd <= chart_end_utc):
chart_trades.append(trade)
except:
continue # Skip problematic trades
# Cache the result
self._trade_cache = {
'key': cache_key,
'time': time.time(),
'trades': chart_trades
}
else:
# Use cached trades
chart_trades = self._trade_cache['trades']
# STEP 7: Render trade markers (optimized)
if chart_trades:
profitable_entries_x, profitable_entries_y = [], []
profitable_exits_x, profitable_exits_y = [], []
for trade in chart_trades:
entry_price = trade.get('entry_price', 0)
exit_price = trade.get('exit_price', 0)
entry_time = trade.get('entry_time')
exit_time = trade.get('exit_time')
net_pnl = trade.get('net_pnl', 0)
if not all([entry_price, exit_price, entry_time, exit_time]):
continue
# Convert to local time for display
entry_local = self._to_local_timezone(entry_time)
exit_local = self._to_local_timezone(exit_time)
# Only show profitable trades as filled markers (cleaner UI)
if net_pnl > 0:
profitable_entries_x.append(entry_local)
profitable_entries_y.append(entry_price)
profitable_exits_x.append(exit_local)
profitable_exits_y.append(exit_price)
# Add connecting line for all trades
line_color = '#00ff88' if net_pnl > 0 else '#ff6b6b'
fig.add_trace(
go.Scatter(
x=[entry_local, exit_local], y=[entry_price, exit_price],
mode='lines', line=dict(color=line_color, width=2, dash='dash'),
name="Trade", showlegend=False, hoverinfo='skip'
), row=1, col=1
)
# Add profitable trade markers
if profitable_entries_x:
fig.add_trace(
go.Scatter(
x=profitable_entries_x, y=profitable_entries_y, mode='markers',
marker=dict(color='#00ff88', size=12, symbol='triangle-up',
line=dict(color='white', width=1)),
name="Profitable Entry", showlegend=True,
hovertemplate="<b>ENTRY</b><br>$%{y:.2f}<br>%{x}<extra></extra>"
), row=1, col=1
)
if profitable_exits_x:
fig.add_trace(
go.Scatter(
x=profitable_exits_x, y=profitable_exits_y, mode='markers',
marker=dict(color='#00ff88', size=12, symbol='triangle-down',
line=dict(color='white', width=1)),
name="Profitable Exit", showlegend=True,
hovertemplate="<b>EXIT</b><br>$%{y:.2f}<br>%{x}<extra></extra>"
), row=1, col=1
)
# STEP 8: Update layout efficiently
latest_price = df['close'].iloc[-1] if not df.empty else 0
current_time = datetime.now().strftime("%H:%M:%S")
fig.update_layout(
title=f"{symbol} | ${latest_price:.2f} | {current_time}",
template="plotly_dark", height=400, xaxis_rangeslider_visible=False,
margin=dict(l=20, r=20, t=50, b=20),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
fig.update_yaxes(title_text="Price ($)", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
# Performance logging (minimal)
chart_time = (time.time() - chart_start) * 1000
if chart_time > 200: # Only log slow charts
logger.warning(f"[CHART] Slow chart render: {chart_time:.0f}ms")
return fig
except Exception as e:
logger.error(f"Optimized chart error: {e}")
return self._create_empty_chart(f"{symbol} Chart", f"Chart Error: {str(e)}")
def _create_price_chart_optimized(self, symbol, current_price):
"""Optimized chart creation with minimal data fetching"""
try: