Quarterly 60/40 Stock and Bond Rebalancing with Drift Checks
Summary
This strategy maintains a portfolio split between a broad US stock ETF and a long-term Treasury ETF. It targets a 60% stock allocation and 40% bond allocation, checks prices daily, and submits trades to move holdings toward those weights. The code schedules rebalancing on the first run and at quarterly intervals, and also checks whether the stock allocation has drifted more than five percentage points from its target.
The example uses whole-share quantities and skips an asset if its latest price is unavailable. It demonstrates allocation-based portfolio construction and periodic rebalancing, but provides no performance results or comparison with alternatives. The drift check examines the stock allocation only, and the quarterly date condition may trigger more than once within the qualifying early-month window. The backtest uses historical Yahoo Finance data from 2020 through 2023 and names the stock ETF as its benchmark; those choices limit what can be inferred about live trading, data quality, costs, or other markets.
Key ideas
- The portfolio targets 60% stocks and 40% bonds using two exchange-traded funds.
- It checks holdings daily and schedules rebalancing quarterly.
- A stock-weight deviation greater than five percentage points can trigger an additional rebalance.
- Trade sizes are calculated in whole shares from portfolio value and current prices.
- The example includes a historical backtest setup but reports no performance evidence.
Tags
Full text
# classic_60_40.py
```py
"""
Classic 60/40 Portfolio
Asset class: Stocks + Bonds
Data source: Yahoo Finance (free)
Description: Classic 60% stocks (SPY), 40% bonds (TLT) portfolio
with quarterly rebalancing when drift exceeds 5%.
"""
from datetime import datetime
from lumibot.strategies import Strategy
from lumibot.backtesting import YahooDataBacktesting
class Classic6040(Strategy):
parameters = {
"stock_symbol": "SPY",
"bond_symbol": "TLT",
"stock_weight": 0.60,
"bond_weight": 0.40,
"rebalance_threshold": 0.05,
}
def initialize(self):
self.sleeptime = "1D"
self.vars.last_rebalance = None
def on_trading_iteration(self):
current_date = self.get_datetime().date()
# Rebalance quarterly or on first iteration
should_rebalance = (
self.first_iteration
or self.vars.last_rebalance is None
or (current_date.month - self.vars.last_rebalance.month) % 3 == 0
and current_date.day <= 5
)
if not should_rebalance:
# Check drift
stock_pos = self.get_position(self.parameters["stock_symbol"])
if stock_pos:
stock_value = stock_pos.quantity * self.get_last_price(self.parameters["stock_symbol"])
current_stock_weight = stock_value / self.portfolio_value
drift = abs(current_stock_weight - self.parameters["stock_weight"])
if drift > self.parameters["rebalance_threshold"]:
should_rebalance = True
self.log_message(f"Drift {drift:.1%} exceeds threshold, rebalancing")
if not should_rebalance:
return
portfolio = self.portfolio_value
for symbol, weight in [
(self.parameters["stock_symbol"], self.parameters["stock_weight"]),
(self.parameters["bond_symbol"], self.parameters["bond_weight"]),
]:
price = self.get_last_price(symbol)
if not price:
continue
target_qty = int((portfolio * weight) // price)
current_position = self.get_position(symbol)
current_qty = current_position.quantity if current_position else 0
diff = target_qty - current_qty
if diff > 0:
self.submit_order(self.create_order(symbol, diff, "buy"))
elif diff < 0:
self.submit_order(self.create_order(symbol, abs(diff), "sell"))
self.vars.last_rebalance = current_date
self.log_message(f"Rebalanced to {self.parameters['stock_weight']:.0%}/{self.parameters['bond_weight']:.0%}")
if __name__ == "__main__":
Classic6040.backtest(
YahooDataBacktesting,
datetime(2020, 1, 1),
datetime(2024, 1, 1),
benchmark_asset="SPY",
)
```Shown in full with attribution under the source's licence. Licence: MIT
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.