I see that this thread is filled with coders who appear to have a good understanding of Python I have been working for a while on something relatively simple: creating a system that allows for backtesting and viewing the latest signal from the Bold Asset Allocation, aggressive version. Bold Asset Allocation - Allocate Smartly
It should be fairly straightforward, yet I am unable to get the returns to display correctly at all. This issue persists even when I tried to incorporate the combination from Jrinne's latest script.
I don't understand why the returns are nowhere near the 11-12% since 2016. Is there anyone who can identify the error?
I see that attempts to resolve this have been made previously: Anyone know how to build these in P123 - #11 by Whycliffes
import backtrader as bt
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
import time
from collections import OrderedDict
import matplotlib.pyplot as plt
class KeyllyStrategy(bt.Strategy):
params = dict(
lookback_months=12,
defensive_allocation=1/3,
rebalance_threshold=0.01
)
def __init__(self):
self.canary = ['SPY', 'EFA', 'EEM', 'AGG']
self.offensive = ['QQQ']
self.defensive = ['AGG', 'LQD', 'PDBC']
self.safe = 'BIL'
self.start_date = self.datas[0].datetime.date(0)
self.end_date = self.datas[0].datetime.date(-1)
self.monthly_returns = OrderedDict()
current_date = pd.Timestamp(self.start_date)
while current_date <= pd.Timestamp(self.end_date):
self.monthly_returns[current_date.strftime('%Y-%m')] = (current_date.date(), None)
current_date += pd.offsets.MonthEnd(1)
self.annual_returns = {}
self.monthly_closes = {asset: [] for asset in self.canary + self.offensive + self.defensive + [self.safe]}
self.portfolio_start_value = self.broker.getvalue()
self.month_start_value = self.portfolio_start_value
self.year_start_value = self.portfolio_start_value
self.current_month = None
self.current_year = None
self.last_month = None
self.current_weights = {d._name: 0.0 for d in self.datas}
self.target_weights = {d._name: 0.0 for d in self.datas}
self.mode = 'defensive'
self.pending_orders = []
self.initialization_complete = False
# Enhanced tracking
self.portfolio_values = []
self.positions_history = []
self.daily_returns = []
def notify_order(self, order):
if order.status in [order.Completed]:
self.pending_orders.remove(order)
self.log(f"Order completed: {order.data._name}")
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} - {txt}')
def track_returns(self):
if not self.initialization_complete:
return
current_date = self.datas[0].datetime.date(0)
portfolio_value = sum(self.getposition(d).size * d.close[0] for d in self.datas)
cash = self.broker.getcash()
total_value = portfolio_value + cash
self.portfolio_values.append((current_date, total_value))
month_key = current_date.strftime('%Y-%m')
if self.current_month is None:
self.current_month = current_date.month
self.current_year = current_date.year
self.month_start_value = total_value
self.year_start_value = total_value
return
if self.is_last_trading_day_of_month(current_date):
monthly_return = ((total_value / self.month_start_value) - 1) * 100
self.monthly_returns[month_key] = (current_date, monthly_return)
self.month_start_value = total_value
if current_date.year != self.current_year:
annual_return = ((total_value / self.year_start_value) - 1) * 100
self.annual_returns[self.current_year] = annual_return
self.year_start_value = total_value
self.current_year = current_date.year
self.current_month = current_date.month
self.log(f"Month End Value: {total_value:.2f}, Return: {monthly_return:.2f}%")
def next(self):
current_date = self.datas[0].datetime.date(0)
if self.last_month != current_date.month:
for asset in self.monthly_closes:
data = self.get_data_by_name(asset)
if data:
self.monthly_closes[asset].append(data.close[0])
self.last_month = current_date.month
if len(self.monthly_closes['SPY']) < 13:
self.log("Building historical data...")
return
if not self.initialization_complete:
self.initialization_complete = True
self.month_start_value = self.broker.getvalue()
self.track_returns()
if not self.is_last_trading_day_of_month(current_date) or self.pending_orders:
return
canary_momentum = {asset: self.calculate_13612W_momentum(self.monthly_closes[asset])
for asset in self.canary}
self.log(f"Canary momentum: {canary_momentum}")
all_positive = all(mom > 0 for mom in canary_momentum.values())
self.target_weights = {d._name: 0.0 for d in self.datas}
portfolio_value = self.broker.getvalue()
if all_positive:
self.mode = 'offensive'
self.target_weights['QQQ'] = 1.0
self.log("Switching to offensive mode: 100% QQQ")
for data in self.datas:
if data._name == 'QQQ':
target_value = portfolio_value
target_size = target_value / data.close[0]
order = self.order_target_size(data, target_size)
self.pending_orders.append(order)
else:
self.mode = 'defensive'
defensive_scores = {asset: self.calculate_relative_momentum(self.monthly_closes[asset])
for asset in self.defensive}
sorted_assets = sorted(defensive_scores.items(), key=lambda x: x[1], reverse=True)
best_defensive = sorted_assets[0][0]
self.target_weights[best_defensive] = self.params.defensive_allocation
self.target_weights[self.safe] = 1 - self.params.defensive_allocation
self.log(f"Switching to defensive mode: {best_defensive}={self.params.defensive_allocation*100:.0f}%, {self.safe}={100-self.params.defensive_allocation*100:.0f}%")
for data in self.datas:
target_weight = self.target_weights[data._name]
if target_weight > 0:
target_value = portfolio_value * target_weight
target_size = target_value / data.close[0]
order = self.order_target_size(data, target_size)
self.pending_orders.append(order)
self.positions_history.append({
'date': current_date,
'mode': self.mode,
'weights': self.target_weights.copy()
})
def calculate_13612W_momentum(self, prices):
if len(prices) < 13:
return -np.inf
p0 = prices[-1]
p1 = prices[-2]
p3 = prices[-4]
p6 = prices[-7]
p12 = prices[-13]
return (12 * (p0/p1 - 1)) + (4 * (p0/p3 - 1)) + (2 * (p0/p6 - 1)) + (p0/p12 - 1)
def calculate_relative_momentum(self, prices):
if len(prices) < 13:
return -np.inf
return prices[-1] / np.mean(prices[-13:]) - 1
def get_data_by_name(self, name):
return next((d for d in self.datas if d._name == name), None)
def is_last_trading_day_of_month(self, dt):
current_date = pd.Timestamp(dt)
next_day = current_date + pd.Timedelta(days=1)
return current_date.month != next_day.month
def calculate_performance_metrics(self):
portfolio_df = pd.DataFrame(self.portfolio_values, columns=['Date', 'Value'])
portfolio_df.set_index('Date', inplace=True)
returns = portfolio_df['Value'].pct_change()
ann_return = (1 + returns).prod() ** (252/len(returns)) - 1
ann_vol = returns.std() * np.sqrt(252)
sharpe = ann_return / ann_vol if ann_vol != 0 else 0
cum_returns = (1 + returns).cumprod()
rolling_max = cum_returns.expanding().max()
drawdowns = (cum_returns - rolling_max) / rolling_max
return {
'Annual Return': ann_return * 100,
'Annual Volatility': ann_vol * 100,
'Sharpe Ratio': sharpe,
'Max Drawdown': drawdowns.min() * 100,
'Cumulative Return': (cum_returns.iloc[-1] - 1) * 100
}
def plot_results(self):
portfolio_df = pd.DataFrame(self.portfolio_values, columns=['Date', 'Value'])
portfolio_df.set_index('Date', inplace=True)
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(20, 15))
# Plot 1: Cumulative Returns
returns = portfolio_df['Value'].pct_change()
cum_returns = (1 + returns).cumprod()
ax1.plot(cum_returns.index, (cum_returns - 1) * 100)
ax1.set_title('Cumulative Returns (%)')
ax1.grid(True)
# Plot 2: Drawdowns
rolling_max = cum_returns.expanding().max()
drawdowns = (cum_returns - rolling_max) / rolling_max * 100
ax2.fill_between(drawdowns.index, drawdowns, 0, color='red', alpha=0.3)
ax2.set_title('Drawdowns (%)')
ax2.grid(True)
# Plot 3: Monthly Returns
monthly_returns = pd.Series([ret for _, ret in self.monthly_returns.values() if ret is not None])
ax3.bar(range(len(monthly_returns)), monthly_returns)
ax3.set_title('Monthly Returns (%)')
ax3.grid(True)
# Plot 4: Strategy Mode
modes_df = pd.DataFrame(self.positions_history)
ax4.plot(modes_df['date'], [1 if mode == 'offensive' else 0 for mode in modes_df['mode']])
ax4.set_title('Strategy Mode (1=Offensive, 0=Defensive)')
ax4.grid(True)
plt.tight_layout()
plt.show()
def download_with_retry(ticker, start_date, end_date, max_retries=5):
for attempt in range(max_retries):
try:
buffer_start = start_date - timedelta(days=730)
buffer_end = end_date + timedelta(days=5)
df = yf.download(ticker, start=buffer_start, end=buffer_end,
progress=False, interval='1d', auto_adjust=True)
if not df.empty and len(df) > 100:
date_range = pd.date_range(start=start_date, end=end_date, freq='B')
df = df.reindex(date_range, method='ffill')
return df
time.sleep(2)
except Exception as e:
print(f"Retry {attempt + 1} for {ticker}")
time.sleep(2)
return None
if __name__ == '__main__':
cerebro = bt.Cerebro()
cerebro.addstrategy(KeyllyStrategy)
cerebro.broker.setcommission(commission=0.001)
cerebro.addobserver(bt.observers.Value)
cerebro.addobserver(bt.observers.Trades)
cerebro.addobserver(bt.observers.DrawDown)
tickers = ['SPY', 'EFA', 'EEM', 'AGG', 'QQQ', 'PDBC', 'BIL', 'LQD']
start_date = datetime(2015, 1, 1)
end_date = datetime(2023, 11, 16)
for ticker in tickers:
print(f"Downloading data for {ticker}...")
df = download_with_retry(ticker, start_date, end_date)
if df is not None and not df.empty:
data = bt.feeds.PandasData(dataname=df, fromdate=start_date, todate=end_date)
cerebro.adddata(data, name=ticker)
else:
print(f"Failed to load {ticker}")
exit(1)
start_cash = 100000
cerebro.broker.set_cash(start_cash)
cerebro.addsizer(bt.sizers.PercentSizer)
print(f"Starting Portfolio Value: {cerebro.broker.getvalue():.2f}")
results = cerebro.run()
strat = results[0]
print("\nMonthly Returns:")
for month_key, (date, ret) in sorted(strat.monthly_returns.items()):
if ret is not None:
print(f"{month_key}: {ret:.2f}%")
print("\nAnnual Returns:")
for year, ret in sorted(strat.annual_returns.items()):
print(f"{year}: {ret:.2f}%")
metrics = strat.calculate_performance_metrics()
print("\nPerformance Metrics:")
for metric, value in metrics.items():
print(f"{metric}: {value:.2f}%")
end_value = cerebro.broker.getvalue()
total_return = ((end_value - start_cash) / start_cash) * 100
years = (end_date - start_date).days / 365.25
cagr = ((end_value / start_cash) ** (1/years) - 1) * 100
print(f"\nFinal Portfolio Value: {end_value:.2f}")
print(f"Total Return: {total_return:.2f}%")
print(f"CAGR: {cagr:.2f}%")
strat.plot_results()
cerebro.plot(style='candlestick', volume=False)