Untitled

 avatar
unknown
plain_text
5 months ago
6.0 kB
4
Indexable
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ========================================================
#                     PARAMETERS                         
# ========================================================
# Define backtest period and initial investment settings
start_date = "2022-12-08"
end_date = "2023-12-09"
initial_capital = 100000.0  # Starting portfolio value

# Constant daily return for "box" strategy
daily_box_return = 0.45 / 100 / 21  # About 0.00214% daily return

# Technical indicator configurations
rsi_period = 6  # Relative Strength Index (RSI) calculation period
ma_period = 5   # Moving average (MA) window size (in days)

# Categorize tickers by strategy
low_ma_tickers = ["TQQQ", "LABU", "SPXL"]  # High-risk, high-reward assets
high_ma_tickers = ["WMT", "MSTR", "AMZN", "KO", "BRK-B", "AAPL", "TSLA", "NVDA"]  # Stable or growth-focused
all_tickers = ["SPY"] + low_ma_tickers + high_ma_tickers  # Combine all tickers

# ========================================================
#                     FUNCTIONS                          
# ========================================================
def download_data(tickers, start, end):
    """
    Fetch historical price data for a list of tickers from Yahoo Finance.
    """
    data = yf.download(tickers, start=start, end=end, timeout=90)
    prices = data['Adj Close']
    return prices

def compute_RSI(prices, period):
    """
    Calculate the Relative Strength Index (RSI) for a price series.
    """
    delta = prices.diff()
    up = delta.clip(lower=0)
    down = -1 * delta.clip(upper=0)

    ema_up = up.ewm(com=period-1, adjust=False).mean()
    ema_down = down.ewm(com=period-1, adjust=False).mean()

    rs = ema_up / ema_down
    rsi = 100 - (100 / (1 + rs))
    return rsi

# ========================================================
#                MAIN BACKTEST LOGIC                     
# ========================================================

# Load price data for all selected tickers
prices = download_data(all_tickers, start_date, end_date)

# Ensure consistency in column naming for BRK-B
if 'BRK-B' not in prices.columns:
    for col in prices.columns:
        if 'BRK' in col and '.' in col:
            prices.rename(columns={col: 'BRK-B'}, inplace=True)
            break

# Clean the price data
prices.dropna(how='all', inplace=True)
prices.fillna(method='ffill', inplace=True)

# Compute technical indicators
spy_rsi = compute_RSI(prices["SPY"], period=rsi_period)  # RSI for SPY
ma_low = prices[low_ma_tickers].rolling(ma_period).mean()  # Moving averages for "low MA" group
ma_high = prices[high_ma_tickers].rolling(ma_period).mean()  # Moving averages for "high MA" group

# Initialize portfolio and track performance
dates = prices.index
capital = initial_capital
portfolio_values = pd.Series(index=dates, dtype=float)
portfolio_values.iloc[0] = capital

chosen_ticker = None  # Ticker chosen at the end of each day

# Simulate daily portfolio performance
for i in range(len(dates)):
    today = dates[i]

    # Realize returns for the previous day's choice
    if i > 0:
        yesterday = dates[i - 1]
        if chosen_ticker == "BOX":
            # Apply box strategy daily return
            capital *= (1 + daily_box_return)
        else:
            # Calculate actual daily return for the chosen ticker
            if chosen_ticker in prices.columns:
                prev_price = prices.loc[yesterday, chosen_ticker]
                curr_price = prices.loc[today, chosen_ticker]
                daily_ret = (curr_price / prev_price) - 1
                capital *= (1 + daily_ret)
            else:
                # Fallback to box return if unexpected issue
                capital *= (1 + daily_box_return)

    # Record portfolio value for today
    portfolio_values.loc[today] = capital

    # Decide what to hold for tomorrow
    if i < len(dates) - 1:  # Skip decision for the last day
        current_rsi = spy_rsi.loc[today] if today in spy_rsi.index else np.nan

        if pd.isna(current_rsi):
            # Default to box strategy if RSI data is unavailable
            chosen_ticker = "BOX"
        else:
            if current_rsi >= 90:
                # Overbought - default to box
                chosen_ticker = "BOX"
            elif current_rsi <= 28:
                # Oversold - pick ticker with lowest MA in low MA group
                current_mas = ma_low.loc[today]
                chosen_ticker = current_mas.idxmin() if not current_mas.isna().any() else "BOX"
            else:
                # Neutral RSI - pick ticker with highest MA in high MA group
                current_mas = ma_high.loc[today]
                chosen_ticker = current_mas.idxmax() if not current_mas.isna().any() else "BOX"

# ========================================================
#                     RESULTS                            
# ========================================================
# Calculate final metrics
final_value = capital
total_return = (final_value / initial_capital - 1) * 100
num_days = max(len(dates) - 1, 1)
cagr = ((final_value / initial_capital)**(252 / num_days) - 1) * 100 if num_days > 1 else 0.0

# Print results
print("========================================================")
print(f"Final Capital: ${final_value:,.2f}")
print(f"Total Return: {total_return:.2f}%")
print(f"CAGR: {cagr:.2f}%")
print("========================================================")

# Plot equity curve
plt.figure(figsize=(12, 6))
plt.plot(portfolio_values, label="Equity Curve", color="blue")
plt.title("Equity Curve")
plt.xlabel("Date")
plt.ylabel("Portfolio Value")
plt.legend()
plt.grid()
plt.show()

# Analyze drawdown
peak = portfolio_values.cummax()
drawdown = (portfolio_values - peak) / peak
max_drawdown = drawdown.min()

print("========================================================")
print(f"Maximum Drawdown: {max_drawdown * 100:.2f}%")
print("========================================================")
Editor is loading...
Leave a Comment