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()