pwb-toolbox 0.1.9__tar.gz → 0.1.11__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.
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/PKG-INFO +1 -1
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/backtest/__init__.py +2 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/backtest/engine.py +12 -2
- pwb_toolbox-0.1.11/pwb_toolbox/backtest/portfolio.py +30 -0
- pwb_toolbox-0.1.11/pwb_toolbox/backtest/universe.py +37 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/datasets/__init__.py +26 -16
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox.egg-info/PKG-INFO +1 -1
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox.egg-info/SOURCES.txt +2 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/setup.cfg +1 -1
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/LICENSE.txt +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/README.md +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/__init__.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/backtest/base_strategy.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/backtest/ib_connector.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/performance/__init__.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/performance/metrics.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/performance/plots.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox/performance/trade_stats.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox.egg-info/dependency_links.txt +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox.egg-info/requires.txt +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pwb_toolbox.egg-info/top_level.txt +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/pyproject.toml +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_backtest.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_execution_models.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_hf_token.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_ib_connector.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_portfolio_models.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_risk_models.py +0 -0
- {pwb_toolbox-0.1.9 → pwb_toolbox-0.1.11}/tests/test_universe_models.py +0 -0
@@ -3,7 +3,16 @@ import pandas as pd
|
|
3
3
|
import pwb_toolbox.datasets as pwb_ds
|
4
4
|
|
5
5
|
|
6
|
-
def run_strategy(
|
6
|
+
def run_strategy(
|
7
|
+
signal,
|
8
|
+
signal_kwargs,
|
9
|
+
portfolio,
|
10
|
+
portfolio_kwargs,
|
11
|
+
symbols,
|
12
|
+
start_date,
|
13
|
+
leverage,
|
14
|
+
cash,
|
15
|
+
):
|
7
16
|
"""Run a tactical asset allocation strategy with Backtrader."""
|
8
17
|
# Load the data from https://paperswithbacktest.com/datasets
|
9
18
|
pivot_df = pwb_ds.get_pricing(
|
@@ -18,7 +27,7 @@ def run_strategy(signal, signal_kwargs, portfolio, symbols, start_date, leverage
|
|
18
27
|
pivot_df.ffill(inplace=True) # forward-fill holidays
|
19
28
|
pivot_df.bfill(inplace=True) # back-fill leading IPO gaps
|
20
29
|
cerebro = bt.Cerebro()
|
21
|
-
for symbol in
|
30
|
+
for symbol in pivot_df.columns.levels[0]:
|
22
31
|
data = bt.feeds.PandasData(dataname=pivot_df[symbol].copy())
|
23
32
|
cerebro.adddata(data, name=symbol)
|
24
33
|
cerebro.addstrategy(
|
@@ -27,6 +36,7 @@ def run_strategy(signal, signal_kwargs, portfolio, symbols, start_date, leverage
|
|
27
36
|
leverage=0.9,
|
28
37
|
signal_cls=signal,
|
29
38
|
signal_kwargs=signal_kwargs,
|
39
|
+
**portfolio_kwargs,
|
30
40
|
)
|
31
41
|
cerebro.broker.set_cash(cash)
|
32
42
|
strategy = cerebro.run()[0]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from .base_strategy import BaseStrategy
|
2
|
+
|
3
|
+
|
4
|
+
class MonthlyEqualWeightPortfolio(BaseStrategy):
|
5
|
+
params = (
|
6
|
+
("leverage", 0.9),
|
7
|
+
("signal_cls", None),
|
8
|
+
("signal_kwargs", {}),
|
9
|
+
)
|
10
|
+
|
11
|
+
def __init__(self):
|
12
|
+
super().__init__()
|
13
|
+
self.sig = {
|
14
|
+
d._name: self.p.signal_cls(d, **self.p.signal_kwargs) for d in self.datas
|
15
|
+
}
|
16
|
+
self.last_month = -1
|
17
|
+
|
18
|
+
def next(self):
|
19
|
+
"""Rebalance portfolio at the start of each month."""
|
20
|
+
super().next()
|
21
|
+
today = self.datas[0].datetime.date(0)
|
22
|
+
if today.month == self.last_month:
|
23
|
+
return
|
24
|
+
self.last_month = today.month
|
25
|
+
longs = [
|
26
|
+
d for d in self.datas if self.is_tradable(d) and self.sig[d._name][0] == 1
|
27
|
+
]
|
28
|
+
wt = (self.p.leverage / len(longs)) if longs else 0.0
|
29
|
+
for d in self.datas:
|
30
|
+
self.order_target_percent(d, target=wt if d in longs else 0)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from typing import List
|
2
|
+
import pandas as pd
|
3
|
+
import pwb_toolbox.datasets as pwb_ds
|
4
|
+
|
5
|
+
|
6
|
+
def get_most_liquid_symbols(n: int = 1_200) -> List[str]:
|
7
|
+
"""Return the `n` most liquid stock symbols (volume × price on last bar)."""
|
8
|
+
df = pwb_ds.load_dataset("Stocks-Daily-Price", adjust=True, extend=True)
|
9
|
+
last_date = df["date"].max()
|
10
|
+
today = df[df["date"] == last_date].copy()
|
11
|
+
today["liquidity"] = today["volume"] * today["close"]
|
12
|
+
liquid = today.sort_values("liquidity", ascending=False)
|
13
|
+
return liquid["symbol"].tolist()[:n]
|
14
|
+
|
15
|
+
|
16
|
+
def get_least_volatile_symbols(symbols=["sp500"], start="1990-01-01") -> List[str]:
|
17
|
+
pivot = pwb_ds.get_pricing(
|
18
|
+
symbol_list=symbols,
|
19
|
+
fields=["open", "high", "low", "close"],
|
20
|
+
start_date=start,
|
21
|
+
extend=True,
|
22
|
+
)
|
23
|
+
td = pd.bdate_range(pivot.index.min(), pivot.index.max())
|
24
|
+
pivot = pivot.reindex(td).ffill().bfill()
|
25
|
+
symbols = []
|
26
|
+
for sym in pivot.columns.levels[0]:
|
27
|
+
df = (
|
28
|
+
pivot[sym]
|
29
|
+
.copy()
|
30
|
+
.reset_index()
|
31
|
+
.rename(columns={"index": "date"})
|
32
|
+
.set_index("date")
|
33
|
+
)
|
34
|
+
if df.close.pct_change().abs().max() > 3: # same volatility filter
|
35
|
+
continue
|
36
|
+
symbols.append(sym)
|
37
|
+
return symbols
|
@@ -921,6 +921,9 @@ def get_pricing(
|
|
921
921
|
fields = ["close"]
|
922
922
|
if isinstance(symbol_list, str):
|
923
923
|
symbol_list = [symbol_list]
|
924
|
+
if isinstance(symbol_list, list) and "sp500" in symbol_list:
|
925
|
+
symbol_list.remove("sp500")
|
926
|
+
symbol_list += SP500_SYMBOLS
|
924
927
|
|
925
928
|
fields = [f.lower() for f in fields]
|
926
929
|
bad = [f for f in fields if f not in ALLOWED_FIELDS]
|
@@ -928,25 +931,32 @@ def get_pricing(
|
|
928
931
|
raise ValueError(f"Invalid field(s): {bad}. Allowed: {sorted(ALLOWED_FIELDS)}")
|
929
932
|
|
930
933
|
# --------------------------------------------------------------- download
|
931
|
-
|
932
|
-
|
933
|
-
(
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
]
|
940
|
-
|
934
|
+
universe = ds.load_dataset(
|
935
|
+
"paperswithbacktest/Universe-Daily-Price",
|
936
|
+
token=_get_hf_token(),
|
937
|
+
)
|
938
|
+
mapping = universe["train"].to_pandas()
|
939
|
+
mapping = mapping.set_index("symbol")["repo_id"].to_dict()
|
940
|
+
|
941
|
+
grouped = defaultdict(list)
|
942
|
+
remaining = []
|
943
|
+
for sym in symbol_list:
|
944
|
+
repo_id = mapping.get(sym)
|
945
|
+
repo_id = repo_id.split("/")[1] if isinstance(repo_id, str) else None
|
946
|
+
if repo_id:
|
947
|
+
grouped[repo_id].append(sym)
|
948
|
+
else:
|
949
|
+
print(f"Warning: No dataset found for symbol '{sym}'")
|
950
|
+
remaining.append(sym)
|
951
|
+
|
941
952
|
frames = []
|
942
|
-
for
|
943
|
-
|
944
|
-
|
945
|
-
df_part = load_dataset(dataset_name, list(remaining), extend=ext_flag)
|
953
|
+
for repo_id, syms in grouped.items():
|
954
|
+
ext_flag = extend if repo_id != "Indices-Daily-Price" else False
|
955
|
+
df_part = load_dataset(repo_id, syms, extend=ext_flag)
|
946
956
|
if not df_part.empty:
|
947
957
|
frames.append(df_part)
|
948
|
-
|
949
|
-
df = pd.concat(frames, ignore_index=True)
|
958
|
+
|
959
|
+
df = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
|
950
960
|
|
951
961
|
df["date"] = pd.to_datetime(df["date"])
|
952
962
|
df.set_index("date", inplace=True)
|
@@ -12,6 +12,8 @@ pwb_toolbox/backtest/__init__.py
|
|
12
12
|
pwb_toolbox/backtest/base_strategy.py
|
13
13
|
pwb_toolbox/backtest/engine.py
|
14
14
|
pwb_toolbox/backtest/ib_connector.py
|
15
|
+
pwb_toolbox/backtest/portfolio.py
|
16
|
+
pwb_toolbox/backtest/universe.py
|
15
17
|
pwb_toolbox/datasets/__init__.py
|
16
18
|
pwb_toolbox/performance/__init__.py
|
17
19
|
pwb_toolbox/performance/metrics.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|