OmidSakaki commited on
Commit
8b48d05
·
verified ·
1 Parent(s): 2705a8c

Update src/visualizers/chart_renderer.py

Browse files
Files changed (1) hide show
  1. src/visualizers/chart_renderer.py +376 -182
src/visualizers/chart_renderer.py CHANGED
@@ -1,216 +1,410 @@
1
  import plotly.graph_objects as go
2
  from plotly.subplots import make_subplots
 
3
  import numpy as np
 
 
 
 
 
 
 
 
4
 
5
  class ChartRenderer:
6
- def __init__(self):
7
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- def render_price_chart(self, prices, actions=None, current_step=0):
10
- """Render price chart with actions"""
 
 
 
 
11
  fig = go.Figure()
 
12
 
13
- if not prices:
14
- # Return empty figure if no data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  fig.update_layout(
16
- title="Price Chart - No Data Available",
 
 
 
 
 
17
  xaxis_title="Time Step",
18
- yaxis_title="Price",
19
- height=300,
20
- template="plotly_white"
 
 
21
  )
22
- return fig
23
-
24
- # Add price line
25
- fig.add_trace(go.Scatter(
26
- x=list(range(len(prices))),
27
- y=prices,
28
- mode='lines',
29
- name='Price',
30
- line=dict(color='blue', width=2)
31
- ))
32
-
33
- # Add action markers if provided
34
- if actions and len(actions) == len(prices):
35
- buy_indices = [i for i, action in enumerate(actions) if action == 1]
36
- sell_indices = [i for i, action in enumerate(actions) if action == 2]
37
- close_indices = [i for i, action in enumerate(actions) if action == 3]
38
 
39
- if buy_indices:
40
- fig.add_trace(go.Scatter(
41
- x=buy_indices,
42
- y=[prices[i] for i in buy_indices],
43
- mode='markers',
44
- name='Buy',
45
- marker=dict(color='green', size=10, symbol='triangle-up',
46
- line=dict(width=2, color='darkgreen'))
47
- ))
48
 
49
- if sell_indices:
50
- fig.add_trace(go.Scatter(
51
- x=sell_indices,
52
- y=[prices[i] for i in sell_indices],
53
- mode='markers',
54
- name='Sell',
55
- marker=dict(color='red', size=10, symbol='triangle-down',
56
- line=dict(width=2, color='darkred'))
57
- ))
58
 
59
- if close_indices:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  fig.add_trace(go.Scatter(
61
- x=close_indices,
62
- y=[prices[i] for i in close_indices],
63
  mode='markers',
64
- name='Close',
65
- marker=dict(color='orange', size=8, symbol='x',
66
- line=dict(width=2, color='darkorange'))
 
 
 
 
 
 
67
  ))
68
-
69
- fig.update_layout(
70
- title=f"Price Chart (Step: {current_step})",
71
- xaxis_title="Time Step",
72
- yaxis_title="Price",
73
- height=300,
74
- showlegend=True,
75
- template="plotly_white"
76
- )
77
-
78
- return fig
79
 
80
- def create_performance_chart(self, net_worth_history, reward_history, initial_balance):
81
- """Create portfolio performance chart"""
82
- fig = make_subplots(
83
- rows=2, cols=1,
84
- subplot_titles=['Portfolio Value Over Time', 'Step Rewards'],
85
- vertical_spacing=0.15
86
- )
87
 
88
- if not net_worth_history:
89
- fig.update_layout(title="No Data Available", height=400)
90
- return fig
91
 
92
- # Portfolio value
93
- fig.add_trace(go.Scatter(
94
- x=list(range(len(net_worth_history))),
95
- y=net_worth_history,
96
- mode='lines+markers',
97
- name='Net Worth',
98
- line=dict(color='green', width=3),
99
- marker=dict(size=4)
100
- ), row=1, col=1)
101
-
102
- # Add initial balance reference line
103
- fig.add_hline(y=initial_balance, line_dash="dash",
104
- line_color="red", annotation_text="Initial Balance",
105
- row=1, col=1)
106
-
107
- # Rewards as bar chart
108
- if reward_history:
109
- fig.add_trace(go.Bar(
110
- x=list(range(len(reward_history))),
111
- y=reward_history,
112
- name='Reward',
113
- marker_color=['green' if r >= 0 else 'red' for r in reward_history],
114
- opacity=0.7
115
- ), row=2, col=1)
116
-
117
- fig.update_layout(height=500, showlegend=False, template="plotly_white")
118
- fig.update_yaxes(title_text="Value ($)", row=1, col=1)
119
- fig.update_yaxes(title_text="Reward", row=2, col=1)
120
- fig.update_xaxes(title_text="Step", row=2, col=1)
121
-
122
- return fig
123
 
124
- def create_action_distribution(self, actions):
125
- """Create action distribution pie chart"""
126
- fig = go.Figure()
 
 
 
127
 
128
- if not actions:
129
- fig.update_layout(title="No Actions Available", height=300)
130
- return fig
131
-
132
- action_names = ['Hold', 'Buy', 'Sell', 'Close']
133
- action_counts = [actions.count(i) for i in range(4)]
134
-
135
- colors = ['blue', 'green', 'red', 'orange']
136
-
137
- fig = go.Figure(data=[go.Pie(
138
- labels=action_names,
139
- values=action_counts,
140
- hole=.4,
141
- marker_colors=colors,
142
- textinfo='label+percent+value',
143
- hoverinfo='label+percent+value'
144
- )])
145
-
146
- fig.update_layout(
147
- title="Action Distribution",
148
- height=350,
149
- annotations=[dict(text='Actions', x=0.5, y=0.5, font_size=16, showarrow=False)],
150
- template="plotly_white"
151
- )
152
 
153
- return fig
154
-
155
- def create_training_progress(self, training_history):
156
- """Create training progress visualization"""
157
- if not training_history:
158
- fig = go.Figure()
159
- fig.update_layout(title="No Training Data Available", height=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  return fig
 
 
 
 
 
 
 
 
 
 
161
 
162
- episodes = [h['episode'] for h in training_history]
163
- rewards = [h['reward'] for h in training_history]
164
- net_worths = [h['net_worth'] for h in training_history]
165
- losses = [h.get('loss', 0) for h in training_history]
166
-
167
- fig = make_subplots(
168
- rows=2, cols=2,
169
- subplot_titles=['Episode Rewards', 'Portfolio Value',
170
- 'Training Loss', 'Moving Average Reward (5)'],
171
- specs=[[{}, {}], [{}, {}]]
172
- )
173
 
174
- # Rewards
175
- fig.add_trace(go.Scatter(
176
- x=episodes, y=rewards, mode='lines+markers',
177
- name='Reward', line=dict(color='blue', width=2),
178
- marker=dict(size=4)
179
- ), row=1, col=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- # Portfolio value
182
- fig.add_trace(go.Scatter(
183
- x=episodes, y=net_worths, mode='lines+markers',
184
- name='Net Worth', line=dict(color='green', width=2),
185
- marker=dict(size=4)
186
- ), row=1, col=2)
187
 
188
- # Loss
189
- if any(loss > 0 for loss in losses):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  fig.add_trace(go.Scatter(
191
- x=episodes, y=losses, mode='lines+markers',
192
- name='Loss', line=dict(color='red', width=2),
193
  marker=dict(size=4)
194
- ), row=2, col=1)
195
-
196
- # Moving average reward
197
- if len(rewards) > 5:
198
- ma_rewards = []
199
- for i in range(len(rewards)):
200
- start_idx = max(0, i - 4)
201
- ma = np.mean(rewards[start_idx:i+1])
202
- ma_rewards.append(ma)
203
 
 
204
  fig.add_trace(go.Scatter(
205
- x=episodes, y=ma_rewards, mode='lines',
206
- name='MA Reward (5)', line=dict(color='orange', width=3, dash='dash')
207
- ), row=2, col=2)
208
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  fig.update_layout(
210
- height=600,
211
- showlegend=True,
212
- title_text="Training Progress Over Episodes",
213
- template="plotly_white"
214
  )
215
-
216
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import plotly.graph_objects as go
2
  from plotly.subplots import make_subplots
3
+ import plotly.express as px
4
  import numpy as np
5
+ import pandas as pd
6
+ from typing import List, Dict, Any, Optional, Union
7
+ import logging
8
+ from datetime import datetime
9
+ import warnings
10
+ warnings.filterwarnings('ignore')
11
+
12
+ logger = logging.getLogger(__name__)
13
 
14
  class ChartRenderer:
15
+ """Advanced chart renderer for trading visualizations with error handling"""
16
+
17
+ def __init__(self, theme: str = "plotly_white", default_height: int = 400):
18
+ self.theme = theme
19
+ self.default_height = default_height
20
+ self._validate_plotly()
21
+
22
+ def _validate_plotly(self):
23
+ """Validate Plotly installation and capabilities"""
24
+ try:
25
+ import plotly
26
+ logger.info(f"Plotly version: {plotly.__version__}")
27
+ except ImportError:
28
+ raise ImportError("Plotly is required for ChartRenderer")
29
+
30
+ def _safe_data_validation(self, data, expected_len: Optional[int] = None,
31
+ data_type: str = "data") -> bool:
32
+ """Validate input data safely"""
33
+ if data is None or len(data) == 0:
34
+ logger.warning(f"No {data_type} provided")
35
+ return False
36
+
37
+ if expected_len and len(data) != expected_len:
38
+ logger.warning(f"{data_type} length mismatch: expected {expected_len}, got {len(data)}")
39
+
40
+ if isinstance(data, (list, np.ndarray)):
41
+ if np.any(np.isnan(data)) or np.any(np.isinf(data)):
42
+ logger.warning(f"{data_type} contains NaN or Inf values")
43
+ return False
44
+
45
+ return True
46
 
47
+ def render_price_chart(self, prices: Union[List[float], np.ndarray],
48
+ actions: Optional[List[int]] = None,
49
+ current_step: int = 0,
50
+ title: Optional[str] = None,
51
+ height: Optional[int] = None) -> go.Figure:
52
+ """Render interactive price chart with trading actions"""
53
  fig = go.Figure()
54
+ height = height or self.default_height
55
 
56
+ # Validate data
57
+ if not self._safe_data_validation(prices, data_type="prices"):
58
+ return self._create_empty_figure("No Price Data", height)
59
+
60
+ try:
61
+ # Convert to numpy for consistency
62
+ prices = np.array(prices, dtype=np.float64)
63
+ time_steps = np.arange(len(prices))
64
+
65
+ # Add main price trace
66
+ fig.add_trace(go.Scatter(
67
+ x=time_steps,
68
+ y=prices,
69
+ mode='lines',
70
+ name='Price',
71
+ line=dict(color='#1f77b4', width=2),
72
+ hovertemplate='<b>Step %{x}</b><br>Price: $%{y:.2f}<extra></extra>'
73
+ ))
74
+
75
+ # Add action markers with validation
76
+ if actions and self._safe_data_validation(actions, len(prices), "actions"):
77
+ self._add_action_markers(fig, prices, actions, time_steps)
78
+
79
+ # Add current step indicator
80
+ if 0 <= current_step < len(prices):
81
+ fig.add_vline(
82
+ x=current_step,
83
+ line_dash="dash",
84
+ line_color="orange",
85
+ annotation_text=f"Current Step ({current_step})",
86
+ annotation_position="top right"
87
+ )
88
+
89
+ # Calculate and add key metrics
90
+ self._add_price_metrics(fig, prices)
91
+
92
+ title = title or f"Asset Price Evolution (Step: {current_step})"
93
  fig.update_layout(
94
+ title={
95
+ 'text': title,
96
+ 'x': 0.5,
97
+ 'xanchor': 'center',
98
+ 'font': {'size': 16}
99
+ },
100
  xaxis_title="Time Step",
101
+ yaxis_title="Price ($)",
102
+ height=height + 100,
103
+ showlegend=True,
104
+ template=self.theme,
105
+ hovermode='x unified'
106
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')
109
+ fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')
 
 
 
 
 
 
 
110
 
111
+ return fig
 
 
 
 
 
 
 
 
112
 
113
+ except Exception as e:
114
+ logger.error(f"Error rendering price chart: {e}")
115
+ return self._create_empty_figure("Error Rendering Price Chart", height)
116
+
117
+ def _add_action_markers(self, fig: go.Figure, prices: np.ndarray,
118
+ actions: List[int], time_steps: np.ndarray):
119
+ """Add buy/sell/close action markers to figure"""
120
+ action_configs = {
121
+ 1: {'name': 'Buy', 'color': '#2ca02c', 'symbol': 'triangle-up'},
122
+ 2: {'name': 'Sell', 'color': '#d62728', 'symbol': 'triangle-down'},
123
+ 3: {'name': 'Close', 'color': '#ff7f0e', 'symbol': 'x'}
124
+ }
125
+
126
+ for action_id, config in action_configs.items():
127
+ indices = [i for i, a in enumerate(actions) if a == action_id]
128
+ if indices:
129
+ action_prices = prices[indices]
130
  fig.add_trace(go.Scatter(
131
+ x=[time_steps[i] for i in indices],
132
+ y=action_prices,
133
  mode='markers',
134
+ name=config['name'],
135
+ marker=dict(
136
+ color=config['color'],
137
+ size=12,
138
+ symbol=config['symbol'],
139
+ line=dict(width=2, color='white')
140
+ ),
141
+ hovertemplate=f'<b>{config["name"]}</b><br>Step: %{{x}}<br>Price: $%{{y:.2f}}<extra></extra>',
142
+ showlegend=True
143
  ))
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ def _add_price_metrics(self, fig: go.Figure, prices: np.ndarray):
146
+ """Add price statistics as annotations"""
147
+ if len(prices) < 2:
148
+ return
 
 
 
149
 
150
+ max_price = np.max(prices)
151
+ min_price = np.min(prices)
152
+ avg_price = np.mean(prices)
153
 
154
+ # Add horizontal reference lines
155
+ fig.add_hline(y=max_price, line_dash="dot", line_color="green",
156
+ annotation_text=f"Max: ${max_price:.2f}")
157
+ fig.add_hline(y=min_price, line_dash="dot", line_color="red",
158
+ annotation_text=f"Min: ${min_price:.2f}")
159
+ fig.add_hline(y=avg_price, line_dash="dash", line_color="blue",
160
+ annotation_text=f"Avg: ${avg_price:.2f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ def create_performance_chart(self, net_worth_history: List[float],
163
+ reward_history: Optional[List[float]] = None,
164
+ initial_balance: float = 10000,
165
+ height: Optional[int] = None) -> go.Figure:
166
+ """Create comprehensive performance dashboard"""
167
+ height = height or 600
168
 
169
+ if not self._safe_data_validation(net_worth_history, data_type="net worth history"):
170
+ return self._create_empty_figure("No Performance Data", height)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ try:
173
+ fig = make_subplots(
174
+ rows=2, cols=2,
175
+ subplot_titles=['Portfolio Value', 'Returns vs Initial Balance',
176
+ 'Cumulative Reward', 'Reward Distribution'],
177
+ vertical_spacing=0.1,
178
+ horizontal_spacing=0.1,
179
+ specs=[[{"secondary_y": False}, {"secondary_y": False}],
180
+ [{"secondary_y": False}, {"secondary_y": False}]]
181
+ )
182
+
183
+ steps = np.arange(len(net_worth_history))
184
+ net_worth = np.array(net_worth_history)
185
+
186
+ # Portfolio value
187
+ fig.add_trace(
188
+ go.Scatter(x=steps, y=net_worth, mode='lines', name='Net Worth',
189
+ line=dict(color='#2ca02c', width=3)),
190
+ row=1, col=1
191
+ )
192
+
193
+ # Initial balance reference
194
+ fig.add_hline(y=initial_balance, line_dash="dash", line_color="red",
195
+ annotation_text=f"Initial: ${initial_balance:.2f}",
196
+ row=1, col=1)
197
+
198
+ # Returns comparison
199
+ returns = (net_worth - initial_balance) / initial_balance * 100
200
+ fig.add_trace(
201
+ go.Scatter(x=steps, y=returns, mode='lines', name='Returns %',
202
+ line=dict(color='#ff7f0e', width=2)),
203
+ row=1, col=2
204
+ )
205
+ fig.add_hline(y=0, line_dash="solid", line_color="gray", row=1, col=2)
206
+
207
+ # Cumulative reward
208
+ if reward_history and self._safe_data_validation(reward_history):
209
+ cum_reward = np.cumsum(reward_history)
210
+ fig.add_trace(
211
+ go.Scatter(x=steps[:len(cum_reward)], y=cum_reward, mode='lines',
212
+ name='Cumulative Reward', line=dict(color='#9467bd', width=2)),
213
+ row=2, col=1
214
+ )
215
+
216
+ # Reward distribution
217
+ if reward_history:
218
+ fig.add_trace(
219
+ go.Histogram(x=reward_history, name='Reward Distribution',
220
+ marker_color='#1f77b4', opacity=0.7),
221
+ row=2, col=2
222
+ )
223
+
224
+ fig.update_layout(
225
+ height=height,
226
+ showlegend=True,
227
+ title_text="Trading Performance Dashboard",
228
+ template=self.theme
229
+ )
230
+
231
+ # Update axis titles
232
+ fig.update_yaxes(title_text="Value ($)", row=1, col=1)
233
+ fig.update_yaxes(title_text="Returns (%)", row=1, col=2)
234
+ fig.update_yaxes(title_text="Cumulative Reward", row=2, col=1)
235
+ fig.update_xaxes(title_text="Steps", row=2, col=1)
236
+ fig.update_xaxes(title_text="Reward Value", row=2, col=2)
237
+
238
  return fig
239
+
240
+ except Exception as e:
241
+ logger.error(f"Error creating performance chart: {e}")
242
+ return self._create_empty_figure("Error in Performance Chart", height)
243
+
244
+ def create_action_distribution(self, actions: List[int],
245
+ title: Optional[str] = None,
246
+ height: Optional[int] = None) -> go.Figure:
247
+ """Create interactive action distribution visualization"""
248
+ height = height or 350
249
 
250
+ if not self._safe_data_validation(actions, data_type="actions"):
251
+ return self._create_empty_figure("No Actions Data", height)
 
 
 
 
 
 
 
 
 
252
 
253
+ try:
254
+ action_names = ['Hold', 'Buy', 'Sell', 'Close']
255
+ action_counts = [actions.count(i) for i in range(4)]
256
+ total_actions = sum(action_counts)
257
+
258
+ colors = ['#1f77b4', '#2ca02c', '#d62728', '#ff7f0e']
259
+
260
+ fig = go.Figure(data=[go.Pie(
261
+ labels=action_names,
262
+ values=action_counts,
263
+ hole=0.4,
264
+ marker_colors=colors,
265
+ textinfo='label+percent+value',
266
+ hovertemplate='<b>%{label}</b><br>Count: %{value}<br>Percentage: %{percent}<extra></extra>',
267
+ pull=[0, 0, 0, 0] # Equal spacing
268
+ )])
269
+
270
+ title = title or f"Action Distribution (Total: {total_actions} actions)"
271
+ fig.update_layout(
272
+ title={
273
+ 'text': title,
274
+ 'x': 0.5,
275
+ 'xanchor': 'center'
276
+ },
277
+ height=height,
278
+ showlegend=True,
279
+ template=self.theme,
280
+ annotations=[dict(
281
+ text='Trading Actions',
282
+ x=0.5, y=0.5,
283
+ font_size=16,
284
+ showarrow=False
285
+ )]
286
+ )
287
+
288
+ return fig
289
+
290
+ except Exception as e:
291
+ logger.error(f"Error creating action distribution: {e}")
292
+ return self._create_empty_figure("Error in Action Distribution", height)
293
+
294
+ def create_training_progress(self, training_history: List[Dict],
295
+ window_size: int = 10,
296
+ height: Optional[int] = None) -> go.Figure:
297
+ """Create comprehensive training progress dashboard"""
298
+ height = height or 700
299
 
300
+ if not training_history:
301
+ return self._create_empty_figure("No Training Data", height)
 
 
 
 
302
 
303
+ try:
304
+ # Extract data safely
305
+ episodes = [h.get('episode', i) for i, h in enumerate(training_history)]
306
+ rewards = [h.get('reward', 0) for h in training_history]
307
+ net_worths = [h.get('net_worth', 0) for h in training_history]
308
+ losses = [h.get('loss', 0) for h in training_history]
309
+
310
+ fig = make_subplots(
311
+ rows=2, cols=2,
312
+ subplot_titles=['Total Reward per Episode', 'Final Net Worth',
313
+ 'Training Loss', 'Moving Average Reward'],
314
+ specs=[[{"secondary_y": False}, {"secondary_y": False}],
315
+ [{"secondary_y": False}, {"secondary_y": False}]]
316
+ )
317
+
318
+ # Rewards
319
  fig.add_trace(go.Scatter(
320
+ x=episodes, y=rewards, mode='lines+markers',
321
+ name='Episode Reward', line=dict(color='#1f77b4', width=2),
322
  marker=dict(size=4)
323
+ ), row=1, col=1)
 
 
 
 
 
 
 
 
324
 
325
+ # Net worth
326
  fig.add_trace(go.Scatter(
327
+ x=episodes, y=net_worths, mode='lines+markers',
328
+ name='Final Net Worth', line=dict(color='#2ca02c', width=2),
329
+ marker=dict(size=4)
330
+ ), row=1, col=2)
331
+
332
+ # Loss (only if we have meaningful loss values)
333
+ valid_losses = [l for l in losses if l > 0]
334
+ if valid_losses:
335
+ fig.add_trace(go.Scatter(
336
+ x=episodes, y=losses, mode='lines',
337
+ name='Training Loss', line=dict(color='#d62728', width=2)
338
+ ), row=2, col=1)
339
+
340
+ # Moving average
341
+ if len(rewards) >= window_size:
342
+ ma_rewards = pd.Series(rewards).rolling(window=window_size, min_periods=1).mean()
343
+ fig.add_trace(go.Scatter(
344
+ x=episodes, y=ma_rewards, mode='lines',
345
+ name=f'MA Reward ({window_size})',
346
+ line=dict(color='#ff7f0e', width=3, dash='dash')
347
+ ), row=2, col=2)
348
+
349
+ fig.update_layout(
350
+ height=height,
351
+ showlegend=True,
352
+ title_text=f"Training Progress - {len(episodes)} Episodes",
353
+ template=self.theme
354
+ )
355
+
356
+ # Update axes
357
+ fig.update_yaxes(title_text="Reward", row=1, col=1)
358
+ fig.update_yaxes(title_text="Net Worth ($)", row=1, col=2)
359
+ fig.update_yaxes(title_text="Loss", row=2, col=1)
360
+ fig.update_xaxes(title_text="Episodes", row=2, col=1)
361
+
362
+ return fig
363
+
364
+ except Exception as e:
365
+ logger.error(f"Error creating training progress chart: {e}")
366
+ return self._create_empty_figure("Error in Training Progress", height)
367
+
368
+ def _create_empty_figure(self, title: str, height: int) -> go.Figure:
369
+ """Create a safe empty figure"""
370
+ fig = go.Figure()
371
  fig.update_layout(
372
+ title=title,
373
+ height=height,
374
+ template=self.theme
 
375
  )
376
+ return fig
377
+
378
+ def save_chart(self, fig: go.Figure, filename: str, format: str = 'html'):
379
+ """Save chart to file"""
380
+ try:
381
+ if format == 'html':
382
+ fig.write_html(filename)
383
+ elif format == 'png':
384
+ fig.write_image(filename)
385
+ elif format == 'pdf':
386
+ fig.write_image(filename, width=1200, height=800)
387
+ logger.info(f"Chart saved as {filename}")
388
+ except Exception as e:
389
+ logger.error(f"Error saving chart: {e}")
390
+
391
+ def show(self, fig: go.Figure):
392
+ """Display chart (if in interactive environment)"""
393
+ try:
394
+ fig.show()
395
+ except Exception as e:
396
+ logger.warning(f"Could not display chart: {e}")
397
+
398
+
399
+ # Utility functions for batch rendering
400
+ def render_dashboard(prices, actions, net_worth, rewards, config):
401
+ """Create a complete trading dashboard"""
402
+ renderer = ChartRenderer()
403
+
404
+ figs = {
405
+ 'price': renderer.render_price_chart(prices, actions),
406
+ 'performance': renderer.create_performance_chart(net_worth, rewards, config.initial_balance),
407
+ 'actions': renderer.create_action_distribution(actions)
408
+ }
409
+
410
+ return figs