Skip to content

Commit 140ea0f

Browse files
authored
feat(backtest): add weekly/monthly rebalancing
- Update _should_rebalance to respect W and M frequencies - Refactor run method to use conditional rebalancing logic - Add comprehensive tests for D/W/M rebalancing - Document rebalance_freq parameter in README - All tests pass; daily default unchanged * chore: update build artifacts and coverage data Closes issue #4
1 parent 864d4e9 commit 140ea0f

File tree

6 files changed

+333
-160
lines changed

6 files changed

+333
-160
lines changed

.coverage

-52 KB
Binary file not shown.

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ QuantResearchStarter aims to provide a clean, well-documented starting point for
2222

2323
* **Data management** — download market data or generate synthetic price series for experiments.
2424
* **Factor library** — example implementations of momentum, value, size, and volatility factors.
25-
* **Vectorized backtesting engine** — supports transaction costs, slippage, and portfolio constraints.
25+
* **Vectorized backtesting engine** — supports transaction costs, slippage, portfolio constraints, and configurable rebalancing frequencies (daily, weekly, monthly).
2626
* **Risk & performance analytics** — returns, drawdowns, Sharpe, turnover, and other risk metrics.
2727
* **CLI & scripts** — small tools to generate data, compute factors, and run backtests from the terminal.
2828
* **Production-ready utilities** — type hints, tests, continuous integration, and documentation scaffolding.
@@ -93,6 +93,30 @@ results = bt.run()
9393
print(results.performance.summary())
9494
```
9595

96+
### Rebalancing Frequency
97+
98+
The backtester supports different rebalancing frequencies to match your strategy needs:
99+
100+
```python
101+
from quant_research_starter.backtest import VectorizedBacktest
102+
103+
# Daily rebalancing (default)
104+
bt_daily = VectorizedBacktest(prices, signals, rebalance_freq="D")
105+
106+
# Weekly rebalancing (reduces turnover and transaction costs)
107+
bt_weekly = VectorizedBacktest(prices, signals, rebalance_freq="W")
108+
109+
# Monthly rebalancing (lowest turnover)
110+
bt_monthly = VectorizedBacktest(prices, signals, rebalance_freq="M")
111+
112+
results = bt_monthly.run()
113+
```
114+
115+
Supported frequencies:
116+
- `"D"`: Daily rebalancing (default)
117+
- `"W"`: Weekly rebalancing (rebalances when the week changes)
118+
- `"M"`: Monthly rebalancing (rebalances when the month changes)
119+
96120
> The code above is illustrative—see `examples/` for fully working notebooks and scripts.
97121
98122
---

src/quant_research_starter.egg-info/PKG-INFO

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ QuantResearchStarter aims to provide a clean, well-documented starting point for
6565

6666
* **Data management** — download market data or generate synthetic price series for experiments.
6767
* **Factor library** — example implementations of momentum, value, size, and volatility factors.
68-
* **Vectorized backtesting engine** — supports transaction costs, slippage, and portfolio constraints.
68+
* **Vectorized backtesting engine** — supports transaction costs, slippage, portfolio constraints, and configurable rebalancing frequencies (daily, weekly, monthly).
6969
* **Risk & performance analytics** — returns, drawdowns, Sharpe, turnover, and other risk metrics.
7070
* **CLI & scripts** — small tools to generate data, compute factors, and run backtests from the terminal.
7171
* **Production-ready utilities** — type hints, tests, continuous integration, and documentation scaffolding.
@@ -136,6 +136,30 @@ results = bt.run()
136136
print(results.performance.summary())
137137
```
138138

139+
### Rebalancing Frequency
140+
141+
The backtester supports different rebalancing frequencies to match your strategy needs:
142+
143+
```python
144+
from quant_research_starter.backtest import VectorizedBacktest
145+
146+
# Daily rebalancing (default)
147+
bt_daily = VectorizedBacktest(prices, signals, rebalance_freq="D")
148+
149+
# Weekly rebalancing (reduces turnover and transaction costs)
150+
bt_weekly = VectorizedBacktest(prices, signals, rebalance_freq="W")
151+
152+
# Monthly rebalancing (lowest turnover)
153+
bt_monthly = VectorizedBacktest(prices, signals, rebalance_freq="M")
154+
155+
results = bt_monthly.run()
156+
```
157+
158+
Supported frequencies:
159+
- `"D"`: Daily rebalancing (default)
160+
- `"W"`: Weekly rebalancing (rebalances when the week changes)
161+
- `"M"`: Monthly rebalancing (rebalances when the month changes)
162+
139163
> The code above is illustrative—see `examples/` for fully working notebooks and scripts.
140164

141165
---

src/quant_research_starter/backtest/vectorized.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,30 @@ def run(self, weight_scheme: str = "rank") -> Dict:
6464
"""
6565
print("Running backtest...")
6666

67-
# Vectorized returns-based backtest with daily rebalancing
67+
# Vectorized returns-based backtest with configurable rebalancing
6868
returns_df = self.prices.pct_change().dropna()
6969
aligned_signals = self.signals.loc[returns_df.index]
7070

71-
# Compute daily target weights from signals
72-
weights = aligned_signals.apply(
73-
lambda row: self._calculate_weights(row, weight_scheme), axis=1
74-
)
75-
# Ensure full DataFrame with same columns order
76-
weights = weights.reindex(columns=self.prices.columns).fillna(0.0)
71+
# Track rebalancing
72+
prev_rebalance_date = None
73+
current_weights = pd.Series(0.0, index=self.prices.columns)
74+
75+
# Compute daily weights from signals (rebalance only on rebalance dates)
76+
weights_list = []
77+
for date in returns_df.index:
78+
if self._should_rebalance(date, prev_rebalance_date):
79+
# Rebalance: compute new target weights
80+
current_weights = self._calculate_weights(
81+
aligned_signals.loc[date], weight_scheme
82+
)
83+
prev_rebalance_date = date
84+
85+
# Append current weights (maintain between rebalances)
86+
weights_list.append(current_weights)
87+
88+
weights = pd.DataFrame(
89+
weights_list, index=returns_df.index, columns=self.prices.columns
90+
).fillna(0.0)
7791

7892
# Previous day weights for PnL calculation
7993
weights_prev = weights.shift(1).fillna(0.0)
@@ -104,11 +118,42 @@ def run(self, weight_scheme: str = "rank") -> Dict:
104118

105119
return self._generate_results()
106120

107-
def _should_rebalance(self, date: pd.Timestamp) -> bool:
108-
"""Check if we should rebalance on given date."""
109-
# Simple daily rebalancing for now
110-
# Could be extended for weekly/monthly rebalancing
111-
return True
121+
def _should_rebalance(
122+
self, date: pd.Timestamp, prev_rebalance_date: Optional[pd.Timestamp] = None
123+
) -> bool:
124+
"""Check if we should rebalance on given date.
125+
126+
Args:
127+
date: Current date to check
128+
prev_rebalance_date: Last rebalance date (None for first rebalance)
129+
130+
Returns:
131+
True if should rebalance, False otherwise
132+
"""
133+
# Always rebalance on first date
134+
if prev_rebalance_date is None:
135+
return True
136+
137+
if self.rebalance_freq == "D":
138+
# Daily rebalancing
139+
return True
140+
elif self.rebalance_freq == "W":
141+
# Weekly rebalancing - rebalance if week changed
142+
return (
143+
date.isocalendar()[1] != prev_rebalance_date.isocalendar()[1]
144+
or date.year != prev_rebalance_date.year
145+
)
146+
elif self.rebalance_freq == "M":
147+
# Monthly rebalancing - rebalance if month changed
148+
return (
149+
date.month != prev_rebalance_date.month
150+
or date.year != prev_rebalance_date.year
151+
)
152+
else:
153+
raise ValueError(
154+
f"Unsupported rebalance frequency: {self.rebalance_freq}. "
155+
f"Supported frequencies: 'D' (daily), 'W' (weekly), 'M' (monthly)"
156+
)
112157

113158
def _calculate_weights(self, signals: pd.Series, scheme: str) -> pd.Series:
114159
"""Convert signals to portfolio weights."""
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
"""Factors module public API."""
2-
3-
from .base import Factor
4-
from .bollinger import BollingerBandsFactor
5-
from .momentum import CrossSectionalMomentum, MomentumFactor
6-
from .size import SizeFactor
7-
from .value import ValueFactor
8-
from .volatility import IdiosyncraticVolatility, VolatilityFactor
9-
10-
__all__ = [
11-
"Factor",
12-
"BollingerBandsFactor",
13-
"CrossSectionalMomentum",
14-
"MomentumFactor",
15-
"SizeFactor",
16-
"ValueFactor",
17-
"IdiosyncraticVolatility",
18-
"VolatilityFactor",
19-
]
1+
"""Factors module public API."""
2+
3+
from .base import Factor
4+
from .bollinger import BollingerBandsFactor
5+
from .momentum import CrossSectionalMomentum, MomentumFactor
6+
from .size import SizeFactor
7+
from .value import ValueFactor
8+
from .volatility import IdiosyncraticVolatility, VolatilityFactor
9+
10+
__all__ = [
11+
"Factor",
12+
"BollingerBandsFactor",
13+
"CrossSectionalMomentum",
14+
"MomentumFactor",
15+
"SizeFactor",
16+
"ValueFactor",
17+
"IdiosyncraticVolatility",
18+
"VolatilityFactor",
19+
]

0 commit comments

Comments
 (0)