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.

@@ -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,3 @@
1
+ include requirements.txt
2
+ recursive-include panelbeater *.py
3
+ recursive-include panelbeater *.csv
@@ -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,3 @@
1
+ """panelbeater initialisation."""
2
+
3
+ __VERSION__ = "0.0.1"
@@ -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,2 @@
1
+ [console_scripts]
2
+ panelbeater = panelbeater.__main__:main
@@ -0,0 +1,9 @@
1
+ yfinance==0.2.66
2
+ pandas>=2.3.3
3
+ pandas-datareader>=0.10.0
4
+ numpy>=2.3.4
5
+ feature-engine>=1.9.3
6
+ requests-cache>=1.2.1
7
+ scikit-learn>=1.6.1
8
+ wavetrainer>=0.2.37
9
+ tqdm>=4.67.1
@@ -0,0 +1 @@
1
+ panelbeater
@@ -0,0 +1,9 @@
1
+ yfinance==0.2.66
2
+ pandas>=2.3.3
3
+ pandas-datareader>=0.10.0
4
+ numpy>=2.3.4
5
+ feature-engine>=1.9.3
6
+ requests-cache>=1.2.1
7
+ scikit-learn>=1.6.1
8
+ wavetrainer>=0.2.37
9
+ tqdm>=4.67.1
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )