panelbeater 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of panelbeater might be problematic. Click here for more details.
- panelbeater-0.0.1/LICENSE +21 -0
- panelbeater-0.0.1/MANIFEST.in +3 -0
- panelbeater-0.0.1/PKG-INFO +82 -0
- panelbeater-0.0.1/README.md +59 -0
- panelbeater-0.0.1/panelbeater/__init__.py +3 -0
- panelbeater-0.0.1/panelbeater/__main__.py +69 -0
- panelbeater-0.0.1/panelbeater/download.py +70 -0
- panelbeater-0.0.1/panelbeater/features.py +55 -0
- panelbeater-0.0.1/panelbeater/normalizer.py +21 -0
- panelbeater-0.0.1/panelbeater.egg-info/PKG-INFO +82 -0
- panelbeater-0.0.1/panelbeater.egg-info/SOURCES.txt +17 -0
- panelbeater-0.0.1/panelbeater.egg-info/dependency_links.txt +1 -0
- panelbeater-0.0.1/panelbeater.egg-info/entry_points.txt +2 -0
- panelbeater-0.0.1/panelbeater.egg-info/not-zip-safe +1 -0
- panelbeater-0.0.1/panelbeater.egg-info/requires.txt +9 -0
- panelbeater-0.0.1/panelbeater.egg-info/top_level.txt +1 -0
- panelbeater-0.0.1/requirements.txt +9 -0
- panelbeater-0.0.1/setup.cfg +4 -0
- panelbeater-0.0.1/setup.py +45 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Will Sackfield
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: panelbeater
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A CLI for finding mispriced options.
|
|
5
|
+
Home-page: https://github.com/8W9aG/panelbeater
|
|
6
|
+
Author: Will Sackfield
|
|
7
|
+
Author-email: will.sackfield@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: options
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: yfinance==0.2.66
|
|
15
|
+
Requires-Dist: pandas>=2.3.3
|
|
16
|
+
Requires-Dist: pandas-datareader>=0.10.0
|
|
17
|
+
Requires-Dist: numpy>=2.3.4
|
|
18
|
+
Requires-Dist: feature-engine>=1.9.3
|
|
19
|
+
Requires-Dist: requests-cache>=1.2.1
|
|
20
|
+
Requires-Dist: scikit-learn>=1.6.1
|
|
21
|
+
Requires-Dist: wavetrainer>=0.2.37
|
|
22
|
+
Requires-Dist: tqdm>=4.67.1
|
|
23
|
+
|
|
24
|
+
# panelbeater
|
|
25
|
+
|
|
26
|
+
<a href="https://pypi.org/project/panelbeater/">
|
|
27
|
+
<img alt="PyPi" src="https://img.shields.io/pypi/v/panelbeater">
|
|
28
|
+
</a>
|
|
29
|
+
|
|
30
|
+
A CLI for finding mispriced options.
|
|
31
|
+
|
|
32
|
+
## Dependencies :globe_with_meridians:
|
|
33
|
+
|
|
34
|
+
Python 3.11.6:
|
|
35
|
+
|
|
36
|
+
- [yfinance](https://ranaroussi.github.io/yfinance/)
|
|
37
|
+
- [pandas](https://pandas.pydata.org/)
|
|
38
|
+
- [pandas-datareader](https://pandas-datareader.readthedocs.io/en/latest/)
|
|
39
|
+
- [numpy](https://numpy.org/)
|
|
40
|
+
- [feature-engine](https://feature-engine.trainindata.com/en/latest/)
|
|
41
|
+
- [requests-cache](https://requests-cache.readthedocs.io/en/stable/)
|
|
42
|
+
- [scikit-learn](https://scikit-learn.org/stable/)
|
|
43
|
+
- [wavetrainer](https://github.com/8W9aG/wavetrainer/)
|
|
44
|
+
- [tqdm](https://tqdm.github.io/)
|
|
45
|
+
|
|
46
|
+
## Raison D'être :thought_balloon:
|
|
47
|
+
|
|
48
|
+
`panelbeater` trains models at t+X iteratively to come up with the calibrated expected distribution of an asset price in the future. It then finds the current prices of options for an asset, and determines whether it should be bought and for how much.
|
|
49
|
+
|
|
50
|
+
## Architecture :triangular_ruler:
|
|
51
|
+
|
|
52
|
+
`panelbeater` goes through the following steps:
|
|
53
|
+
1. Downloads the historical data.
|
|
54
|
+
2. Performs feature engineering on the data.
|
|
55
|
+
3. Trains the required models to operate on the data panel.
|
|
56
|
+
4. Downloads the current data.
|
|
57
|
+
5. Runs inference on t+X for the latest options to find the probability distribution on the asset prices to their expiry dates.
|
|
58
|
+
6. Finds any mispriced options and size the position accordingly.
|
|
59
|
+
|
|
60
|
+
## Installation :inbox_tray:
|
|
61
|
+
|
|
62
|
+
This is a python package hosted on pypi, so to install simply run the following command:
|
|
63
|
+
|
|
64
|
+
`pip install panelbeater`
|
|
65
|
+
|
|
66
|
+
or install using this local repository:
|
|
67
|
+
|
|
68
|
+
`python setup.py install --old-and-unmanageable`
|
|
69
|
+
|
|
70
|
+
## Usage example :eyes:
|
|
71
|
+
|
|
72
|
+
You can run `panelbeater` as a CLI like so:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
panelbeater
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This performs a full train, inference and attempts to find mispriced options.
|
|
79
|
+
|
|
80
|
+
## License :memo:
|
|
81
|
+
|
|
82
|
+
The project is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# panelbeater
|
|
2
|
+
|
|
3
|
+
<a href="https://pypi.org/project/panelbeater/">
|
|
4
|
+
<img alt="PyPi" src="https://img.shields.io/pypi/v/panelbeater">
|
|
5
|
+
</a>
|
|
6
|
+
|
|
7
|
+
A CLI for finding mispriced options.
|
|
8
|
+
|
|
9
|
+
## Dependencies :globe_with_meridians:
|
|
10
|
+
|
|
11
|
+
Python 3.11.6:
|
|
12
|
+
|
|
13
|
+
- [yfinance](https://ranaroussi.github.io/yfinance/)
|
|
14
|
+
- [pandas](https://pandas.pydata.org/)
|
|
15
|
+
- [pandas-datareader](https://pandas-datareader.readthedocs.io/en/latest/)
|
|
16
|
+
- [numpy](https://numpy.org/)
|
|
17
|
+
- [feature-engine](https://feature-engine.trainindata.com/en/latest/)
|
|
18
|
+
- [requests-cache](https://requests-cache.readthedocs.io/en/stable/)
|
|
19
|
+
- [scikit-learn](https://scikit-learn.org/stable/)
|
|
20
|
+
- [wavetrainer](https://github.com/8W9aG/wavetrainer/)
|
|
21
|
+
- [tqdm](https://tqdm.github.io/)
|
|
22
|
+
|
|
23
|
+
## Raison D'être :thought_balloon:
|
|
24
|
+
|
|
25
|
+
`panelbeater` trains models at t+X iteratively to come up with the calibrated expected distribution of an asset price in the future. It then finds the current prices of options for an asset, and determines whether it should be bought and for how much.
|
|
26
|
+
|
|
27
|
+
## Architecture :triangular_ruler:
|
|
28
|
+
|
|
29
|
+
`panelbeater` goes through the following steps:
|
|
30
|
+
1. Downloads the historical data.
|
|
31
|
+
2. Performs feature engineering on the data.
|
|
32
|
+
3. Trains the required models to operate on the data panel.
|
|
33
|
+
4. Downloads the current data.
|
|
34
|
+
5. Runs inference on t+X for the latest options to find the probability distribution on the asset prices to their expiry dates.
|
|
35
|
+
6. Finds any mispriced options and size the position accordingly.
|
|
36
|
+
|
|
37
|
+
## Installation :inbox_tray:
|
|
38
|
+
|
|
39
|
+
This is a python package hosted on pypi, so to install simply run the following command:
|
|
40
|
+
|
|
41
|
+
`pip install panelbeater`
|
|
42
|
+
|
|
43
|
+
or install using this local repository:
|
|
44
|
+
|
|
45
|
+
`python setup.py install --old-and-unmanageable`
|
|
46
|
+
|
|
47
|
+
## Usage example :eyes:
|
|
48
|
+
|
|
49
|
+
You can run `panelbeater` as a CLI like so:
|
|
50
|
+
|
|
51
|
+
```shell
|
|
52
|
+
panelbeater
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This performs a full train, inference and attempts to find mispriced options.
|
|
56
|
+
|
|
57
|
+
## License :memo:
|
|
58
|
+
|
|
59
|
+
The project is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""The CLI for finding mispriced options."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
import requests_cache
|
|
6
|
+
import wavetrainer as wt
|
|
7
|
+
|
|
8
|
+
from .download import download
|
|
9
|
+
from .features import features
|
|
10
|
+
from .normalizer import normalize
|
|
11
|
+
|
|
12
|
+
_TICKERS = [
|
|
13
|
+
# Equities
|
|
14
|
+
"SPY",
|
|
15
|
+
"QQQ",
|
|
16
|
+
"EEM",
|
|
17
|
+
# Commodities
|
|
18
|
+
"GC=F",
|
|
19
|
+
"CL=F",
|
|
20
|
+
"SI=F",
|
|
21
|
+
# FX
|
|
22
|
+
"EURUSD=X",
|
|
23
|
+
"USDJPY=X",
|
|
24
|
+
# Crypto
|
|
25
|
+
"BTC-USD",
|
|
26
|
+
"ETH-USD",
|
|
27
|
+
]
|
|
28
|
+
_MACROS = [
|
|
29
|
+
"GDP",
|
|
30
|
+
"UNRATE",
|
|
31
|
+
"CPIAUCSL",
|
|
32
|
+
"FEDFUNDS",
|
|
33
|
+
"DGS10",
|
|
34
|
+
"T10Y2Y",
|
|
35
|
+
"M2SL",
|
|
36
|
+
"VIXCLS",
|
|
37
|
+
"DTWEXBGS",
|
|
38
|
+
"INDPRO",
|
|
39
|
+
]
|
|
40
|
+
_WINDOWS = [
|
|
41
|
+
5,
|
|
42
|
+
10,
|
|
43
|
+
20,
|
|
44
|
+
60,
|
|
45
|
+
120,
|
|
46
|
+
200,
|
|
47
|
+
]
|
|
48
|
+
_LAGS = [1, 3, 5, 10, 20, 30]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main() -> None:
|
|
52
|
+
"""The main CLI function."""
|
|
53
|
+
session = requests_cache.CachedSession("panelbeater-cache")
|
|
54
|
+
wavetrainer = wt.create(
|
|
55
|
+
"panelbeater-train",
|
|
56
|
+
walkforward_timedelta=datetime.timedelta(days=7),
|
|
57
|
+
validation_size=datetime.timedelta(days=365),
|
|
58
|
+
test_size=datetime.timedelta(days=365),
|
|
59
|
+
allowed_models={"catboost"},
|
|
60
|
+
max_false_positive_reduction_steps=0,
|
|
61
|
+
)
|
|
62
|
+
df_y = download(tickers=_TICKERS, macros=_MACROS, session=session)
|
|
63
|
+
df_x = features(df=df_y.copy(), windows=_WINDOWS, lags=_LAGS)
|
|
64
|
+
df_y = normalize(df=df_y)
|
|
65
|
+
wavetrainer.fit(df_x, y=df_y)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Download historical data."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import requests_cache
|
|
6
|
+
import tqdm
|
|
7
|
+
import yfinance as yf
|
|
8
|
+
from pandas_datareader import data as fred
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _load_yahoo_prices(tickers: list[str]) -> pd.DataFrame:
|
|
12
|
+
"""Adj Close for all tickers, daily."""
|
|
13
|
+
print(f"Download tickers: {tickers}")
|
|
14
|
+
px = yf.download(
|
|
15
|
+
tickers,
|
|
16
|
+
start="2000-01-01",
|
|
17
|
+
end=None,
|
|
18
|
+
auto_adjust=True,
|
|
19
|
+
progress=False,
|
|
20
|
+
)
|
|
21
|
+
if px is None:
|
|
22
|
+
raise ValueError("px is null")
|
|
23
|
+
if not isinstance(px, pd.DataFrame):
|
|
24
|
+
raise ValueError("px is not a dataframe")
|
|
25
|
+
px = px["Close"]
|
|
26
|
+
if isinstance(px.columns, pd.MultiIndex):
|
|
27
|
+
px = px.droplevel(0, axis=1)
|
|
28
|
+
pxf = px.sort_index().astype(float)
|
|
29
|
+
if not isinstance(pxf, pd.DataFrame):
|
|
30
|
+
raise ValueError("pxf is not a dataframe")
|
|
31
|
+
return pxf
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_fred_series(
|
|
35
|
+
codes: list[str], session: requests_cache.CachedSession
|
|
36
|
+
) -> pd.DataFrame:
|
|
37
|
+
"""Load FRED series, forward-fill to daily to align with markets."""
|
|
38
|
+
dfs = []
|
|
39
|
+
for code in tqdm.tqdm(codes, desc="Downloading macros"):
|
|
40
|
+
s = fred.DataReader(code, "fred", start="2000-01-01", session=session)
|
|
41
|
+
s.columns = [code]
|
|
42
|
+
dfs.append(s)
|
|
43
|
+
macro = pd.concat(dfs, axis=1).sort_index()
|
|
44
|
+
# daily frequency with forward-fill (macro is slower cadence)
|
|
45
|
+
macro = macro.asfreq("D").ffill()
|
|
46
|
+
return macro
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def download(
|
|
50
|
+
tickers: list[str], macros: list[str], session: requests_cache.CachedSession
|
|
51
|
+
) -> pd.DataFrame:
|
|
52
|
+
"""Download the historical data."""
|
|
53
|
+
prices = _load_yahoo_prices(tickers=tickers)
|
|
54
|
+
macro = _load_fred_series(codes=macros, session=session)
|
|
55
|
+
idx = prices.index.union(macro.index)
|
|
56
|
+
prices = prices.reindex(idx).ffill()
|
|
57
|
+
macro = macro.reindex(idx).ffill()
|
|
58
|
+
prices_min = prices.dropna(how="all").index.min()
|
|
59
|
+
macro_min = macro.dropna(how="all").index.min()
|
|
60
|
+
common_start = max(prices_min, macro_min) # type: ignore
|
|
61
|
+
prices = prices.loc[common_start:]
|
|
62
|
+
macro = macro.loc[common_start:]
|
|
63
|
+
levels = pd.concat(
|
|
64
|
+
[prices.add_prefix("PX_"), macro.add_prefix("MACRO_")], axis=1
|
|
65
|
+
).ffill()
|
|
66
|
+
return (
|
|
67
|
+
levels.replace([np.inf, -np.inf], np.nan)
|
|
68
|
+
.pct_change(fill_method=None)
|
|
69
|
+
.replace([np.inf, -np.inf], np.nan)
|
|
70
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Generate features over a dataframe."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import tqdm
|
|
8
|
+
from feature_engine.datetime import DatetimeFeatures
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _ticker_features(df: pd.DataFrame, windows: list[int]) -> pd.DataFrame:
|
|
12
|
+
cols = df.columns.values.tolist()
|
|
13
|
+
for col in tqdm.tqdm(cols, desc="Generating ticker features"):
|
|
14
|
+
s = df[col]
|
|
15
|
+
for w in windows:
|
|
16
|
+
with warnings.catch_warnings():
|
|
17
|
+
warnings.simplefilter("ignore", category=pd.errors.PerformanceWarning)
|
|
18
|
+
# SMA
|
|
19
|
+
sma = s.rolling(w).mean()
|
|
20
|
+
df[f"{col}_sma_{w}"] = sma / s - 1
|
|
21
|
+
# PCT
|
|
22
|
+
df[f"{col}_pctchg_{w}"] = s.pct_change(w, fill_method=None)
|
|
23
|
+
# Z-Score
|
|
24
|
+
mu = s.rolling(w).mean()
|
|
25
|
+
sigma = s.rolling(w).std()
|
|
26
|
+
df[f"{col}_z_{w}"] = (s - mu) / sigma
|
|
27
|
+
return df
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _meta_ticker_feature(
|
|
31
|
+
df: pd.DataFrame, lags: list[int], windows: list[int]
|
|
32
|
+
) -> pd.DataFrame:
|
|
33
|
+
dfs = [df]
|
|
34
|
+
for lag in tqdm.tqdm(lags, desc="Generating lags"):
|
|
35
|
+
dfs.append(df.shift(lag).add_suffix(f"_lag{lag}"))
|
|
36
|
+
for window in tqdm.tqdm(windows, desc="Generating window features"):
|
|
37
|
+
dfs.append(df.rolling(window).mean().add_suffix(f"_rmean{window}")) # type: ignore
|
|
38
|
+
dfs.append(df.rolling(window).std().add_suffix(f"_rstd{window}")) # type: ignore
|
|
39
|
+
dfs.append(df.diff().add_suffix("_diff1"))
|
|
40
|
+
return pd.concat(dfs, axis=1).replace([np.inf, -np.inf], np.nan)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _dt_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
44
|
+
print("Generating datetime features")
|
|
45
|
+
dtf = DatetimeFeatures(features_to_extract="all", variables="index")
|
|
46
|
+
return dtf.fit_transform(df)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def features(df: pd.DataFrame, windows: list[int], lags: list[int]) -> pd.DataFrame:
|
|
50
|
+
"""Generate features on a dataframe."""
|
|
51
|
+
cols = df.columns.values.tolist()
|
|
52
|
+
df = _ticker_features(df=df, windows=windows)
|
|
53
|
+
df = _meta_ticker_feature(df, lags=lags, windows=windows)
|
|
54
|
+
df = _dt_features(df=df)
|
|
55
|
+
return df.drop(columns=cols)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Normalize the Y targets to standard deviations."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import tqdm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize(df: pd.DataFrame) -> pd.DataFrame:
|
|
10
|
+
"""Normalize the dataframe per column by z-score bucketing."""
|
|
11
|
+
mu = df.rolling(365).mean()
|
|
12
|
+
sigma = df.rolling(365).std()
|
|
13
|
+
df = ((((df - mu) / sigma) * 2.0).round() / 2.0).clip(-3, 3)
|
|
14
|
+
dfs = []
|
|
15
|
+
for col in tqdm.tqdm(df.columns, desc="Normalising targets"):
|
|
16
|
+
for unique_val in df[col].unique():
|
|
17
|
+
if math.isnan(unique_val):
|
|
18
|
+
continue
|
|
19
|
+
s = (df[col] == unique_val).rename(f"{col}_{unique_val}")
|
|
20
|
+
dfs.append(s)
|
|
21
|
+
return pd.concat(dfs, axis=1)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: panelbeater
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A CLI for finding mispriced options.
|
|
5
|
+
Home-page: https://github.com/8W9aG/panelbeater
|
|
6
|
+
Author: Will Sackfield
|
|
7
|
+
Author-email: will.sackfield@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: options
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: yfinance==0.2.66
|
|
15
|
+
Requires-Dist: pandas>=2.3.3
|
|
16
|
+
Requires-Dist: pandas-datareader>=0.10.0
|
|
17
|
+
Requires-Dist: numpy>=2.3.4
|
|
18
|
+
Requires-Dist: feature-engine>=1.9.3
|
|
19
|
+
Requires-Dist: requests-cache>=1.2.1
|
|
20
|
+
Requires-Dist: scikit-learn>=1.6.1
|
|
21
|
+
Requires-Dist: wavetrainer>=0.2.37
|
|
22
|
+
Requires-Dist: tqdm>=4.67.1
|
|
23
|
+
|
|
24
|
+
# panelbeater
|
|
25
|
+
|
|
26
|
+
<a href="https://pypi.org/project/panelbeater/">
|
|
27
|
+
<img alt="PyPi" src="https://img.shields.io/pypi/v/panelbeater">
|
|
28
|
+
</a>
|
|
29
|
+
|
|
30
|
+
A CLI for finding mispriced options.
|
|
31
|
+
|
|
32
|
+
## Dependencies :globe_with_meridians:
|
|
33
|
+
|
|
34
|
+
Python 3.11.6:
|
|
35
|
+
|
|
36
|
+
- [yfinance](https://ranaroussi.github.io/yfinance/)
|
|
37
|
+
- [pandas](https://pandas.pydata.org/)
|
|
38
|
+
- [pandas-datareader](https://pandas-datareader.readthedocs.io/en/latest/)
|
|
39
|
+
- [numpy](https://numpy.org/)
|
|
40
|
+
- [feature-engine](https://feature-engine.trainindata.com/en/latest/)
|
|
41
|
+
- [requests-cache](https://requests-cache.readthedocs.io/en/stable/)
|
|
42
|
+
- [scikit-learn](https://scikit-learn.org/stable/)
|
|
43
|
+
- [wavetrainer](https://github.com/8W9aG/wavetrainer/)
|
|
44
|
+
- [tqdm](https://tqdm.github.io/)
|
|
45
|
+
|
|
46
|
+
## Raison D'être :thought_balloon:
|
|
47
|
+
|
|
48
|
+
`panelbeater` trains models at t+X iteratively to come up with the calibrated expected distribution of an asset price in the future. It then finds the current prices of options for an asset, and determines whether it should be bought and for how much.
|
|
49
|
+
|
|
50
|
+
## Architecture :triangular_ruler:
|
|
51
|
+
|
|
52
|
+
`panelbeater` goes through the following steps:
|
|
53
|
+
1. Downloads the historical data.
|
|
54
|
+
2. Performs feature engineering on the data.
|
|
55
|
+
3. Trains the required models to operate on the data panel.
|
|
56
|
+
4. Downloads the current data.
|
|
57
|
+
5. Runs inference on t+X for the latest options to find the probability distribution on the asset prices to their expiry dates.
|
|
58
|
+
6. Finds any mispriced options and size the position accordingly.
|
|
59
|
+
|
|
60
|
+
## Installation :inbox_tray:
|
|
61
|
+
|
|
62
|
+
This is a python package hosted on pypi, so to install simply run the following command:
|
|
63
|
+
|
|
64
|
+
`pip install panelbeater`
|
|
65
|
+
|
|
66
|
+
or install using this local repository:
|
|
67
|
+
|
|
68
|
+
`python setup.py install --old-and-unmanageable`
|
|
69
|
+
|
|
70
|
+
## Usage example :eyes:
|
|
71
|
+
|
|
72
|
+
You can run `panelbeater` as a CLI like so:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
panelbeater
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This performs a full train, inference and attempts to find mispriced options.
|
|
79
|
+
|
|
80
|
+
## License :memo:
|
|
81
|
+
|
|
82
|
+
The project is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
requirements.txt
|
|
5
|
+
setup.py
|
|
6
|
+
panelbeater/__init__.py
|
|
7
|
+
panelbeater/__main__.py
|
|
8
|
+
panelbeater/download.py
|
|
9
|
+
panelbeater/features.py
|
|
10
|
+
panelbeater/normalizer.py
|
|
11
|
+
panelbeater.egg-info/PKG-INFO
|
|
12
|
+
panelbeater.egg-info/SOURCES.txt
|
|
13
|
+
panelbeater.egg-info/dependency_links.txt
|
|
14
|
+
panelbeater.egg-info/entry_points.txt
|
|
15
|
+
panelbeater.egg-info/not-zip-safe
|
|
16
|
+
panelbeater.egg-info/requires.txt
|
|
17
|
+
panelbeater.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
panelbeater
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Setup panelbeater."""
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
readme_path = Path(__file__).absolute().parent.joinpath('README.md')
|
|
7
|
+
long_description = readme_path.read_text(encoding='utf-8')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def install_requires() -> typing.List[str]:
|
|
11
|
+
"""Find the install requires strings from requirements.txt"""
|
|
12
|
+
requires = []
|
|
13
|
+
with open(
|
|
14
|
+
Path(__file__).absolute().parent.joinpath('requirements.txt'), "r"
|
|
15
|
+
) as requirments_txt_handle:
|
|
16
|
+
requires = [
|
|
17
|
+
x
|
|
18
|
+
for x in requirments_txt_handle
|
|
19
|
+
if not x.startswith(".") and not x.startswith("-e")
|
|
20
|
+
]
|
|
21
|
+
return requires
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
setup(
|
|
25
|
+
name='panelbeater',
|
|
26
|
+
version='0.0.1',
|
|
27
|
+
description='A CLI for finding mispriced options.',
|
|
28
|
+
long_description=long_description,
|
|
29
|
+
long_description_content_type='text/markdown',
|
|
30
|
+
classifiers=[
|
|
31
|
+
'License :: OSI Approved :: MIT License',
|
|
32
|
+
'Programming Language :: Python :: 3',
|
|
33
|
+
],
|
|
34
|
+
keywords='options',
|
|
35
|
+
url='https://github.com/8W9aG/panelbeater',
|
|
36
|
+
author='Will Sackfield',
|
|
37
|
+
author_email='will.sackfield@gmail.com',
|
|
38
|
+
license='MIT',
|
|
39
|
+
install_requires=install_requires(),
|
|
40
|
+
zip_safe=False,
|
|
41
|
+
packages=find_packages(),
|
|
42
|
+
entry_points = {
|
|
43
|
+
'console_scripts': ['panelbeater=panelbeater.__main__:main'],
|
|
44
|
+
},
|
|
45
|
+
)
|