DanielKiani's picture
Version 1.0 release
349ad65
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()