sukiboo commited on
Commit
f2e9588
Β·
1 Parent(s): 587133c

add ticker validation

Browse files
Files changed (4) hide show
  1. app.ipynb +1 -1
  2. src/dash_app.py +21 -8
  3. src/prices.py +8 -1
  4. src/utils.py +7 -0
app.ipynb CHANGED
@@ -33,7 +33,7 @@
33
  " "
34
  ],
35
  "text/plain": [
36
- "<IPython.lib.display.IFrame at 0x10f60e840>"
37
  ]
38
  },
39
  "metadata": {},
 
33
  " "
34
  ],
35
  "text/plain": [
36
+ "<IPython.lib.display.IFrame at 0x119e2b890>"
37
  ]
38
  },
39
  "metadata": {},
src/dash_app.py CHANGED
@@ -7,7 +7,12 @@ from src.style_elements import (
7
  setup_interval_buttons,
8
  setup_ticker_selection,
9
  )
10
- from src.utils import adjust_date_range, date_to_idx_range, get_date_range
 
 
 
 
 
11
 
12
 
13
  class NormalizedAssetPricesApp:
@@ -96,6 +101,7 @@ class NormalizedAssetPricesApp:
96
  Output("ticker-selection", "options"),
97
  Output("ticker-selection", "value"),
98
  Output("ticker-input", "value"),
 
99
  ],
100
  [Input("ticker-input", "n_submit"), Input("ticker-selection", "value")],
101
  [State("ticker-input", "value"), State("ticker-selection", "value")],
@@ -103,16 +109,23 @@ class NormalizedAssetPricesApp:
103
  )
104
  def update_tickers(n_submit, selected_tickers, input_ticker, current_tickers):
105
  triggered_id = ctx.triggered_id
 
 
 
106
  if triggered_id == "ticker-input" and input_ticker and input_ticker.strip():
107
- ticker = input_ticker.strip().upper()
108
- tickers = current_tickers if current_tickers else []
109
- if ticker not in tickers:
 
 
 
110
  tickers = tickers + [ticker]
 
 
 
 
111
  options = [{"label": t, "value": t} for t in tickers]
112
- return options, tickers, ""
113
- tickers = selected_tickers if selected_tickers else []
114
- options = [{"label": t, "value": t} for t in tickers]
115
- return options, tickers, input_ticker or ""
116
 
117
  @self.app.callback(
118
  Output("plotly-normalized-asset-prices", "figure"),
 
7
  setup_interval_buttons,
8
  setup_ticker_selection,
9
  )
10
+ from src.utils import (
11
+ adjust_date_range,
12
+ date_to_idx_range,
13
+ get_date_range,
14
+ normalize_ticker_symbol,
15
+ )
16
 
17
 
18
  class NormalizedAssetPricesApp:
 
101
  Output("ticker-selection", "options"),
102
  Output("ticker-selection", "value"),
103
  Output("ticker-input", "value"),
104
+ Output("ticker-input", "placeholder"),
105
  ],
106
  [Input("ticker-input", "n_submit"), Input("ticker-selection", "value")],
107
  [State("ticker-input", "value"), State("ticker-selection", "value")],
 
109
  )
110
  def update_tickers(n_submit, selected_tickers, input_ticker, current_tickers):
111
  triggered_id = ctx.triggered_id
112
+ tickers = current_tickers if current_tickers else []
113
+ options = [{"label": t, "value": t} for t in tickers]
114
+
115
  if triggered_id == "ticker-input" and input_ticker and input_ticker.strip():
116
+ ticker = normalize_ticker_symbol(input_ticker)
117
+ if ticker in tickers:
118
+ return options, tickers, "", f"⚠️ `{ticker}` already added"
119
+ elif not self.prices.is_valid_ticker(ticker):
120
+ return options, tickers, "", f"❌ `{ticker}` is not valid"
121
+ else:
122
  tickers = tickers + [ticker]
123
+ options = [{"label": t, "value": t} for t in tickers]
124
+ return options, tickers, "", f"βœ… `{ticker}` added"
125
+ else:
126
+ tickers = selected_tickers if selected_tickers else []
127
  options = [{"label": t, "value": t} for t in tickers]
128
+ return options, tickers, input_ticker or "", "Enter ticker symbol..."
 
 
 
129
 
130
  @self.app.callback(
131
  Output("plotly-normalized-asset-prices", "figure"),
src/prices.py CHANGED
@@ -68,8 +68,8 @@ class Prices:
68
  df.drop(ticker, axis=1, inplace=True)
69
 
70
  def add_ticker(self, ticker):
71
- self.tickers.append(ticker)
72
  ticker_df = self.get_historical_prices(ticker)
 
73
  self.prices_raw[ticker] = ticker_df
74
  self.prices_normalized[ticker] = ticker_df / ticker_df.iloc[0]
75
  self.percentage_changes[ticker] = (
@@ -81,3 +81,10 @@ class Prices:
81
  .rolling(window=PRICES_ROLLING_WINDOW, min_periods=PRICES_ROLLING_MIN_PERIOD)
82
  .sum()
83
  )
 
 
 
 
 
 
 
 
68
  df.drop(ticker, axis=1, inplace=True)
69
 
70
  def add_ticker(self, ticker):
 
71
  ticker_df = self.get_historical_prices(ticker)
72
+ self.tickers.append(ticker)
73
  self.prices_raw[ticker] = ticker_df
74
  self.prices_normalized[ticker] = ticker_df / ticker_df.iloc[0]
75
  self.percentage_changes[ticker] = (
 
81
  .rolling(window=PRICES_ROLLING_WINDOW, min_periods=PRICES_ROLLING_MIN_PERIOD)
82
  .sum()
83
  )
84
+
85
+ def is_valid_ticker(self, ticker):
86
+ try:
87
+ data = yf.download(ticker, period="5d", progress=False, auto_adjust=False)
88
+ return data is not None and not data.empty
89
+ except Exception:
90
+ return False
src/utils.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from datetime import timedelta
2
 
3
  import numpy as np
@@ -5,7 +6,13 @@ import pandas as pd
5
  from dateutil import parser # type: ignore
6
 
7
 
 
 
 
 
 
8
  def get_available_tickers():
 
9
  nasdaq_url = "https://www.nasdaqtrader.com/dynamic/SymDir/nasdaqlisted.txt"
10
  nyse_url = "https://www.nasdaqtrader.com/dynamic/SymDir/otherlisted.txt"
11
  nasdaq_tickers = pd.read_csv(nasdaq_url, sep="|")["Symbol"].dropna().tolist()[:-1]
 
1
+ import re
2
  from datetime import timedelta
3
 
4
  import numpy as np
 
6
  from dateutil import parser # type: ignore
7
 
8
 
9
+ def normalize_ticker_symbol(ticker_input):
10
+ """Remove all non-alphanumeric characters from ticker input, return uppercase."""
11
+ return re.sub(r"[^A-Z0-9]", "", ticker_input.upper())
12
+
13
+
14
  def get_available_tickers():
15
+ """This list is too limited, I opted for any input + validation in the app."""
16
  nasdaq_url = "https://www.nasdaqtrader.com/dynamic/SymDir/nasdaqlisted.txt"
17
  nyse_url = "https://www.nasdaqtrader.com/dynamic/SymDir/otherlisted.txt"
18
  nasdaq_tickers = pd.read_csv(nasdaq_url, sep="|")["Symbol"].dropna().tolist()[:-1]