Spaces:
Sleeping
Sleeping
fix price normalization
Browse files- app.ipynb +2 -23
- src/dash_app.py +9 -34
- src/style_elements.py +10 -6
- src/utils.py +22 -0
app.ipynb
CHANGED
|
@@ -18,13 +18,6 @@
|
|
| 18 |
"id": "5d3ea548",
|
| 19 |
"metadata": {},
|
| 20 |
"outputs": [
|
| 21 |
-
{
|
| 22 |
-
"name": "stdout",
|
| 23 |
-
"output_type": "stream",
|
| 24 |
-
"text": [
|
| 25 |
-
"plot_prices: date_range=[None, None]\n"
|
| 26 |
-
]
|
| 27 |
-
},
|
| 28 |
{
|
| 29 |
"data": {
|
| 30 |
"text/html": [
|
|
@@ -40,7 +33,7 @@
|
|
| 40 |
" "
|
| 41 |
],
|
| 42 |
"text/plain": [
|
| 43 |
-
"<IPython.lib.display.IFrame at
|
| 44 |
]
|
| 45 |
},
|
| 46 |
"metadata": {},
|
|
@@ -51,21 +44,7 @@
|
|
| 51 |
"output_type": "stream",
|
| 52 |
"text": [
|
| 53 |
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 54 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n"
|
| 55 |
-
"plot_prices: date_range=['1980-12-12', '2025-01-31']\n",
|
| 56 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 57 |
-
"plot_prices: date_range=['1991-02-08 09:20', '2025-01-31']\n",
|
| 58 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 59 |
-
"plot_prices: date_range=['1991-04-13 08:40', '2025-01-31']\n",
|
| 60 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 61 |
-
"plot_prices: date_range=['1991-04-13 08:40', '2009-10-25 01:46:40']\n",
|
| 62 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 63 |
-
"plot_prices: date_range=['1998-04-16 20:54:30.1644', '2005-03-24 20:59:54.675']\n",
|
| 64 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 65 |
-
"['2004-03-24', '2005-03-24 20:59:54.675']\n",
|
| 66 |
-
"plot_prices: date_range=['2004-03-24', '2005-03-24 20:59:54.675']\n",
|
| 67 |
-
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 68 |
-
"['2004-03-24', '2005-03-24 20:59:54.675']\n"
|
| 69 |
]
|
| 70 |
}
|
| 71 |
],
|
|
|
|
| 18 |
"id": "5d3ea548",
|
| 19 |
"metadata": {},
|
| 20 |
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
{
|
| 22 |
"data": {
|
| 23 |
"text/html": [
|
|
|
|
| 33 |
" "
|
| 34 |
],
|
| 35 |
"text/plain": [
|
| 36 |
+
"<IPython.lib.display.IFrame at 0x7fd23f91ee60>"
|
| 37 |
]
|
| 38 |
},
|
| 39 |
"metadata": {},
|
|
|
|
| 44 |
"output_type": "stream",
|
| 45 |
"text": [
|
| 46 |
"Selected tickers: AAPL, GOOGL, MSFT\n",
|
| 47 |
+
"Selected tickers: AAPL, GOOGL, MSFT\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
]
|
| 49 |
}
|
| 50 |
],
|
src/dash_app.py
CHANGED
|
@@ -1,8 +1,4 @@
|
|
| 1 |
-
from datetime import timedelta
|
| 2 |
-
|
| 3 |
-
import numpy as np
|
| 4 |
from dash import Dash, Input, Output, State, ctx, dcc, html
|
| 5 |
-
from dateutil import parser # type: ignore
|
| 6 |
|
| 7 |
from src.prices import get_historical_prices
|
| 8 |
from src.style_elements import (
|
|
@@ -10,13 +6,13 @@ from src.style_elements import (
|
|
| 10 |
setup_interval_buttons,
|
| 11 |
setup_ticker_selection,
|
| 12 |
)
|
| 13 |
-
from src.utils import date_to_idx_range, get_date_range
|
| 14 |
|
| 15 |
|
| 16 |
class NormalizedAssetPricesApp:
|
| 17 |
|
| 18 |
-
# TODO: add initial interval
|
| 19 |
-
def __init__(self, initial_tickers=["AAPL", "GOOGL", "MSFT"]):
|
| 20 |
|
| 21 |
self.setup_env(initial_tickers)
|
| 22 |
|
|
@@ -34,31 +30,18 @@ class NormalizedAssetPricesApp:
|
|
| 34 |
self.rolling_changes = self.percentage_changes.rolling(window=251, min_periods=1).sum()
|
| 35 |
self.timestamps = self.prices.index
|
| 36 |
self.idx_range = [0, -1]
|
| 37 |
-
self.normalize_prices()
|
| 38 |
-
# TODO: this is horrible, pls fix
|
| 39 |
-
self.fig = plot_prices(
|
| 40 |
-
self.timestamps, self.prices, self.prices_normalized, self.rolling_changes, [None, None]
|
| 41 |
-
)
|
| 42 |
|
| 43 |
-
|
| 44 |
-
idx0, idx1 = self.idx_range
|
| 45 |
-
date0, date1 = self.timestamps[idx0], self.timestamps[idx1]
|
| 46 |
-
price = self.prices.loc[date0]
|
| 47 |
-
self.prices_normalized = np.nan * self.prices
|
| 48 |
-
self.prices_normalized.loc[date0:date1] = 100 * (self.prices[date0:date1] / price - 1)
|
| 49 |
|
| 50 |
def update_figure(self, date_range=[None, None]):
|
| 51 |
idx_range = date_to_idx_range(self.timestamps, date_range)
|
| 52 |
-
# do not update the figure if the range is unchanged
|
| 53 |
if idx_range != self.idx_range:
|
| 54 |
self.idx_range = idx_range
|
| 55 |
-
self.normalize_prices()
|
| 56 |
self.fig = plot_prices(
|
| 57 |
self.timestamps,
|
| 58 |
self.prices,
|
| 59 |
-
self.prices_normalized,
|
| 60 |
self.rolling_changes,
|
| 61 |
-
|
| 62 |
)
|
| 63 |
return self.fig
|
| 64 |
|
|
@@ -107,22 +90,14 @@ class NormalizedAssetPricesApp:
|
|
| 107 |
date_range = get_date_range(current_figure["layout"])
|
| 108 |
triggered_id = ctx.triggered_id
|
| 109 |
if triggered_id in self.interval_buttons_ids:
|
| 110 |
-
date_range =
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
|
| 113 |
fig = self.update_figure(date_range)
|
| 114 |
return fig
|
| 115 |
|
| 116 |
-
# TODO: filter date_range=[None, None]
|
| 117 |
-
# UPD: might have fixed it by setting the initial idx range to [0, -1]
|
| 118 |
-
def adjust_date_range(self, date_range, button_id):
|
| 119 |
-
start_date, end_date = date_range
|
| 120 |
-
start_date = max(
|
| 121 |
-
parser.parse(end_date) - timedelta(days=self.interval_offsets[button_id]),
|
| 122 |
-
self.timestamps[0],
|
| 123 |
-
).strftime("%Y-%m-%d")
|
| 124 |
-
return [start_date, end_date]
|
| 125 |
-
|
| 126 |
def run(self, **kwargs):
|
| 127 |
self.app.run_server(**kwargs)
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from dash import Dash, Input, Output, State, ctx, dcc, html
|
|
|
|
| 2 |
|
| 3 |
from src.prices import get_historical_prices
|
| 4 |
from src.style_elements import (
|
|
|
|
| 6 |
setup_interval_buttons,
|
| 7 |
setup_ticker_selection,
|
| 8 |
)
|
| 9 |
+
from src.utils import adjust_date_range, date_to_idx_range, get_date_range
|
| 10 |
|
| 11 |
|
| 12 |
class NormalizedAssetPricesApp:
|
| 13 |
|
| 14 |
+
# TODO: add initial interval via self.idx_range
|
| 15 |
+
def __init__(self, initial_tickers=["AAPL", "GOOGL", "MSFT"], initial_interval="1y"):
|
| 16 |
|
| 17 |
self.setup_env(initial_tickers)
|
| 18 |
|
|
|
|
| 30 |
self.rolling_changes = self.percentage_changes.rolling(window=251, min_periods=1).sum()
|
| 31 |
self.timestamps = self.prices.index
|
| 32 |
self.idx_range = [0, -1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
self.fig = plot_prices(self.timestamps, self.prices, self.rolling_changes, self.idx_range)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def update_figure(self, date_range=[None, None]):
|
| 37 |
idx_range = date_to_idx_range(self.timestamps, date_range)
|
|
|
|
| 38 |
if idx_range != self.idx_range:
|
| 39 |
self.idx_range = idx_range
|
|
|
|
| 40 |
self.fig = plot_prices(
|
| 41 |
self.timestamps,
|
| 42 |
self.prices,
|
|
|
|
| 43 |
self.rolling_changes,
|
| 44 |
+
idx_range,
|
| 45 |
)
|
| 46 |
return self.fig
|
| 47 |
|
|
|
|
| 90 |
date_range = get_date_range(current_figure["layout"])
|
| 91 |
triggered_id = ctx.triggered_id
|
| 92 |
if triggered_id in self.interval_buttons_ids:
|
| 93 |
+
date_range = adjust_date_range(
|
| 94 |
+
self.timestamps, self.interval_offsets, date_range, triggered_id
|
| 95 |
+
)
|
| 96 |
+
print(f"interval update: {date_range=}")
|
| 97 |
|
| 98 |
fig = self.update_figure(date_range)
|
| 99 |
return fig
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
def run(self, **kwargs):
|
| 102 |
self.app.run_server(**kwargs)
|
| 103 |
|
src/style_elements.py
CHANGED
|
@@ -5,6 +5,7 @@ import plotly.graph_objects as go
|
|
| 5 |
from dash import dcc, html
|
| 6 |
|
| 7 |
from src.prices import get_available_tickers
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def setup_ticker_selection(initial_tickers):
|
|
@@ -64,8 +65,11 @@ def setup_interval_buttons():
|
|
| 64 |
return interval_buttons_html, interval_buttons_ids, interval_offsets
|
| 65 |
|
| 66 |
|
| 67 |
-
def plot_prices(timestamps, prices,
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
fig = go.Figure()
|
| 70 |
|
| 71 |
# rangeslider plot
|
|
@@ -88,7 +92,7 @@ def plot_prices(timestamps, prices, prices_normalized, rolling_changes, date_ran
|
|
| 88 |
fig.add_trace(
|
| 89 |
go.Scatter(
|
| 90 |
x=timestamps,
|
| 91 |
-
y=prices_normalized[asset],
|
| 92 |
line=dict(width=3, color=next(colors)),
|
| 93 |
name=asset,
|
| 94 |
xaxis="x2",
|
|
@@ -111,11 +115,11 @@ def plot_prices(timestamps, prices, prices_normalized, rolling_changes, date_ran
|
|
| 111 |
# configure axes
|
| 112 |
xaxis1_dict = dict(rangeslider=dict(visible=True, thickness=0.1), tickangle=-30, nticks=20)
|
| 113 |
xaxis2_dict = dict(matches="x1", showticklabels=False)
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
xaxis2_dict["range"] = date_range
|
| 117 |
yaxis1_dict = dict(showticklabels=False)
|
| 118 |
yaxis2_dict = dict(
|
|
|
|
| 119 |
title="relative price change",
|
| 120 |
nticks=12,
|
| 121 |
tickformat=".0f",
|
|
|
|
| 5 |
from dash import dcc, html
|
| 6 |
|
| 7 |
from src.prices import get_available_tickers
|
| 8 |
+
from src.utils import normalize_prices
|
| 9 |
|
| 10 |
|
| 11 |
def setup_ticker_selection(initial_tickers):
|
|
|
|
| 65 |
return interval_buttons_html, interval_buttons_ids, interval_offsets
|
| 66 |
|
| 67 |
|
| 68 |
+
def plot_prices(timestamps, prices, rolling_changes, idx_range):
|
| 69 |
+
idx0, idx1 = idx_range
|
| 70 |
+
date_range = [timestamps[idx0], timestamps[idx1]]
|
| 71 |
+
prices_normalized = normalize_prices(prices, date_range)
|
| 72 |
+
|
| 73 |
fig = go.Figure()
|
| 74 |
|
| 75 |
# rangeslider plot
|
|
|
|
| 92 |
fig.add_trace(
|
| 93 |
go.Scatter(
|
| 94 |
x=timestamps,
|
| 95 |
+
y=100 * prices_normalized[asset],
|
| 96 |
line=dict(width=3, color=next(colors)),
|
| 97 |
name=asset,
|
| 98 |
xaxis="x2",
|
|
|
|
| 115 |
# configure axes
|
| 116 |
xaxis1_dict = dict(rangeslider=dict(visible=True, thickness=0.1), tickangle=-30, nticks=20)
|
| 117 |
xaxis2_dict = dict(matches="x1", showticklabels=False)
|
| 118 |
+
xaxis1_dict["range"] = date_range
|
| 119 |
+
xaxis2_dict["range"] = date_range
|
|
|
|
| 120 |
yaxis1_dict = dict(showticklabels=False)
|
| 121 |
yaxis2_dict = dict(
|
| 122 |
+
autorange=True,
|
| 123 |
title="relative price change",
|
| 124 |
nticks=12,
|
| 125 |
tickformat=".0f",
|
src/utils.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
def date_to_idx_range(timestamps, date_range):
|
| 2 |
idx_range = (
|
| 3 |
timestamps.get_indexer(date_range, method="nearest").tolist()
|
|
@@ -18,3 +24,19 @@ def get_date_range(figure_layout):
|
|
| 18 |
# else:
|
| 19 |
# print(figure_layout)
|
| 20 |
return date_range
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import timedelta
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
from dateutil import parser # type: ignore
|
| 5 |
+
|
| 6 |
+
|
| 7 |
def date_to_idx_range(timestamps, date_range):
|
| 8 |
idx_range = (
|
| 9 |
timestamps.get_indexer(date_range, method="nearest").tolist()
|
|
|
|
| 24 |
# else:
|
| 25 |
# print(figure_layout)
|
| 26 |
return date_range
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def adjust_date_range(timestamps, interval_offsets, date_range, button_id):
|
| 30 |
+
start_date, end_date = date_range
|
| 31 |
+
start_date = max(
|
| 32 |
+
parser.parse(end_date) - timedelta(days=interval_offsets[button_id]),
|
| 33 |
+
timestamps[0],
|
| 34 |
+
).strftime("%Y-%m-%d")
|
| 35 |
+
return [start_date, end_date]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def normalize_prices(prices, date_range):
|
| 39 |
+
date0, date1 = date_range
|
| 40 |
+
prices_normalized = np.nan * prices
|
| 41 |
+
prices_normalized.loc[date0:date1] = prices[date0:date1] / prices.loc[date0] - 1
|
| 42 |
+
return prices_normalized
|