66from datetime import datetime , time , timedelta
77import pytz
88import math
9+ import time as time_module
10+ import threading
911
1012class OptionsMonitor :
1113 def __init__ (self , root ):
1214 self .root = root
13- self .root .title ("Options Monitor - Ver 1.5 " )
15+ self .root .title ("Options Monitor - Ver 1.6 " )
1416 self .data_file = r"C:\ProgramData\ShadowWhisperer\OptionsMonitor\data.csv"
1517 self .data = self .load_data ()
16- self .price_cache = {} # Cache yfinance prices
17- self .sort_reverse = {} # Track sort direction
18- self .last_market_status = None # Cache market status
19- self .refresh_interval = None # Track refresh interval
20- self .last_interval = "15 Mins" # Default Autoupdate time
21- self .last_updated_label = None # Reference to last updated label
22- self .DevMode = 0
23- self .current_sort_col = None # Track current sorted column
24- self .current_sort_reverse = False # Track current sort direction
18+ self .price_cache = {}
19+ self .sort_reverse = {}
20+ self .last_market_status = None
21+ self .refresh_interval = None
22+ self .last_interval = "15 Mins"
23+ self .last_updated_label = None
24+ self .DevMode = 0 #Dev Mode
25+ self .current_sort_col = None
26+ self .current_sort_reverse = False
2527 self .setup_gui ()
28+ self .populate_treeview ()
29+ threading .Thread (target = self .fetch_initial_prices , daemon = True ).start ()
2630
2731 def load_data (self ):
2832 os .makedirs (os .path .dirname (self .data_file ), exist_ok = True )
@@ -88,7 +92,6 @@ def setup_gui(self):
8892 self .status_frame .grid (row = 2 , column = 0 , sticky = "ew" )
8993
9094 self .update_market_status ()
91- self .populate_treeview ()
9295 self .schedule_refresh ()
9396
9497 def is_market_open (self ):
@@ -147,11 +150,20 @@ def refresh_if_open():
147150 def add_option (self ):
148151 add_window = tk .Toplevel (self .root )
149152 add_window .title ("Add" )
150- add_window .geometry ("220x210" )
151153 add_window .resizable (False , False )
152154
155+ root_x = self .root .winfo_x ()
156+ root_y = self .root .winfo_y ()
157+ root_width = self .root .winfo_width ()
158+ root_height = self .root .winfo_height ()
159+ window_width = 210
160+ window_height = 192
161+ x = root_x + (root_width - window_width ) // 2
162+ y = root_y + (root_height - window_height ) // 2
163+ add_window .geometry (f"{ window_width } x{ window_height } +{ x } +{ y } " )
164+
153165 form_frame = tk .Frame (add_window )
154- form_frame .pack (pady = 2 , padx = 1 )
166+ form_frame .pack (pady = 4 , padx = 1 )
155167
156168 tk .Label (form_frame , text = "Ticker" ).grid (row = 0 , column = 0 , sticky = "e" , padx = 4 , pady = 4 )
157169 ticker_entry = tk .Entry (form_frame )
@@ -178,7 +190,7 @@ def add_option(self):
178190
179191 tk .Button (add_window , text = "Add Option" , command = lambda : self ._submit_option (
180192 ticker_entry , call_put_var , contracts_entry , strike_entry , close_date_entry , add_window ), width = 10
181- ).pack (pady = 1 )
193+ ).pack (pady = 3 )
182194
183195 def _submit_option (self , ticker_entry , call_put_var , contracts_entry , strike_entry , close_date_entry , window ):
184196 ticker = ticker_entry .get ().upper ()
@@ -202,36 +214,58 @@ def _submit_option(self, ticker_entry, call_put_var, contracts_entry, strike_ent
202214 else :
203215 messagebox .showerror ("Error" , "Invalid Ticker." )
204216
217+ def fetch_initial_prices (self ):
218+ unique_tickers = {row [0 ] for row in self .data if len (row ) == 5 }
219+ for ticker in unique_tickers :
220+ if ticker not in self .price_cache or self .price_cache [ticker ] in ["Checking" , "?" ]:
221+ self .price_cache [ticker ] = "Checking"
222+ for attempt in range (3 ):
223+ try :
224+ yf_ticker = yf .Ticker (ticker )
225+ quote = yf_ticker .get_info ().get ('regularMarketPrice' , None )
226+ if quote is None or (isinstance (quote , float ) and math .isnan (quote )):
227+ history = yf_ticker .history (period = "1d" )
228+ quote = round (history ["Close" ].iloc [- 1 ], 2 ) if not history .empty and not math .isnan (history ["Close" ].iloc [- 1 ]) else None
229+ self .price_cache [ticker ] = quote if quote is not None and not math .isnan (quote ) else "?"
230+ if self .DevMode == 1 :
231+ print (f"[{ datetime .now ().strftime ('%H:%M:%S' )} ] Lookup { ticker } : { 'Success' if quote is not None and not math .isnan (quote ) else 'Failed' } , Price: { quote if quote is not None and not math .isnan (quote ) else 'N/A' } " )
232+ break
233+ except Exception as e :
234+ if self .DevMode == 1 :
235+ print (f"[{ datetime .now ().strftime ('%H:%M:%S' )} ] Lookup { ticker } : Failed, Error: { str (e )} " )
236+ if attempt < 2 :
237+ time_module .sleep (1 )
238+ self .price_cache [ticker ] = "?"
239+ self .root .after (0 , self .populate_treeview )
240+
205241 def populate_treeview (self ):
206242 for item in self .tree .get_children ():
207243 self .tree .delete (item )
208244 unique_tickers = {row [0 ] for row in self .data if len (row ) == 5 }
209245 for ticker in unique_tickers :
210246 if ticker not in self .price_cache :
211- try :
212- yf_ticker = yf .Ticker (ticker )
213- quote = yf_ticker .get_info ().get ('regularMarketPrice' , None )
214- if quote is None :
215- history = yf_ticker .history (period = "1d" )
216- quote = round (history ["Close" ].iloc [- 1 ], 2 ) if not history .empty else None
217- self .price_cache [ticker ] = quote if quote is not None else float ('nan' )
218- except Exception :
219- self .price_cache [ticker ] = float ('nan' )
247+ self .price_cache [ticker ] = "Checking"
220248 for index , row in enumerate (self .data ):
221249 if len (row ) != 5 :
222250 continue
223- current_price = self .price_cache .get (row [0 ], float ( 'nan' ) )
251+ current_price = self .price_cache .get (row [0 ], "Checking" )
224252 option = row [2 ]
225253 strike_price = row [4 ]
226254 contracts = row [3 ]
227- outcome = self .calculate_outcome (option , current_price , strike_price ) if not math .isnan (current_price ) else ""
228- diff = current_price - strike_price if not math .isnan (current_price ) else float ('nan' )
229- diff_fmt = f"+{ round (diff , 2 ):.2f} " if not math .isnan (diff ) and diff > 0 else f"-{ round (abs (diff ), 2 ):.2f} " if not math .isnan (diff ) and diff < 0 else ""
255+ outcome = ""
256+ diff = float ('nan' )
257+ diff_fmt = ""
258+ value = ""
259+ value_fmt = ""
260+ if current_price not in ["Checking" , "?" ]:
261+ outcome = self .calculate_outcome (option , current_price , strike_price ) if not math .isnan (current_price ) else ""
262+ diff = current_price - strike_price if not math .isnan (current_price ) else float ('nan' )
263+ diff_fmt = f"+{ round (diff , 2 ):.2f} " if not math .isnan (diff ) and diff > 0 else f"-{ round (abs (diff ), 2 ):.2f} " if not math .isnan (diff ) and diff < 0 else ""
264+ value = round (diff * (contracts * 100 ), 2 ) if outcome and not math .isnan (diff ) else ""
265+ value_fmt = f"{ int (value ):,} " if value else ""
230266 diff_tag = 'green_diff' if not math .isnan (diff ) and diff > 0 else 'red_diff' if not math .isnan (diff ) and diff < 0 else ''
231- value = round (diff * (contracts * 100 ), 2 ) if outcome and not math .isnan (diff ) else ""
232- value_fmt = f"{ int (value )} " if value else ""
233267 strike_price_fmt = int (strike_price ) if strike_price == int (strike_price ) else round (strike_price , 2 )
234- current_price_fmt = int (current_price ) if not math .isnan (current_price ) and current_price == int (current_price ) else round (current_price , 2 ) if not math . isnan ( current_price ) else ""
268+ current_price_fmt = current_price if current_price in [ "Checking" , "?" ] else ( int (current_price ) if not math .isnan (current_price ) and current_price == int (current_price ) else round (current_price , 2 ))
235269 tag = 'redrow' if outcome else ('oddrow' if index % 2 else 'evenrow' )
236270 item = self .tree .insert ("" , "end" , tags = (tag , f"list_index_{ index } " ), values = (
237271 row [0 ], row [1 ], row [2 ], row [3 ], strike_price_fmt , current_price_fmt , diff_fmt , outcome , value_fmt
@@ -242,61 +276,35 @@ def populate_treeview(self):
242276 self .last_updated_label .config (text = f"{ datetime .now ().strftime ('%H:%M:%S' )} " )
243277
244278 def calculate_outcome (self , call_put , current_price , strike_price ):
245- if call_put == "Call " :
246- return "Sell " if current_price > strike_price else ""
247- else :
248- return "Purchase " if current_price < strike_price else ""
279+ if call_put == "Put " :
280+ return "Purchase " if strike_price > current_price else ""
281+ else : # Call
282+ return "Sell " if strike_price < current_price else ""
249283
250284 def refresh_data (self ):
251285 unique_tickers = {row [0 ] for row in self .data if len (row ) == 5 }
252286 for ticker in unique_tickers :
253287 if ticker not in self .price_cache or self .is_market_open ():
254- try :
255- yf_ticker = yf .Ticker (ticker )
256- quote = yf_ticker .get_info ().get ('regularMarketPrice' , None )
257- if quote is None :
258- history = yf_ticker .history (period = "1d" )
259- quote = round (history ["Close" ].iloc [- 1 ], 2 ) if not history .empty else None
260- self .price_cache [ticker ] = quote if quote is not None else float ('nan' )
261- if self .DevMode == 1 :
262- print (f"[{ datetime .now ().strftime ('%H:%M:%S' )} ] Lookup { ticker } : { 'Success' if quote is not None else 'Failed' } , Price: { quote if quote is not None else 'N/A' } " )
263- except Exception as e :
264- self .price_cache [ticker ] = float ('nan' )
265- if self .DevMode == 1 :
266- print (f"[{ datetime .now ().strftime ('%H:%M:%S' )} ] Lookup { ticker } : Failed, Error: { str (e )} " )
267-
268- for item in self .tree .get_children ():
269- tags = self .tree .item (item , "tags" )
270- list_index = None
271- for tag in tags :
272- if tag .startswith ("list_index_" ):
273- list_index = int (tag .replace ("list_index_" , "" ))
274- break
275- if list_index is not None and list_index < len (self .data ) and len (self .data [list_index ]) == 5 :
276- row = self .data [list_index ]
277- current_price = self .price_cache .get (row [0 ], float ('nan' ))
278- option = row [2 ]
279- strike_price = row [4 ]
280- contracts = row [3 ]
281- outcome = self .calculate_outcome (option , current_price , strike_price ) if not math .isnan (current_price ) else ""
282- diff = current_price - strike_price if not math .isnan (current_price ) else float ('nan' )
283- diff_fmt = f"+{ round (diff , 2 ):.2f} " if not math .isnan (diff ) and diff > 0 else f"-{ round (abs (diff ), 2 ):.2f} " if not math .isnan (diff ) and diff < 0 else ""
284- diff_tag = 'green_diff' if not math .isnan (diff ) and diff > 0 else 'red_diff' if not math .isnan (diff ) and diff < 0 else ''
285- value = round (diff * (contracts * 100 ), 2 ) if outcome and not math .isnan (diff ) else ""
286- value_fmt = f"{ int (value )} " if value else ""
287- strike_price_fmt = int (strike_price ) if strike_price == int (strike_price ) else round (strike_price , 2 )
288- current_price_fmt = int (current_price ) if not math .isnan (current_price ) and current_price == int (current_price ) else round (current_price , 2 ) if not math .isnan (current_price ) else ""
289- tag = 'redrow' if outcome else ('oddrow' if int (self .tree .index (item )) % 2 else 'evenrow' )
290- tags = [t for t in tags if t .startswith ('list_index_' ) or t in ('oddrow' , 'evenrow' , 'redrow' )]
291- tags .append (tag )
292- self .tree .item (item , tags = tuple (tags ))
293- self .tree .item (item , values = (
294- row [0 ], row [1 ], row [2 ], row [3 ], strike_price_fmt , current_price_fmt , diff_fmt , outcome , value_fmt
295- ))
296- if diff_tag :
297- self .tree .set (item , "Diff" , diff_fmt )
288+ for attempt in range (3 ):
289+ try :
290+ yf_ticker = yf .Ticker (ticker )
291+ quote = yf_ticker .get_info ().get ('regularMarketPrice' , None )
292+ if quote is None or (isinstance (quote , float ) and math .isnan (quote )):
293+ history = yf_ticker .history (period = "1d" )
294+ quote = round (history ["Close" ].iloc [- 1 ], 2 ) if not history .empty and not math .isnan (history ["Close" ].iloc [- 1 ]) else None
295+ self .price_cache [ticker ] = quote if quote is not None and not math .isnan (quote ) else "?"
296+ if self .DevMode == 1 :
297+ print (f"[{ datetime .now ().strftime ('%H:%M:%S' )} ] Lookup { ticker } : { 'Success' if quote is not None and not math .isnan (quote ) else 'Failed' } , Price: { quote if quote is not None and not math .isnan (quote ) else 'N/A' } " )
298+ break
299+ except Exception as e :
300+ if self .DevMode == 1 :
301+ print (f"[{ datetime .now ().strftime ('%H:%M:%S' )} ] Lookup { ticker } : Failed, Error: { str (e )} " )
302+ if attempt < 2 :
303+ time_module .sleep (1 )
304+ self .price_cache [ticker ] = "?"
305+ self .root .after (0 , self .populate_treeview )
298306
299- self .last_updated_label . config ( text = f" { datetime . now (). strftime ( '%H:%M:%S' ) } " )
307+ self .root . after ( 0 , self . populate_treeview )
300308
301309 def remove_selected (self ):
302310 selected = self .tree .selection ()
@@ -344,7 +352,7 @@ def sort_column(self, col):
344352 def get_sort_key (item ):
345353 if len (item ) != 5 :
346354 return (float ('inf' ), "" )
347- value = item [col_index ] if col_index < 5 else "" # Only use stored values for columns 0-4
355+ value = item [col_index ] if col_index < 5 else ""
348356 ticker = item [0 ]
349357 if col == "Ends" :
350358 try :
@@ -356,21 +364,26 @@ def get_sort_key(item):
356364 elif col == "Diff" :
357365 current_price = self .price_cache .get (ticker , float ('nan' ))
358366 strike_price = item [4 ]
359- if not math .isnan (current_price ):
367+ if current_price not in [ "Checking" , "?" ] and not math .isnan (current_price ):
360368 diff = current_price - strike_price
361369 return (diff , ticker )
362370 return (float ('inf' ), ticker )
363371 elif col == "Current" :
364372 current_price = self .price_cache .get (ticker , float ('nan' ))
365- return (current_price , ticker ) if not math .isnan (current_price ) else (float ('inf' ), ticker )
373+ return (current_price , ticker ) if current_price not in [ "Checking" , "?" ] and not math .isnan (current_price ) else (float ('inf' ), ticker )
366374 elif col == "Value" :
367375 current_price = self .price_cache .get (ticker , float ('nan' ))
368376 strike_price = item [4 ]
369377 contracts = item [3 ]
370- if not math .isnan (current_price ) and self .calculate_outcome (item [2 ], current_price , strike_price ):
378+ if current_price not in [ "Checking" , "?" ] and not math .isnan (current_price ) and self .calculate_outcome (item [2 ], current_price , strike_price ):
371379 value = (current_price - strike_price ) * (contracts * 100 )
372380 return (value , ticker )
373381 return (float ('inf' ), ticker )
382+ elif col == "Outcome" :
383+ current_price = self .price_cache .get (ticker , float ('nan' ))
384+ outcome = self .calculate_outcome (item [2 ], current_price , item [4 ]) if current_price not in ["Checking" , "?" ] and not math .isnan (current_price ) else ""
385+ priority = 1 if outcome == "Purchase" else 2 if outcome == "Sell" else 3
386+ return (priority , ticker )
374387 elif col in ["Contracts" , "Strike" ]:
375388 return (value , ticker )
376389 else :
@@ -450,5 +463,6 @@ def save_edit(event=None):
450463if __name__ == "__main__" :
451464 root = tk .Tk ()
452465 root .geometry ("620x300" )
466+ root .resizable (True , True )
453467 app = OptionsMonitor (root )
454468 root .mainloop ()
0 commit comments