| import pandas as pd |
| import numpy as np |
| import matplotlib.pyplot as plt |
| import os |
|
|
| def buy_and_hold(df_assets, initial_balance=10000): |
| """ |
| 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) |
|
|
| |
| initial_investment_per_asset = initial_balance / n_assets |
|
|
| |
| initial_prices = df_assets.iloc[0] |
|
|
| |
| |
| shares = initial_investment_per_asset / (initial_prices + 1e-8) |
|
|
| |
| 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): |
| """ |
| 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) |
|
|
| |
| weights = np.full(n_assets, 1/n_assets) |
|
|
| portfolio_value = initial_balance |
| portfolio_values = pd.Series(index=df_assets.index, dtype=float) |
|
|
| last_rebalance_date = None |
|
|
| for i, (date, prices) in enumerate(df_assets.iterrows()): |
| |
| portfolio_values[date] = portfolio_value |
|
|
| |
| |
| rebalance_this_day = False |
| if i == 0: |
| rebalance_this_day = True |
| elif rebalance_freq == 'M' and date.month != df_assets.index[i-1].month: |
| rebalance_this_day = True |
| |
|
|
| if rebalance_this_day: |
| |
| target_asset_values = portfolio_value * (1/n_assets) |
| current_asset_values = weights * portfolio_value |
| trades = target_asset_values - current_asset_values |
|
|
| |
| transaction_costs = np.sum(np.abs(trades)) * transaction_cost_pct |
| portfolio_value -= transaction_costs |
|
|
| |
| weights = np.full(n_assets, 1/n_assets) |
| last_rebalance_date = date |
|
|
| |
| |
| today_prices = prices |
| 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] |
|
|
| |
| price_change_ratio = next_day_prices / (today_prices + 1e-8) |
|
|
| |
| portfolio_value = np.sum( (weights * portfolio_value) * price_change_ratio ) |
|
|
| |
| new_asset_values = (weights * portfolio_value) * price_change_ratio |
| |
| if np.sum(new_asset_values) > 1e-8: |
| weights = new_asset_values / np.sum(new_asset_values) |
| else: |
| weights = np.full(n_assets, 1/n_assets) |
|
|
|
|
| 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(): |
| |
| full_eval_df = pd.read_csv('data/eval.csv', index_col='Date', parse_dates=True) |
|
|
| |
| asset_columns = ['AAPL', 'BTC-USD', 'MSFT', 'SPY', 'TLT'] |
| test_df_assets_only = full_eval_df[asset_columns] |
|
|
| |
| bnh_values = buy_and_hold1(test_df_assets_only) |
| ewp_values = equally_weighted_rebalanced(test_df_assets_only) |
|
|
| |
| 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) |
|
|
| |
| from matplotlib.ticker import FuncFormatter |
| formatter = FuncFormatter(lambda x, p: f'${x:,.0f}') |
| ax.yaxis.set_major_formatter(formatter) |
|
|
| plt.tight_layout() |
|
|
| |
| 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() |