Skip to content

Instantly share code, notes, and snippets.

@ryankert01
Created February 2, 2025 10:40
Show Gist options
  • Save ryankert01/3ba2fa9a1ce7938e42e9cb04fd033015 to your computer and use it in GitHub Desktop.
Save ryankert01/3ba2fa9a1ce7938e42e9cb04fd033015 to your computer and use it in GitHub Desktop.
Backtrack Leverage ETF strategy
# ETF Portfolio Strategy:
# 1. Initial Investment: $10,000 base investment
# 2. Periodic Investment: Additional $10,000 contribution every year on February 15th
# 3. Asset Allocation: 50/50 allocation between VOO and QQQ
# 4. Rebalancing: Annual rebalancing on February 15th (same day as contribution)
# 5. Leveraged Option: 1.25x leverage on the base portfolio with 3% annual borrowing cost
#
# Key Features:
# - Historical data analysis from 2015 to present
# - Comparison between base and leveraged portfolio performance
# - Comprehensive performance metrics calculation
# - Advanced risk analysis
# - Visual performance analysis through charts
import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
from scipy import stats
from pandas.tseries.offsets import MonthEnd
def fetch_data(tickers, start_date, end_date):
"""Fetch historical data for given tickers"""
data = pd.DataFrame()
for ticker in tickers:
df = yf.download(ticker, start=start_date, end=end_date)['Adj Close']
data[ticker] = df
return data
def calculate_portfolio_value(data, initial_investment, annual_contribution, weights):
"""Calculate portfolio value with periodic investments and rebalancing"""
# Initialize portfolio
portfolio_value = pd.Series(index=data.index, dtype=float)
portfolio_value.iloc[0] = initial_investment
# Calculate daily returns
returns = data.pct_change()
# Initialize holdings
holdings = pd.DataFrame(index=data.index, columns=data.columns, dtype=float)
for i, weight in enumerate(weights):
holdings.iloc[0, i] = initial_investment * weight
# Track cash injections
total_invested = initial_investment
for i in range(1, len(data)):
current_date = data.index[i]
prev_date = data.index[i-1]
# Update holdings based on daily price changes
for ticker in data.columns:
if not np.isnan(returns.iloc[i][ticker]):
holdings.iloc[i][ticker] = holdings.iloc[i-1][ticker] * (1 + returns.iloc[i][ticker])
else:
holdings.iloc[i][ticker] = holdings.iloc[i-1][ticker]
# Check if it's February 15th for rebalancing and contribution
if current_date.month == 2 and current_date.day == 15:
# Add annual contribution
total_invested += annual_contribution
current_total = holdings.iloc[i].sum() + annual_contribution
# Rebalance including new contribution
for j, weight in enumerate(weights):
holdings.iloc[i, j] = current_total * weight
portfolio_value.iloc[i] = holdings.iloc[i].sum()
# Calculate portfolio returns
portfolio_returns = portfolio_value.pct_change()
return portfolio_returns, portfolio_value, total_invested
def calculate_leveraged_returns(returns, leverage=1.25, borrowing_cost_annual=0.03):
"""Calculate leveraged returns with borrowing costs"""
# Convert annual borrowing cost to daily
daily_borrowing_cost = (1 + borrowing_cost_annual) ** (1/252) - 1
# Calculate leveraged returns with borrowing cost
leveraged_returns = returns * leverage - (leverage - 1) * daily_borrowing_cost
return leveraged_returns
def calculate_metrics(returns, risk_free_rate=0.03):
"""Calculate various performance metrics"""
metrics = {}
# Convert to numpy array and remove NaN
returns_clean = returns.dropna()
# Basic metrics
metrics['Total Return'] = (1 + returns_clean).prod() - 1
metrics['Annual Return'] = (1 + returns_clean).prod() ** (252/len(returns_clean)) - 1
metrics['Daily Volatility'] = returns_clean.std()
metrics['Annual Volatility'] = returns_clean.std() * np.sqrt(252)
# Sharpe Ratio
excess_returns = returns_clean - risk_free_rate/252
metrics['Sharpe Ratio'] = np.sqrt(252) * excess_returns.mean() / returns_clean.std()
# Sortino Ratio
downside_returns = returns_clean[returns_clean < 0]
downside_std = downside_returns.std()
metrics['Sortino Ratio'] = np.sqrt(252) * excess_returns.mean() / downside_std
# Maximum Drawdown
cum_returns = (1 + returns_clean).cumprod()
rolling_max = cum_returns.expanding().max()
drawdowns = cum_returns/rolling_max - 1
metrics['Maximum Drawdown'] = drawdowns.min()
# Calmar Ratio
metrics['Calmar Ratio'] = metrics['Annual Return'] / abs(metrics['Maximum Drawdown'])
# Value at Risk (VaR) - 95% confidence
metrics['VaR 95%'] = np.percentile(returns_clean, 5)
# Conditional VaR (CVaR/Expected Shortfall)
metrics['CVaR 95%'] = returns_clean[returns_clean <= metrics['VaR 95%']].mean()
# Beta (using S&P 500 as market proxy)
sp500 = yf.download('^GSPC', start=returns_clean.index[0], end=returns_clean.index[-1])['Adj Close'].pct_change()
# Align dates between portfolio returns and market returns
aligned_data = pd.concat([returns_clean, sp500], axis=1).dropna()
slope, _, r_value, _, _ = stats.linregress(aligned_data.iloc[:, 1], aligned_data.iloc[:, 0])
metrics['Beta'] = slope
metrics['R-squared'] = r_value ** 2
# Information Ratio
tracking_error = (returns_clean - aligned_data.iloc[:, 1]).std() * np.sqrt(252)
metrics['Information Ratio'] = (metrics['Annual Return'] - 0.03) / tracking_error
# Kurtosis and Skewness
metrics['Kurtosis'] = returns_clean.kurtosis()
metrics['Skewness'] = returns_clean.skew()
return metrics
def plot_portfolio_performance(returns):
"""Plot cumulative returns and drawdown"""
cumulative_returns = (1 + returns).cumprod()
plt.figure(figsize=(12, 8))
# Plot cumulative returns
plt.subplot(2, 1, 1)
plt.plot(cumulative_returns.index, cumulative_returns.values)
plt.title('Cumulative Portfolio Returns')
plt.grid(True)
# Plot drawdown
plt.subplot(2, 1, 2)
rolling_max = cumulative_returns.expanding().max()
drawdowns = cumulative_returns/rolling_max - 1
plt.plot(drawdowns.index, drawdowns.values)
plt.title('Portfolio Drawdown')
plt.grid(True)
plt.tight_layout()
plt.savefig('portfolio_performance.png')
plt.close()
def main():
# Set parameters
tickers = ['VOO', 'QQQ']
weights = [0.5, 0.5]
start_date = '2015-01-01'
end_date = datetime.now().strftime('%Y-%m-%d')
initial_investment = 10000
annual_contribution = 10000
# Fetch data
print("Fetching data...")
data = fetch_data(tickers, start_date, end_date)
# Calculate portfolio returns and value
print("\nCalculating portfolio returns...")
portfolio_returns, portfolio_value, total_invested = calculate_portfolio_value(
data, initial_investment, annual_contribution, weights
)
# Calculate leveraged portfolio returns
leveraged_returns = calculate_leveraged_returns(portfolio_returns)
# Calculate metrics for both portfolios
print("\nCalculating performance metrics...")
metrics_base = calculate_metrics(portfolio_returns)
metrics_leveraged = calculate_metrics(leveraged_returns)
# Print investment summary
print("\nInvestment Summary:")
print(f"Initial Investment: ${initial_investment:,.2f}")
print(f"Total Invested: ${total_invested:,.2f}")
print(f"Final Portfolio Value: ${portfolio_value.iloc[-1]:,.2f}")
print(f"Total Return: {((portfolio_value.iloc[-1]/total_invested - 1) * 100):,.2f}%")
# Print metrics comparison
print("\nPortfolio Performance Metrics Comparison:")
print("-" * 70)
print(f"{'Metric':<25} {'Base Portfolio':>20} {'1.25x Leveraged':>20}")
print("-" * 70)
for metric in metrics_base.keys():
base_value = metrics_base[metric]
lev_value = metrics_leveraged[metric]
print(f"{metric:<25} {base_value:>20.4f} {lev_value:>20.4f}")
# Plot performance comparison
print("\nGenerating performance plots...")
plt.figure(figsize=(15, 12))
# Plot portfolio value
plt.subplot(3, 1, 1)
plt.plot(portfolio_value.index, portfolio_value.values, label='Portfolio Value')
plt.axhline(y=total_invested, color='r', linestyle='--', label='Total Invested')
plt.title('Portfolio Value Over Time')
plt.grid(True)
plt.legend()
# Plot cumulative returns comparison
plt.subplot(3, 1, 2)
cum_returns_base = (1 + portfolio_returns).cumprod()
cum_returns_lev = (1 + leveraged_returns).cumprod()
plt.plot(cum_returns_base.index, cum_returns_base.values, label='Base Portfolio')
plt.plot(cum_returns_lev.index, cum_returns_lev.values, label='1.25x Leveraged')
plt.title('Cumulative Portfolio Returns Comparison')
plt.grid(True)
plt.legend()
# Plot drawdown comparison
plt.subplot(3, 1, 3)
rolling_max_base = cum_returns_base.expanding().max()
rolling_max_lev = cum_returns_lev.expanding().max()
drawdowns_base = cum_returns_base/rolling_max_base - 1
drawdowns_lev = cum_returns_lev/rolling_max_lev - 1
plt.plot(drawdowns_base.index, drawdowns_base.values, label='Base Portfolio')
plt.plot(drawdowns_lev.index, drawdowns_lev.values, label='1.25x Leveraged')
plt.title('Portfolio Drawdown Comparison')
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig('portfolio_performance_comparison.png')
plt.close()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment