File size: 6,320 Bytes
7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 349ad65 7d2e753 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os # Import os for directory creation
def buy_and_hold(df_assets, initial_balance=10000): # Renamed df to df_assets for clarity
"""
Simulates the Buy and Hold strategy.
Args:
df_assets (pd.DataFrame): DataFrame with daily tradable asset prices ONLY.
initial_balance (int): The starting capital.
Returns:
pd.Series: A Series containing the portfolio value for each day.
"""
print("--- Simulating Buy and Hold ---")
n_assets = len(df_assets.columns)
# Invest an equal amount in each asset at the beginning
initial_investment_per_asset = initial_balance / n_assets
# Get the initial prices
initial_prices = df_assets.iloc[0]
# Calculate the number of shares bought for each asset
# Handle potential division by zero if an asset price is 0 (though unlikely with real data)
shares = initial_investment_per_asset / (initial_prices + 1e-8)
# Calculate the portfolio value for each day
portfolio_values = df_assets.dot(shares)
print(f"Initial Investment: ${initial_balance:.2f}")
print(f"Final Portfolio Value (Buy and Hold): ${portfolio_values.iloc[-1]:.2f}")
return portfolio_values
def equally_weighted_rebalanced(df_assets, initial_balance=10000, rebalance_freq='M', transaction_cost_pct=0.001): # Renamed df to df_assets
"""
Simulates an Equally Weighted Portfolio with periodic rebalancing.
Args:
df_assets (pd.DataFrame): DataFrame with daily tradable asset prices ONLY.
initial_balance (int): The starting capital.
rebalance_freq (str): The rebalancing frequency ('M' for monthly, 'Q' for quarterly).
transaction_cost_pct (float): The transaction cost as a percentage.
Returns:
pd.Series: A Series containing the portfolio value for each day.
"""
print(f"\n--- Simulating Equally Weighted Portfolio (Rebalanced {rebalance_freq}) ---")
n_assets = len(df_assets.columns)
# Set the initial weights to be equal
weights = np.full(n_assets, 1/n_assets)
portfolio_value = initial_balance
portfolio_values = pd.Series(index=df_assets.index, dtype=float) # Explicitly set dtype
last_rebalance_date = None
for i, (date, prices) in enumerate(df_assets.iterrows()):
# Store the portfolio value for the day before any changes
portfolio_values[date] = portfolio_value
# Determine if it's a rebalancing day
# Rebalance on the first day of the new period (month, quarter) or if it's the very first day
rebalance_this_day = False
if i == 0: # Rebalance on the very first day
rebalance_this_day = True
elif rebalance_freq == 'M' and date.month != df_assets.index[i-1].month:
rebalance_this_day = True
# Add 'Q' for quarterly if needed, similar logic
if rebalance_this_day:
# Calculate the value of trades to rebalance
target_asset_values = portfolio_value * (1/n_assets)
current_asset_values = weights * portfolio_value
trades = target_asset_values - current_asset_values
# Apply transaction costs
transaction_costs = np.sum(np.abs(trades)) * transaction_cost_pct
portfolio_value -= transaction_costs
# Reset weights to be equal
weights = np.full(n_assets, 1/n_assets)
last_rebalance_date = date
# Calculate portfolio value for the *next* day before the market opens
# Get price changes from today to the next trading day
today_prices = prices # Already have prices for the current date
next_day_index = df_assets.index.get_loc(date) + 1
if next_day_index < len(df_assets):
next_day_prices = df_assets.iloc[next_day_index]
# Avoid division by zero
price_change_ratio = next_day_prices / (today_prices + 1e-8)
# Update portfolio value based on price changes
portfolio_value = np.sum( (weights * portfolio_value) * price_change_ratio )
# Update weights due to market drift
new_asset_values = (weights * portfolio_value) * price_change_ratio
# Avoid division by zero for total portfolio value
if np.sum(new_asset_values) > 1e-8: # Check if total value is effectively non-zero
weights = new_asset_values / np.sum(new_asset_values)
else:
weights = np.full(n_assets, 1/n_assets) # Default to equal or handle as error
print(f"Initial Investment: ${initial_balance:.2f}")
print(f"Final Portfolio Value (Equally Weighted): ${portfolio_values.iloc[-1]:.2f}")
return portfolio_values.dropna()
def main():
# Load the evaluation data (which contains both assets and macro data)
full_eval_df = pd.read_csv('data/eval.csv', index_col='Date', parse_dates=True)
# --- IMPORTANT: Filter ONLY asset columns for baselines ---
asset_columns = ['AAPL', 'BTC-USD', 'MSFT', 'SPY', 'TLT'] # Define your actual tradable assets
test_df_assets_only = full_eval_df[asset_columns]
# --- Run Baseline Strategies ---
bnh_values = buy_and_hold1(test_df_assets_only)
ewp_values = equally_weighted_rebalanced(test_df_assets_only)
# --- Plot the results ---
plt.style.use('seaborn-v0_8-darkgrid')
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(bnh_values.index, bnh_values, label='Buy and Hold', color='blue', linewidth=2)
ax.plot(ewp_values.index, ewp_values, label='Equally Weighted (Rebalanced Monthly)', color='green', linewidth=2)
ax.set_title('Baseline Strategy Performance (2021-2023)', fontsize=16)
ax.set_xlabel('Date', fontsize=12)
ax.set_ylabel('Portfolio Value ($)', fontsize=12)
ax.legend(fontsize=12)
# Format the y-axis to show currency
from matplotlib.ticker import FuncFormatter
formatter = FuncFormatter(lambda x, p: f'${x:,.0f}')
ax.yaxis.set_major_formatter(formatter)
plt.tight_layout()
# Ensure results directory exists for saving plot
results_dir = 'results'
os.makedirs(results_dir, exist_ok=True)
plt.savefig(os.path.join(results_dir, 'baseline_performance.png'))
plt.show()
if __name__ == '__main__':
main() |