Spaces:
Sleeping
Sleeping
add ticker validation
Browse files- app.ipynb +1 -1
- src/dash_app.py +21 -8
- src/prices.py +8 -1
- src/utils.py +7 -0
app.ipynb
CHANGED
|
@@ -33,7 +33,7 @@
|
|
| 33 |
" "
|
| 34 |
],
|
| 35 |
"text/plain": [
|
| 36 |
-
"<IPython.lib.display.IFrame at
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 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]
|