Created
February 2, 2025 10:40
-
-
Save ryankert01/3ba2fa9a1ce7938e42e9cb04fd033015 to your computer and use it in GitHub Desktop.
Backtrack Leverage ETF strategy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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