chart rewrite - better and working
This commit is contained in:
@ -122,5 +122,67 @@
|
||||
"wandb_run_id": null,
|
||||
"wandb_artifact_name": null
|
||||
}
|
||||
],
|
||||
"extrema_trainer": [
|
||||
{
|
||||
"checkpoint_id": "extrema_trainer_20250624_221645",
|
||||
"model_name": "extrema_trainer",
|
||||
"model_type": "extrema_trainer",
|
||||
"file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_221645.pt",
|
||||
"created_at": "2025-06-24T22:16:45.728299",
|
||||
"file_size_mb": 0.0013427734375,
|
||||
"performance_score": 0.1,
|
||||
"accuracy": 0.0,
|
||||
"loss": null,
|
||||
"val_accuracy": null,
|
||||
"val_loss": null,
|
||||
"reward": null,
|
||||
"pnl": null,
|
||||
"epoch": null,
|
||||
"training_time_hours": null,
|
||||
"total_parameters": null,
|
||||
"wandb_run_id": null,
|
||||
"wandb_artifact_name": null
|
||||
},
|
||||
{
|
||||
"checkpoint_id": "extrema_trainer_20250624_221915",
|
||||
"model_name": "extrema_trainer",
|
||||
"model_type": "extrema_trainer",
|
||||
"file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_221915.pt",
|
||||
"created_at": "2025-06-24T22:19:15.325368",
|
||||
"file_size_mb": 0.0013427734375,
|
||||
"performance_score": 0.1,
|
||||
"accuracy": 0.0,
|
||||
"loss": null,
|
||||
"val_accuracy": null,
|
||||
"val_loss": null,
|
||||
"reward": null,
|
||||
"pnl": null,
|
||||
"epoch": null,
|
||||
"training_time_hours": null,
|
||||
"total_parameters": null,
|
||||
"wandb_run_id": null,
|
||||
"wandb_artifact_name": null
|
||||
},
|
||||
{
|
||||
"checkpoint_id": "extrema_trainer_20250624_222303",
|
||||
"model_name": "extrema_trainer",
|
||||
"model_type": "extrema_trainer",
|
||||
"file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_222303.pt",
|
||||
"created_at": "2025-06-24T22:23:03.283194",
|
||||
"file_size_mb": 0.0013427734375,
|
||||
"performance_score": 0.1,
|
||||
"accuracy": 0.0,
|
||||
"loss": null,
|
||||
"val_accuracy": null,
|
||||
"val_loss": null,
|
||||
"reward": null,
|
||||
"pnl": null,
|
||||
"epoch": null,
|
||||
"training_time_hours": null,
|
||||
"total_parameters": null,
|
||||
"wandb_run_id": null,
|
||||
"wandb_artifact_name": null
|
||||
}
|
||||
]
|
||||
}
|
@ -16,6 +16,14 @@ do we load the best model for each model type? or we do a cold start each time?
|
||||
we stopped showing executed trades on the chart. let's add them back
|
||||
.
|
||||
update chart every second as well.
|
||||
the list with closed trades is not updated. clear session button does not clear all data.
|
||||
|
||||
add buttons for quick manual buy/sell (max 1 lot. sell closes long, buy closes short if already open position exists)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
>> Training
|
||||
|
||||
|
50
add_current_trade.py
Normal file
50
add_current_trade.py
Normal file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
def add_current_trade():
|
||||
"""Add a trade with current timestamp for immediate visibility"""
|
||||
now = datetime.now()
|
||||
|
||||
# Create a trade that just happened
|
||||
current_trade = {
|
||||
'trade_id': 999,
|
||||
'symbol': 'ETHUSDT',
|
||||
'side': 'LONG',
|
||||
'entry_time': (now - timedelta(seconds=30)).isoformat(), # 30 seconds ago
|
||||
'exit_time': now.isoformat(), # Just now
|
||||
'entry_price': 2434.50,
|
||||
'exit_price': 2434.70,
|
||||
'size': 0.001,
|
||||
'fees': 0.05,
|
||||
'net_pnl': 0.15, # Small profit
|
||||
'mexc_executed': True,
|
||||
'duration_seconds': 30,
|
||||
'leverage': 50.0,
|
||||
'gross_pnl': 0.20,
|
||||
'fee_type': 'TAKER',
|
||||
'fee_rate': 0.0005
|
||||
}
|
||||
|
||||
# Load existing trades
|
||||
try:
|
||||
with open('closed_trades_history.json', 'r') as f:
|
||||
trades = json.load(f)
|
||||
except:
|
||||
trades = []
|
||||
|
||||
# Add the current trade
|
||||
trades.append(current_trade)
|
||||
|
||||
# Save back
|
||||
with open('closed_trades_history.json', 'w') as f:
|
||||
json.dump(trades, f, indent=2)
|
||||
|
||||
print(f"✅ Added current trade: LONG @ {current_trade['entry_time']} -> {current_trade['exit_time']}")
|
||||
print(f" Entry: ${current_trade['entry_price']} | Exit: ${current_trade['exit_price']} | P&L: ${current_trade['net_pnl']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
from datetime import timedelta
|
||||
add_current_trade()
|
88
test_manual_trading.py
Normal file
88
test_manual_trading.py
Normal file
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for manual trading buttons functionality
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
def test_manual_trading():
|
||||
"""Test the manual trading buttons functionality"""
|
||||
print("Testing manual trading buttons...")
|
||||
|
||||
# Check if dashboard is running
|
||||
try:
|
||||
response = requests.get("http://127.0.0.1:8050", timeout=5)
|
||||
if response.status_code == 200:
|
||||
print("✅ Dashboard is running on port 8050")
|
||||
else:
|
||||
print(f"❌ Dashboard returned status code: {response.status_code}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"❌ Dashboard not accessible: {e}")
|
||||
return
|
||||
|
||||
# Check if trades file exists
|
||||
try:
|
||||
with open('closed_trades_history.json', 'r') as f:
|
||||
trades = json.load(f)
|
||||
print(f"📊 Current trades in history: {len(trades)}")
|
||||
if trades:
|
||||
latest_trade = trades[-1]
|
||||
print(f" Latest trade: {latest_trade.get('side')} at ${latest_trade.get('exit_price', latest_trade.get('entry_price'))}")
|
||||
except FileNotFoundError:
|
||||
print("📊 No trades history file found (this is normal for fresh start)")
|
||||
except Exception as e:
|
||||
print(f"❌ Error reading trades file: {e}")
|
||||
|
||||
print("\n🎯 Manual Trading Test Instructions:")
|
||||
print("1. Open dashboard at http://127.0.0.1:8050")
|
||||
print("2. Look for the 'MANUAL BUY' and 'MANUAL SELL' buttons")
|
||||
print("3. Click 'MANUAL BUY' to create a test long position")
|
||||
print("4. Wait a few seconds, then click 'MANUAL SELL' to close and create short")
|
||||
print("5. Check the chart for green triangles showing trade entry/exit points")
|
||||
print("6. Check the 'Closed Trades' table for trade records")
|
||||
|
||||
print("\n📈 Expected Results:")
|
||||
print("- Green triangles should appear on the price chart at trade execution times")
|
||||
print("- Dashed lines should connect entry and exit points")
|
||||
print("- Trade records should appear in the closed trades table")
|
||||
print("- Session P&L should update with trade profits/losses")
|
||||
|
||||
print("\n🔍 Monitoring trades file...")
|
||||
initial_count = 0
|
||||
try:
|
||||
with open('closed_trades_history.json', 'r') as f:
|
||||
initial_count = len(json.load(f))
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"Initial trade count: {initial_count}")
|
||||
print("Watching for new trades... (Press Ctrl+C to stop)")
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(2)
|
||||
try:
|
||||
with open('closed_trades_history.json', 'r') as f:
|
||||
current_trades = json.load(f)
|
||||
current_count = len(current_trades)
|
||||
|
||||
if current_count > initial_count:
|
||||
new_trades = current_trades[initial_count:]
|
||||
for trade in new_trades:
|
||||
print(f"🆕 NEW TRADE: {trade.get('side')} | Entry: ${trade.get('entry_price'):.2f} | Exit: ${trade.get('exit_price'):.2f} | P&L: ${trade.get('net_pnl'):.2f}")
|
||||
initial_count = current_count
|
||||
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error monitoring trades: {e}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n✅ Test monitoring stopped")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_manual_trading()
|
325
web/dashboard.py
325
web/dashboard.py
@ -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
|
||||
@ -841,6 +843,7 @@ class TradingDashboard:
|
||||
], 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([
|
||||
# Recent Trading Signals Column (50%)
|
||||
@ -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([
|
||||
# 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-2"),
|
||||
], 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:
|
||||
@ -1383,6 +1406,83 @@ class TradingDashboard:
|
||||
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:
|
||||
"""
|
||||
Create realistic price movement for demo purposes
|
||||
@ -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:
|
||||
|
Reference in New Issue
Block a user