Xinli Xiao commited on
Commit
e0cbbe9
·
1 Parent(s): 86c20d7
Files changed (2) hide show
  1. __pycache__/app.cpython-313.pyc +0 -0
  2. app.py +92 -21
__pycache__/app.cpython-313.pyc CHANGED
Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ
 
app.py CHANGED
@@ -1,14 +1,69 @@
1
  from datetime import datetime, timedelta
 
 
2
 
3
  import gradio as gr
4
  import numpy as np
5
  import pandas as pd
6
  import yfinance as yf
7
 
 
 
 
 
 
8
 
9
- def conf_int_ind(symb: str, target_time: str | None = None) -> pd.DataFrame:
10
- ticker = yf.Ticker(symb)
11
- expirations = ticker.options
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  if not expirations:
13
  raise ValueError(f"No option expirations found for {symb}.")
14
 
@@ -28,8 +83,8 @@ def conf_int_ind(symb: str, target_time: str | None = None) -> pd.DataFrame:
28
  time_now = datetime.now().strftime("%Y-%m-%d")
29
  t_days = (pd.to_datetime(target_time_r) - pd.to_datetime(time_now)).days + 1
30
 
31
- opt_chain = ticker.option_chain(target_time_r)
32
- current_price = ticker.fast_info["lastPrice"]
33
 
34
  lop = opt_chain.puts.strike[~opt_chain.puts.inTheMoney].iloc[-1]
35
  hip = opt_chain.puts.strike[opt_chain.puts.inTheMoney].iloc[0]
@@ -57,27 +112,37 @@ def conf_int_ind(symb: str, target_time: str | None = None) -> pd.DataFrame:
57
  "+2.5%(c)": [current_price * (1 + 2 * sdc)],
58
  }
59
 
60
- return pd.DataFrame(row).set_index("symbol")
61
 
62
 
63
- def conf_int_duo(symb: str, target_time: str | None = None) -> pd.DataFrame:
 
 
64
  if target_time is None:
65
- expirations = yf.Ticker(symb).options
66
  df_time = pd.to_datetime(expirations)
67
- recent_count = (df_time - datetime.now() <= timedelta(days=14)).sum()
68
- dlist = expirations[:recent_count]
69
  if not dlist:
70
- return conf_int_ind(symb, None)
71
- return pd.concat([conf_int_ind(symb, d) for d in dlist])
 
 
 
72
 
73
- return conf_int_ind(symb, target_time)
74
 
75
 
76
- def conf_int(symblist: str | list[str], target_time: str | None = None) -> pd.DataFrame:
 
 
77
  if isinstance(symblist, str):
78
- return conf_int_duo(symblist, target_time)
79
  if isinstance(symblist, list):
80
- return pd.concat([conf_int_duo(symb, target_time) for symb in symblist])
 
 
 
81
  raise TypeError("symblist must be a string or a list of strings.")
82
 
83
 
@@ -89,18 +154,19 @@ def parse_symbols(symbs_text: str) -> list[str]:
89
  return symbs
90
 
91
 
92
- def run_app(symbs_text: str, disable_target_time: bool, target_time: str) -> pd.DataFrame:
93
  symbs = parse_symbols(symbs_text)
94
  resolved_target_time = None if disable_target_time else (target_time or None)
95
  if not disable_target_time and resolved_target_time is None:
96
  raise gr.Error("Provide target_time in YYYY-MM-DD format, or disable it.")
97
 
98
  try:
99
- df = conf_int(symbs, resolved_target_time)
100
  except Exception as exc:
101
  raise gr.Error(str(exc)) from exc
102
 
103
- return df.reset_index()
 
104
 
105
 
106
  def toggle_target_time(disable_target_time: bool):
@@ -123,6 +189,10 @@ with gr.Blocks(title="Options Confidence Interval") as demo:
123
  label="Disable target_time to include all expiration dates within 14 days",
124
  value=True,
125
  )
 
 
 
 
126
  target_time_input = gr.Textbox(
127
  label="target_time",
128
  placeholder="YYYY-MM-DD",
@@ -131,6 +201,7 @@ with gr.Blocks(title="Options Confidence Interval") as demo:
131
 
132
  submit_btn = gr.Button("Run")
133
  output_df = gr.Dataframe(label="Result")
 
134
 
135
  disable_target_time_input.change(
136
  toggle_target_time,
@@ -139,8 +210,8 @@ with gr.Blocks(title="Options Confidence Interval") as demo:
139
  )
140
  submit_btn.click(
141
  run_app,
142
- inputs=[symbs_input, disable_target_time_input, target_time_input],
143
- outputs=output_df,
144
  )
145
 
146
 
 
1
  from datetime import datetime, timedelta
2
+ from functools import lru_cache
3
+ import time
4
 
5
  import gradio as gr
6
  import numpy as np
7
  import pandas as pd
8
  import yfinance as yf
9
 
10
+ MAX_AUTO_EXPIRATIONS = 3
11
+ RETRY_ATTEMPTS = 3
12
+ RETRY_DELAY_SECONDS = 1.5
13
+ option_chain_cache: dict[tuple[str, str], tuple[object, str]] = {}
14
+ price_cache: dict[str, tuple[float, str]] = {}
15
 
16
+
17
+ @lru_cache(maxsize=128)
18
+ def get_ticker(symb: str) -> yf.Ticker:
19
+ return yf.Ticker(symb)
20
+
21
+
22
+ @lru_cache(maxsize=128)
23
+ def get_expirations(symb: str) -> tuple[str, ...]:
24
+ return tuple(get_ticker(symb).options)
25
+
26
+
27
+ def get_option_chain_with_retry(ticker: yf.Ticker, target_time: str):
28
+ last_error = None
29
+ for attempt in range(RETRY_ATTEMPTS):
30
+ try:
31
+ return ticker.option_chain(target_time)
32
+ except Exception as exc:
33
+ last_error = exc
34
+ if attempt < RETRY_ATTEMPTS - 1:
35
+ time.sleep(RETRY_DELAY_SECONDS * (attempt + 1))
36
+ raise last_error
37
+
38
+
39
+ def get_option_chain(symb: str, target_time: str, force_refresh: bool = False):
40
+ cache_key = (symb, target_time)
41
+ if not force_refresh and cache_key in option_chain_cache:
42
+ return option_chain_cache[cache_key]
43
+
44
+ ticker = get_ticker(symb)
45
+ opt_chain = get_option_chain_with_retry(ticker, target_time)
46
+ downloaded_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
47
+ option_chain_cache[cache_key] = (opt_chain, downloaded_at)
48
+ return opt_chain, downloaded_at
49
+
50
+
51
+ def get_current_price(symb: str, force_refresh: bool = False) -> tuple[float, str]:
52
+ if not force_refresh and symb in price_cache:
53
+ return price_cache[symb]
54
+
55
+ ticker = get_ticker(symb)
56
+ current_price = ticker.fast_info["lastPrice"]
57
+ downloaded_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
58
+ price_cache[symb] = (current_price, downloaded_at)
59
+ return current_price, downloaded_at
60
+
61
+
62
+ def conf_int_ind(
63
+ symb: str, target_time: str | None = None, force_refresh: bool = False
64
+ ) -> tuple[pd.DataFrame, list[str]]:
65
+ ticker = get_ticker(symb)
66
+ expirations = get_expirations(symb)
67
  if not expirations:
68
  raise ValueError(f"No option expirations found for {symb}.")
69
 
 
83
  time_now = datetime.now().strftime("%Y-%m-%d")
84
  t_days = (pd.to_datetime(target_time_r) - pd.to_datetime(time_now)).days + 1
85
 
86
+ opt_chain, option_downloaded_at = get_option_chain(symb, target_time_r, force_refresh)
87
+ current_price, price_downloaded_at = get_current_price(symb, force_refresh)
88
 
89
  lop = opt_chain.puts.strike[~opt_chain.puts.inTheMoney].iloc[-1]
90
  hip = opt_chain.puts.strike[opt_chain.puts.inTheMoney].iloc[0]
 
112
  "+2.5%(c)": [current_price * (1 + 2 * sdc)],
113
  }
114
 
115
+ return pd.DataFrame(row).set_index("symbol"), [option_downloaded_at, price_downloaded_at]
116
 
117
 
118
+ def conf_int_duo(
119
+ symb: str, target_time: str | None = None, force_refresh: bool = False
120
+ ) -> tuple[pd.DataFrame, list[str]]:
121
  if target_time is None:
122
+ expirations = get_expirations(symb)
123
  df_time = pd.to_datetime(expirations)
124
+ recent_mask = (df_time - datetime.now() <= timedelta(days=14)) & (df_time >= datetime.now())
125
+ dlist = list(df_time[recent_mask].strftime("%Y-%m-%d"))[:MAX_AUTO_EXPIRATIONS]
126
  if not dlist:
127
+ return conf_int_ind(symb, None, force_refresh)
128
+ results = [conf_int_ind(symb, d, force_refresh) for d in dlist]
129
+ frames = [result[0] for result in results]
130
+ timestamps = [stamp for result in results for stamp in result[1]]
131
+ return pd.concat(frames), timestamps
132
 
133
+ return conf_int_ind(symb, target_time, force_refresh)
134
 
135
 
136
+ def conf_int(
137
+ symblist: str | list[str], target_time: str | None = None, force_refresh: bool = False
138
+ ) -> tuple[pd.DataFrame, list[str]]:
139
  if isinstance(symblist, str):
140
+ return conf_int_duo(symblist, target_time, force_refresh)
141
  if isinstance(symblist, list):
142
+ results = [conf_int_duo(symb, target_time, force_refresh) for symb in symblist]
143
+ frames = [result[0] for result in results]
144
+ timestamps = [stamp for result in results for stamp in result[1]]
145
+ return pd.concat(frames), timestamps
146
  raise TypeError("symblist must be a string or a list of strings.")
147
 
148
 
 
154
  return symbs
155
 
156
 
157
+ def run_app(symbs_text: str, disable_target_time: bool, target_time: str, force_refresh: bool):
158
  symbs = parse_symbols(symbs_text)
159
  resolved_target_time = None if disable_target_time else (target_time or None)
160
  if not disable_target_time and resolved_target_time is None:
161
  raise gr.Error("Provide target_time in YYYY-MM-DD format, or disable it.")
162
 
163
  try:
164
+ df, download_timestamps = conf_int(symbs, resolved_target_time, force_refresh)
165
  except Exception as exc:
166
  raise gr.Error(str(exc)) from exc
167
 
168
+ latest_download = max(download_timestamps) if download_timestamps else "Unknown"
169
+ return df.reset_index(), f"Last data download: {latest_download}", gr.update(value=False)
170
 
171
 
172
  def toggle_target_time(disable_target_time: bool):
 
189
  label="Disable target_time to include all expiration dates within 14 days",
190
  value=True,
191
  )
192
+ force_refresh_input = gr.Checkbox(
193
+ label="Force refresh",
194
+ value=False,
195
+ )
196
  target_time_input = gr.Textbox(
197
  label="target_time",
198
  placeholder="YYYY-MM-DD",
 
201
 
202
  submit_btn = gr.Button("Run")
203
  output_df = gr.Dataframe(label="Result")
204
+ refresh_timestamp = gr.Textbox(label="Data download timestamp", interactive=False)
205
 
206
  disable_target_time_input.change(
207
  toggle_target_time,
 
210
  )
211
  submit_btn.click(
212
  run_app,
213
+ inputs=[symbs_input, disable_target_time_input, target_time_input, force_refresh_input],
214
+ outputs=[output_df, refresh_timestamp, force_refresh_input],
215
  )
216
 
217