11import asyncio
22import signal
3+ from datetime import datetime
4+ from pathlib import Path
5+
6+ try :
7+ import plotly .graph_objects as go
8+ from plotly .subplots import make_subplots
9+
10+ PLOTLY_AVAILABLE = True
11+ except ImportError :
12+ PLOTLY_AVAILABLE = False
13+ print ("⚠️ Plotly not installed. Charts will not be generated." )
314
415from project_x_py import TradingSuite
516from project_x_py .event_bus import EventType
617
18+ TIMEFRAME = "15sec"
19+
20+
21+ def create_candlestick_chart (bars_data , instrument : str , timeframe : str , filename : str ):
22+ """Create a candlestick chart from bar data using Plotly"""
23+ if not PLOTLY_AVAILABLE :
24+ return False
25+
26+ try :
27+ # Convert Polars DataFrame to dict for easier access
28+ data_dict = bars_data .to_dict ()
29+
30+ # Create figure with secondary y-axis for volume
31+ fig = make_subplots (
32+ rows = 2 ,
33+ cols = 1 ,
34+ shared_xaxes = True ,
35+ vertical_spacing = 0.03 ,
36+ subplot_titles = (f"{ instrument } - { timeframe } Candlestick Chart" , "Volume" ),
37+ row_heights = [0.7 , 0.3 ],
38+ )
39+
40+ # Add candlestick chart with proper price formatting
41+ fig .add_trace (
42+ go .Candlestick (
43+ x = data_dict ["timestamp" ],
44+ open = data_dict ["open" ],
45+ high = data_dict ["high" ],
46+ low = data_dict ["low" ],
47+ close = data_dict ["close" ],
48+ name = "OHLC" ,
49+ increasing_line_color = "green" ,
50+ decreasing_line_color = "red" ,
51+ ),
52+ row = 1 ,
53+ col = 1 ,
54+ )
55+
56+ # Add volume bars
57+ colors = [
58+ "green" if close >= open_ else "red"
59+ for close , open_ in zip (data_dict ["close" ], data_dict ["open" ], strict = False )
60+ ]
61+
62+ fig .add_trace (
63+ go .Bar (
64+ x = data_dict ["timestamp" ],
65+ y = data_dict ["volume" ],
66+ name = "Volume" ,
67+ marker_color = colors ,
68+ showlegend = False ,
69+ hovertemplate = "<b>%{x}</b><br>"
70+ + "Volume: %{y:,}<br>"
71+ + "<extra></extra>" ,
72+ ),
73+ row = 2 ,
74+ col = 1 ,
75+ )
76+
77+ # Update layout
78+ fig .update_layout (
79+ title = f"{ instrument } - Last { bars_data .height } { timeframe } Bars" ,
80+ xaxis_title = "Time" ,
81+ yaxis_title = "Price ($)" ,
82+ template = "plotly_dark" ,
83+ xaxis_rangeslider_visible = False ,
84+ height = 800 ,
85+ showlegend = False ,
86+ )
87+
88+ # Update y-axes with proper formatting
89+ fig .update_yaxes (
90+ title_text = "Price ($)" ,
91+ row = 1 ,
92+ col = 1 ,
93+ tickformat = "$,.2f" , # Format y-axis ticks as currency with 2 decimals
94+ )
95+ fig .update_yaxes (title_text = "Volume" , row = 2 , col = 1 , tickformat = "," )
96+
97+ # Generate HTML filename
98+ html_filename = filename .replace (".csv" , ".html" )
99+
100+ # Save the chart
101+ fig .write_html (html_filename )
102+
103+ print (f"📈 Candlestick chart saved to { html_filename } " )
104+
105+ # Also try to open in browser (optional)
106+ try :
107+ import webbrowser
108+
109+ webbrowser .open (f"file://{ Path (html_filename ).absolute ()} " )
110+ print ("📊 Chart opened in browser" )
111+ except Exception :
112+ pass # Silently fail if browser can't be opened
113+
114+ return True
115+
116+ except Exception as e :
117+ print (f"⚠️ Could not create chart: { e } " )
118+ return False
119+
120+
121+ async def export_bars_to_csv (
122+ suite : TradingSuite , timeframe : str , bars_count : int = 100
123+ ):
124+ """Export the last N bars to a CSV file"""
125+ try :
126+ # Get the last 100 bars
127+ bars_data = await suite .data .get_data (timeframe = timeframe , bars = bars_count )
128+
129+ if bars_data is None or bars_data .is_empty ():
130+ print ("No data available to export." )
131+ return False
132+
133+ if suite .instrument is None :
134+ print ("Suite.instrument is None, skipping chart creation" )
135+ return True
136+
137+ # Generate filename with timestamp
138+ timestamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
139+ filename = f"bars_export_{ suite .instrument .name } _{ timeframe } _{ timestamp } .csv"
140+ filepath = Path (filename )
141+
142+ # Write to CSV
143+ bars_data .write_csv (filepath )
144+
145+ print (f"\n ✅ Successfully exported { bars_data .height } bars to { filename } " )
146+
147+ if suite .instrument is None :
148+ print ("Suite.instrument is None, skipping chart creation" )
149+ return True
150+
151+ # Create candlestick chart
152+ create_candlestick_chart (bars_data , suite .instrument .name , timeframe , filename )
153+
154+ return True
155+
156+ except Exception as e :
157+ print (f"❌ Error exporting data: { e } " )
158+ return False
159+
160+
161+ async def prompt_for_csv_export (suite , timeframe : str ):
162+ """Prompt user to export CSV in a non-blocking way"""
163+ print ("\n " + "=" * 80 )
164+ print ("📊 10 new bars have been received!" )
165+ print (
166+ "Would you like to export the last 100 bars to CSV and generate a candlestick chart?"
167+ )
168+ print ("Type 'y' or 'yes' to export, or press Enter to continue monitoring..." )
169+ print ("=" * 80 )
170+
171+ # Create a task to wait for user input without blocking
172+ loop = asyncio .get_event_loop ()
173+ future = loop .create_future ()
174+
175+ def handle_input ():
176+ try :
177+ # Non-blocking input using asyncio
178+ response = input ().strip ().lower ()
179+ future .set_result (response )
180+ except Exception as e :
181+ future .set_exception (e )
182+
183+ # Run input in executor to avoid blocking
184+ loop .run_in_executor (None , handle_input )
185+
186+ try :
187+ # Wait for input with a timeout
188+ response = await asyncio .wait_for (future , timeout = 10.0 )
189+
190+ if response in ["y" , "yes" ]:
191+ await export_bars_to_csv (suite , timeframe )
192+ return True
193+ except TimeoutError :
194+ print ("\n No response received. Continuing to monitor..." )
195+ except Exception as e :
196+ print (f"\n Error handling input: { e } " )
197+
198+ return False
199+
7200
8201async def main ():
9202 print ("Creating TradingSuite..." )
10203 # Note: Use "MNQ" for Micro E-mini Nasdaq-100 futures
11204 # "NQ" resolves to E-mini Nasdaq (ENQ) which may have different data characteristics
12205 suite = await TradingSuite .create (
13206 instrument = "NQ" , # Works best with MNQ for consistent real-time updates
14- timeframes = ["1min" ],
207+ timeframes = [TIMEFRAME ],
15208 )
16209 print ("TradingSuite created!" )
17210
@@ -21,6 +214,9 @@ async def main():
21214 # Set up signal handler for clean exit
22215 shutdown_event = asyncio .Event ()
23216
217+ # Bar counter
218+ bar_counter = {"count" : 0 , "export_prompted" : False }
219+
24220 def signal_handler (_signum , _frame ):
25221 print ("\n \n Received interrupt signal. Shutting down gracefully..." )
26222 shutdown_event .set ()
@@ -31,31 +227,33 @@ def signal_handler(_signum, _frame):
31227 # Define the event handler as an async function
32228 async def on_new_bar (event ):
33229 """Handle new bar events"""
34- print (f"New bar event received: { event } " )
35- print ("About to call get_current_price..." )
230+ # Increment bar counter
231+ bar_counter ["count" ] += 1
232+
233+ print (f"\n 📊 New bar #{ bar_counter ['count' ]} received" )
234+
36235 try :
37236 current_price = await suite .data .get_current_price ()
38- print (f"Got current price: { current_price } " )
39237 except Exception as e :
40238 print (f"Error getting current price: { e } " )
41239 return
42240
43- print ("About to call get_data..." )
44241 try :
45- last_bars = await suite .data .get_data (timeframe = "15sec" , bars = 5 )
46- print ("Got data" )
242+ last_bars = await suite .data .get_data (timeframe = TIMEFRAME , bars = 5 )
47243 except Exception as e :
48244 print (f"Error getting data: { e } " )
49245 return
50- print (f"\n Current price: { current_price } " )
246+
247+ print (f"Current price: ${ current_price :,.2f} " )
51248 print ("=" * 80 )
52249
53250 if last_bars is not None and not last_bars .is_empty ():
54- print ("Last 5 bars (oldest to newest):" )
251+ print ("Last 6 bars (oldest to newest):" )
252+ print ("Oldest bar is first, current bar is last" )
55253 print ("-" * 80 )
56254
57255 # Get the last 5 bars and iterate through them
58- for row in last_bars .tail (5 ).iter_rows (named = True ):
256+ for row in last_bars .tail (6 ).iter_rows (named = True ):
59257 timestamp = row ["timestamp" ]
60258 open_price = row ["open" ]
61259 high = row ["high" ]
@@ -69,12 +267,24 @@ async def on_new_bar(event):
69267 else :
70268 print ("No bar data available yet" )
71269
270+ # Check if we should prompt for CSV export
271+ if bar_counter ["count" ] == 10 and not bar_counter ["export_prompted" ]:
272+ bar_counter ["export_prompted" ] = True
273+ # Run the prompt in a separate task to avoid blocking
274+ asyncio .create_task (prompt_for_csv_export (suite , TIMEFRAME )) # noqa: RUF006
275+
276+ # Reset the prompt flag after 20 bars so it can prompt again
277+ if bar_counter ["count" ] >= 20 :
278+ bar_counter ["count" ] = 0
279+ bar_counter ["export_prompted" ] = False
280+
72281 # Register the event handler
73282 print ("About to register event handler..." )
74283 await suite .on (EventType .NEW_BAR , on_new_bar )
75284 print ("Event handler registered!" )
76285
77- print ("Monitoring MNQ 15-second bars. Press CTRL+C to exit." )
286+ print (f"\n Monitoring { suite .instrument } { TIMEFRAME } bars. Press CTRL+C to exit." )
287+ print ("📊 CSV export and chart generation will be prompted after 10 new bars." )
78288 print ("Event handler registered and waiting for new bars...\n " )
79289
80290 try :
0 commit comments