深度解析分散化与再平衡策略的实战奥秘-python基金分析

深度解析分散化与再平衡策略的实战奥秘

最近看到一本书很有意思,讲到一个投资的决策、拿到了“加拿大国家杂志奖”。投资决策如下,以及年复合回报率:

C:\Users\tuhou\AppData\Roaming\Typora\typora-user-images\image-20250817120628020.png
在这里插入图片描述

投资策略对此我不再介绍,本文将借鉴这一核心理念,结合国内投资者可及的基金产品,选取国内指数基金国内债券基金国外指数基金各一只(且均拥有十年以上历史数据),通过Python模拟两种不同的再平衡策略。我们将从每只基金1000元的初始固定投入开始,分别模拟在每月动态补齐仅在年末进行强制再平衡这两种策略下的投资回报率,并通过数据对比,为您的投资决策提供实战洞察。

1、基金历史数据获取

为了确保模拟的真实性和有效性,我们选取了以下三只具有长期历史数据的基金作为投资标的:

  • 国内指数基金:易方达上证50增强A (基金代码:110003)
  • 国内债基:易方达安心回报债券B (基金代码:110028)
  • 国外指数基金:国泰纳斯达克100ETF (基金代码:513100)

以天天基金网为例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
将如下表格的值拿下来就行,我直接贴代码了:

代码来源:https://blog.youkuaiyun.com/lildkdkdkjf/article/details/128479315

import requests
import pandas as pd
import datetime
import re
from lxml import etree


# 基金
def download_fund_history_data(fund_code):
    url = "https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code={}&page=1&sdate=19950101&edate={}&per=40".format(
        fund_code,
        datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d"))
    print(url)
    response = requests.get(url)
    print(response.text)
    pages = re.findall('pages:(.*),', response.text)
    int_pages = 0
    if len(pages) > 0:
        int_pages = int(pages[0])
    print("总共{}页".format(int_pages))
    td_all = []
    for i in range(int_pages):
        url = "https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code={}&page={}&sdate=19950101&edate={}&per=40".format(
            fund_code,
            i + 1,
            datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d"))
        print(url)
        response = requests.get(url)
        print(response.text)
        tables = re.findall('"(.*)"', response.text)
        if len(tables) > 0:
            table = tables[0]
            print(table)
            tree = etree.HTML(table)
            td_list = tree.xpath('//tr/td')
            # print(td_list)
            for t in td_list:
                # print(t.text)
                if t.text == None:
                    td_all = td_all + [""]
                else:
                    td_all.append(t.text)
    # print(td_all)
    columns = {
        "date": "净值日期", "a": "单位净值", "b": "累计净值", "c": "日增长率", "d": "申购状态", "e": "赎回状态"
    }
    net_date = []
    net_a = []
    net_b = []
    net_c = []
    net_d = []
    net_e = []
    for k, v in enumerate(td_all):
        i = k % 7
        if i == 0:
            net_date.append(v)
        if i == 1:
            net_a.append(v)
        if i == 2:
            net_b.append(v)
        if i == 3:
            net_c.append(v)
        if i == 4:
            net_d.append(v)
        if i == 5:
            net_e.append(v)

    df = pd.DataFrame({'净值日期': net_date,
                       '单位净值': net_a,
                       '累计净值': net_b,
                       '日增长率': net_c,
                       '申购状态': net_d,
                       '赎回状态': net_e})
    df.to_csv("基金{}历史数据.csv".format(fund_code), index=False, encoding="utf-8")


if __name__ == '__main__':
    download_fund_history_data("110003")

2、 投资策略与模拟分析

我们将模拟两种主要的投资策略:每月动态补齐(月平)和年末只买不卖强制再平衡(年平)。所有模拟均从每只基金初始投入1000元开始

2-1 策略一:每月动态补齐 (月平)

策略描述: 在该策略下,投资者每月会检查三只基金的价值。如果某只基金的价值低于组合中最高价值的基金,投资者将投入额外资金,将其价值补齐到与最高价值基金相同的水平。这一操作仅涉及买入,不涉及卖出,且无固定的每月投入上限,投入金额由补齐需求决定。

策略代码:

import pandas as pd
import io


def simulate_investment_with_real_data(
        fund_nav_data,  # Dictionary of fund_name -> pandas Series of monthly NAVs
        initial_investment_per_fund=1000,
        simulation_start_date_str="2015-01-01"
):
    """
    Simulates monthly portfolio value changes, including monthly contributions
    (precisely topping up underperforming funds to the highest fund's value)
    and year-end mandatory rebalancing, using actual fund NAV data.

    Parameters:
    fund_nav_data (dict): Dictionary where keys are fund names and values are
                          pandas Series containing month-end NAVs.
    initial_investment_per_fund (int): Initial investment amount per fund.
    simulation_start_date_str (str): Simulation start date (YYYY-MM-DD).

    Returns:
    pandas.DataFrame: A report containing monthly fund values, total value,
                      cumulative investment, and monthly operations.
    """
    fund_names = list(fund_nav_data.keys())
    
    # Determine the actual start and end dates for the simulation based on available NAV data
    min_date_available = min(series.index.min() for series in fund_nav_data.values())
    max_date_available = min(series.index.max() for series in fund_nav_data.values())
    
    start_date_obj = pd.to_datetime(simulation_start_date_str)
    if start_date_obj < min_date_available:
        start_date_obj = min_date_available  # Ensure simulation doesn't start before data is available
    
    end_date_obj = max_date_available  # Simulate up to the latest common available date
    
    # Generate month-end dates for NAV lookups, starting from the month before simulation_start_date
    # to enable calculation of the first month's return.
    effective_nav_fetch_start_date = start_date_obj - pd.DateOffset(months=1)
    
    all_potential_month_ends = pd.date_range(
        start=effective_nav_fetch_start_date.to_period('M').start_time,
        end=end_date_obj.to_period('M').end_time,
        freq='M'
    )
    
    # Filter for month-end dates where NAV data is available for all funds
    cleaned_month_ends_for_nav_fetch = []
    for m_end in all_potential_month_ends:
        has_all_data = True
        for fund_name in fund_names:
            if m_end not in fund_nav_data[fund_name].index:
                has_all_data = False
                break
        if has_all_data:
            cleaned_month_ends_for_nav_fetch.append(m_end)
    
    # Error if insufficient data points for return calculation
    if len(cleaned_month_ends_for_nav_fetch) < 2:
        raise ValueError(
            "Insufficient common monthly NAV data available for the specified date range and return calculation.")
    
    cleaned_month_ends_for_nav_fetch = pd.DatetimeIndex(cleaned_month_ends_for_nav_fetch)
    
    # Initialize portfolio with initial investment
    portfolio = {fund_name: float(initial_investment_per_fund) for fund_name in fund_names}
    cumulative_investment = sum(portfolio.values())  # Initial total cumulative investment in the portfolio
    cumulative_investment_per_fund = {fund_name: float(initial_investment_per_fund) for fund_name in
                                      fund_names}  # New: Cumulative investment for each fund
    
    # Store monthly report data
    monthly_report_data = []
    
    # Add the initial investment row, representing the state before the first month's operations
    monthly_report_data.append({
        "Date": (start_date_obj - pd.DateOffset(months=1)).strftime("%Y-%m"),
        # Date representing the state *before* simulation starts
        **{f: round(initial_investment_per_fund, 2) for f in fund_names},
        **{f"累计投入 ({f})": round(cumulative_investment_per_fund[f], 2) for f in fund_names},
        # New columns for per-fund cumulative investment
        "Total Value": round(sum(initial_investment_per_fund for _ in fund_names), 2),
        "累计投入 (总计)": round(cumulative_investment, 2),  # Renamed from "Cumulative Investment"
        "Monthly Operation": "初始投资每只基金1000元"
    })
    
    # Get NAVs from the month *preceding* the simulation start date to calculate the first month's return.
    prev_month_end_date = (start_date_obj - pd.DateOffset(months=1)).to_period('M').end_time
    
    prev_month_end_navs = {}
    for fund_name in fund_names:
        # Attempt to get the previous month's NAV, or the closest available NAV if exact date is missing
        relevant_navs = fund_nav_data[fund_name].loc[fund_nav_data[fund_name].index <= prev_month_end_date]
        if not relevant_navs.empty:
            prev_month_end_navs[fund_name] = relevant_navs.iloc[-1]
        else:
            raise ValueError(
                f"Missing historical NAV for {fund_name} to calculate initial return before {start_date_obj}. Cannot simulate.")
    
    # Iterate through each month in the simulation period
    for current_month_end_date in cleaned_month_ends_for_nav_fetch:
        # Skip dates before the actual simulation start month (these were only fetched for `prev_month_end_navs`)
        if current_month_end_date < start_date_obj.to_period('M').end_time:
            continue
        
        # 1. Apply actual monthly returns
        current_month_navs = {fund_name: fund_nav_data[fund_name].loc[current_month_end_date] for fund_name in
                              fund_names}
        
        for fund_name in fund_names:
            if prev_month_end_navs[fund_name] == 0:  # Avoid division by zero
                monthly_return = 0
            else:
                monthly_return = (current_month_navs[fund_name] / prev_month_end_navs[fund_name]) - 1
            
            portfolio[fund_name] *= (1 + monthly_return)
        
        # Update `prev_month_end_navs` for the next iteration
        prev_month_end_navs = current_month_navs
        
        # 2. Apply monthly contribution based on "top up to max" rule
        monthly_operations_log = []  # To store details of monthly allocation
        
        # Determine the target value for the month's rebalancing: the max value among current funds
        current_fund_values_after_growth = list(portfolio.values())
        max_fund_value_after_growth = max(current_fund_values_after_growth)
        
        added_this_month = 0  # This will be the actual amount added this month to the total portfolio
        funds_to_top_up = {}
        
        # Calculate the amounts needed to bring lower funds up to max_fund_value_after_growth
        for fund_name in fund_names:
            if portfolio[
                fund_name] < max_fund_value_after_growth - 0.01:  # Use a small epsilon for floating point comparison
                needed = max_fund_value_after_growth - portfolio[fund_name]
                funds_to_top_up[fund_name] = needed
        
        if funds_to_top_up:
            # The total amount added this month is exactly the sum of needed amounts
            total_needed_for_top_up = sum(funds_to_top_up.values())
            added_this_month = total_needed_for_top_up
            
            # Distribute this total_needed_for_top_up to the funds that need topping up
            # Sort funds needing top-up by current value to prioritize the lowest
            sorted_funds_to_top_up = sorted(funds_to_top_up.items(), key=lambda item: portfolio[item[0]])
            
            for fund_name, needed_amount in sorted_funds_to_top_up:
                # Add the exact needed amount to each fund
                portfolio[fund_name] += needed_amount
                cumulative_investment_per_fund[fund_name] += needed_amount  # Update per-fund cumulative investment
                monthly_operations_log.append(f"加仓 {fund_name}: {round(needed_amount, 2)}元 (补齐至最高基金价值)")
        else:
            # All funds are already balanced at or near the max, or all are equal.
            # In this scenario, no new investment is added.
            monthly_operations_log.append("月度补齐:组合已平衡,无基金需补齐")
            added_this_month = 0  # No new money added if already balanced per the new rule.
        
        cumulative_investment += added_this_month  # Update total cumulative investment with the actual amount added this month
        
        # 3. Year-end mandatory rebalance (if it's December)
        # This rebalance strictly sets each fund to 1/3 of the current total portfolio value.
        if current_month_end_date.month == 12:
            current_total_value_at_year_end = sum(portfolio.values())
            target_value_per_fund_hard_rebalance = current_total_value_at_year_end / len(fund_names)
            
            rebalance_details = []
            for fund_name in fund_names:
                diff = portfolio[fund_name] - target_value_per_fund_hard_rebalance
                if abs(diff) > 0.01:  # Only rebalance if difference is significant
                    if diff > 0:  # Sell
                        rebalance_details.append(f"卖出 {fund_name}: {round(diff, 2)}元")
                        # For selling, cumulative_investment_per_fund does NOT decrease
                    else:  # Buy
                        buy_amount = abs(diff)
                        rebalance_details.append(f"买入 {fund_name}: {round(buy_amount, 2)}元")
                        cumulative_investment_per_fund[
                            fund_name] += buy_amount  # Update per-fund cumulative investment for buys during rebalance
                portfolio[fund_name] = target_value_per_fund_hard_rebalance
            if rebalance_details:
                monthly_operations_log.append("年末强制再平衡: " + "; ".join(rebalance_details))
            else:
                monthly_operations_log.append("年末强制再平衡: 组合已平衡,无需操作")
        
        # Record this month's values
        monthly_report_data.append({
            "Date": current_month_end_date.strftime("%Y-%m"),
            **{f: round(v, 2) for f, v in portfolio.items()},
            **{f"累计投入 ({f})": round(cumulative_investment_per_fund[f], 2) for f in fund_names},
            # New: Per-fund cumulative investment
            "Total Value": round(sum(portfolio.values()), 2),
            "累计投入 (总计)": round(cumulative_investment, 2),  # Renamed
            "Monthly Operation": "; ".join(monthly_operations_log) if monthly_operations_log else "无特定月度补齐操作"
        })
    
    return pd.DataFrame(monthly_report_data)


# --- Main execution block ---
if __name__ == "__main__":
    fund_data = {}
    fund_names_map = {
        "基金110003历史数据-易方达上证50增强A.csv": "易方达上证50增强A",
        "基金110028历史数据-易方达安心回报债券B.csv": "易方达安心回报债券B",
        "基金513100历史数据-国泰纳斯达克100ETF.csv": "国泰纳斯达克100ETF"
    }
    
    # IMPORTANT: In a local environment, you would use pd.read_csv directly
    # assuming your CSVs are in the same directory as this script.
    # Replace the `content_fetcher.fetch` part with direct file reading.
    for fund_key, fund_name in fund_names_map.items():
        try:
            # Assuming CSV files are in the same directory as the script
            # Ensure proper encoding (utf-8 is common for Chinese characters)
            with open(fund_key, 'r', encoding='utf-8') as f:
                csv_content = f.read()
            df = pd.read_csv(io.StringIO(csv_content))
            df['净值日期'] = pd.to_datetime(df['净值日期'])
            df = df.sort_values('净值日期').set_index('净值日期')
            df_monthly_nav = df['单位净值'].resample('M').last().dropna()
            fund_data[fund_name] = df_monthly_nav
        except FileNotFoundError:
            print(f"错误: 基金数据文件 '{fund_key}' 未找到。请确保文件与脚本在同一目录下。")
            # Exit the script if a file is not found
            exit()
        except Exception as e:
            print(f"读取或处理文件 '{fund_key}' 时发生错误: {e}")
            # Exit on other file processing errors
            exit()
    
    # Define the output CSV file path
    output_csv_file = "investment_report.csv"  # You can change this filename as needed
    
    # Run the simulation
    try:
        simulation_results_df = simulate_investment_with_real_data(
            fund_nav_data=fund_data,
            initial_investment_per_fund=1000,
            simulation_start_date_str="2015-01-01"
        )
        
        # Save the DataFrame to a CSV file
        simulation_results_df.to_csv(output_csv_file, index=False, encoding='utf-8-sig')
        print(f"模拟报告已成功保存到文件: {output_csv_file}")
        
        # Also print to console in Markdown format for immediate review
        print("\n--- 模拟报告 (Markdown 格式) ---")
        print(simulation_results_df.to_markdown(index=False))
    
    except ValueError as e:
        print(f"模拟过程中发生错误: {e}")
2.1.1 2015年至2024年的模拟数据

在这里插入图片描述

时间易方达上证50增强A易方达安心回报债券B国泰纳斯达克100ETF最后价值总成本总价值收益率
151687.721875.671536.031731.245099.425193.721.8%
162343.152685.821975.762435.77004.737307.14.3%
172578.133695.992689.173758.038963.2911274.0925%
184559.515282.953784.94899.6613627.3614698.987%
195692.037798.655142.558472.518633.2325417.536%
209886.5715423.2410453.5717808.1335763.3853424.3949%
2122218.825354.5415181.7627293.5262755.181880.5630%
2241151.541783.7757399.4241430.51140334.69124291.53-11%
2375729.9672389.4964337.2671790.32212456.71215370.961%
24115898.89114582.5592834.38121790.85323315.82365372.5513%
收益率5%6%31%
2.1.2 2020年至2025年的模拟数据
时间易方达上证50增强A易方达安心回报债券B国泰纳斯达克100ETF最后价值总成本总价值收益率
201444.111835.031572.492030.374851.636091.1125%
212850.152967.342111.573111.837929.069335.4917%
225008.744840.496924.954723.6416774.1814170.92-15%
238951.158329.967715.968185.0724997.0724555.21-1.7%
2413530.9513140.5410965.0213885.8137636.5141657.4310.6%
2%5%26%
2-2 策略二:年末只买不卖强制再平衡 (年平)

策略描述: 在该策略下,投资者在每个月仅观察基金的市场表现,不进行任何资金注入或比例调整。只有在每个公历年年终(12月),才会对投资组合进行一次强制再平衡:找出三只基金中价值最高的那一只,然后将其他价值较低的基金补齐到与最高值相同的水平。这一过程严格只涉及买入操作,不卖出任何基金。

我们对“年末只买不卖强制再平衡”策略进行了两个时间跨度的模拟:

策略代码:

import pandas as pd
from datetime import datetime
import numpy as np
import io


def simulate_investment_with_real_data(
        fund_nav_data,  # Dictionary of fund_name -> pandas Series of monthly NAVs
        initial_investment_per_fund=1000,
        simulation_start_date_str="2015-01-01"  # Updated start date as per user's latest code
):
    """
    Simulates monthly portfolio value changes, with only year-end mandatory rebalancing.
    No monthly top-ups. Year-end rebalancing is "buy-only" to the highest fund's value.

    Parameters:
    fund_nav_data (dict): Dictionary where keys are fund names and values are
                          pandas Series containing month-end NAVs.
    initial_investment_per_fund (int): Initial investment amount per fund.
    simulation_start_date_str (str): Simulation start date (YYYY-MM-DD).

    Returns:
    pandas.DataFrame: A report containing monthly fund values, total value,
                      cumulative investment, and monthly operations.
    """
    fund_names = list(fund_nav_data.keys())
    
    # Determine the actual start and end dates for the simulation based on available NAV data
    min_date_available = min(series.index.min() for series in fund_nav_data.values())
    max_date_available = min(series.index.max() for series in fund_nav_data.values())
    
    start_date_obj = pd.to_datetime(simulation_start_date_str)
    if start_date_obj < min_date_available:
        start_date_obj = min_date_available  # Ensure simulation doesn't start before data is available
    
    end_date_obj = max_date_available  # Simulate up to the latest common available date
    
    # Generate month-end dates for NAV lookups, starting from the month before simulation_start_date
    # to enable calculation of the first month's return.
    effective_nav_fetch_start_date = start_date_obj - pd.DateOffset(months=1)
    
    all_potential_month_ends = pd.date_range(
        start=effective_nav_fetch_start_date.to_period('M').start_time,
        end=end_date_obj.to_period('M').end_time,
        freq='M'
    )
    
    # Filter for month-end dates where NAV data is available for all funds
    cleaned_month_ends_for_nav_fetch = []
    for m_end in all_potential_month_ends:
        has_all_data = True
        for fund_name in fund_names:
            if m_end not in fund_nav_data[fund_name].index:
                has_all_data = False
                break
        if has_all_data:
            cleaned_month_ends_for_nav_fetch.append(m_end)
    
    # Error if insufficient data points for return calculation
    if len(cleaned_month_ends_for_nav_fetch) < 2:
        raise ValueError(
            "Insufficient common monthly NAV data available for the specified date range and return calculation.")
    
    cleaned_month_ends_for_nav_fetch = pd.DatetimeIndex(cleaned_month_ends_for_nav_fetch)
    
    # Initialize portfolio with initial investment
    portfolio = {fund_name: float(initial_investment_per_fund) for fund_name in fund_names}
    # Total cumulative investment tracks only initial investment and subsequent buys during rebalance
    cumulative_investment_total = sum(portfolio.values())
    # Per-fund cumulative investment tracks initial investment and buys during rebalance for each fund
    cumulative_investment_per_fund = {fund_name: float(initial_investment_per_fund) for fund_name in fund_names}
    
    # Store monthly report data
    monthly_report_data = []
    
    # Add the initial investment row, representing the state before the first month's operations
    monthly_report_data.append({
        "Date": (start_date_obj - pd.DateOffset(months=1)).strftime("%Y-%m"),
        # Date representing the state *before* simulation starts
        **{f: round(initial_investment_per_fund, 2) for f in fund_names},
        **{f"累计投入 ({f})": round(cumulative_investment_per_fund[f], 2) for f in fund_names},
        "Total Value": round(sum(initial_investment_per_fund for _ in fund_names), 2),
        "累计投入 (总计)": round(cumulative_investment_total, 2),
        "Monthly Operation": "初始投资每只基金1000元"
    })
    
    # Get NAVs from the month *preceding* the simulation start date to calculate the first month's return.
    prev_month_end_date = (start_date_obj - pd.DateOffset(months=1)).to_period('M').end_time
    
    prev_month_end_navs = {}
    for fund_name in fund_names:
        # Attempt to get the previous month's NAV, or the closest available NAV if exact date is missing
        relevant_navs = fund_nav_data[fund_name].loc[fund_nav_data[fund_name].index <= prev_month_end_date]
        if not relevant_navs.empty:
            prev_month_end_navs[fund_name] = relevant_navs.iloc[-1]
        else:
            raise ValueError(
                f"Missing historical NAV for {fund_name} to calculate initial return before {start_date_obj}. Cannot simulate.")
    
    # Iterate through each month in the simulation period
    for current_month_end_date in cleaned_month_ends_for_nav_fetch:
        # Skip dates before the actual simulation start month (these were only fetched for `prev_month_end_navs`)
        if current_month_end_date < start_date_obj.to_period('M').end_time:
            continue
        
        # 1. Apply actual monthly returns
        for fund_name in fund_names:
            current_month_nav = fund_nav_data[fund_name].loc[current_month_end_date]
            if prev_month_end_navs[fund_name] == 0:  # Avoid division by zero
                monthly_return = 0
            else:
                monthly_return = (current_month_nav / prev_month_end_navs[fund_name]) - 1
            
            portfolio[fund_name] *= (1 + monthly_return)
        
        # Update `prev_month_end_navs` for the next iteration
        prev_month_end_navs = {fund_name: fund_nav_data[fund_name].loc[current_month_end_date] for fund_name in
                               fund_names}
        
        # Monthly Operation Log (no monthly top-up)
        monthly_operations_log = ["无月度补齐操作 (基金价值随市场波动)"]
        
        # 2. Year-end mandatory rebalance (if it's December)
        # This rebalance strictly sets each fund to 1/3 of the current total portfolio value.
        # However, per new rule, only buys are allowed to top up to the highest fund's value.
        if current_month_end_date.month == 12:
            current_fund_values_at_year_end = list(portfolio.values())
            max_fund_value_at_year_end = max(current_fund_values_at_year_end)
            
            rebalance_details = []
            for fund_name in fund_names:
                # If current fund value is less than the max, buy to reach the max
                if portfolio[fund_name] < max_fund_value_at_year_end - 0.01:  # Use epsilon for float comparison
                    buy_amount = max_fund_value_at_year_end - portfolio[fund_name]
                    
                    portfolio[fund_name] += buy_amount
                    cumulative_investment_per_fund[
                        fund_name] += buy_amount  # Update per-fund cumulative investment for buys
                    cumulative_investment_total += buy_amount  # Update total cumulative investment for buys
                    rebalance_details.append(f"买入 {fund_name}: {round(buy_amount, 2)}元 (补齐至最高基金价值)")
                # If current fund value is greater than or equal to the max, do nothing (no selling)
            
            if rebalance_details:
                monthly_operations_log.append("年末强制再平衡 (只买不卖,补齐至最高基金价值): " + "; ".join(rebalance_details))
            else:
                monthly_operations_log.append("年末强制再平衡 (只买不卖,补齐至最高基金价值): 组合已平衡或无需买入操作")
        
        # Record this month's values
        monthly_report_data.append({
            "Date": current_month_end_date.strftime("%Y-%m"),
            **{f: round(v, 2) for f, v in portfolio.items()},
            **{f"累计投入 ({f})": round(cumulative_investment_per_fund[f], 2) for f in fund_names},
            "Total Value": round(sum(portfolio.values()), 2),
            "累计投入 (总计)": round(cumulative_investment_total, 2),
            "Monthly Operation": "; ".join(monthly_operations_log)
        })
    
    return pd.DataFrame(monthly_report_data)


# --- Main execution block ---
if __name__ == "__main__":
    fund_data = {}
    # Updated fund names and CSVs as per user's latest provided code
    fund_names_map = {
        "基金110003历史数据-易方达上证50增强A.csv": "易方达上证50增强A",
        "基金110028历史数据-易方达安心回报债券B.csv": "易方达安心回报债券B",
        "基金513100历史数据-国泰纳斯达克100ETF.csv": "国泰纳斯达克100ETF"
    }
    
    # IMPORTANT: In a local environment, you would use pd.read_csv directly
    # assuming your CSVs are in the same directory as this script.
    for fund_key, fund_name in fund_names_map.items():
        try:
            # Assuming CSV files are in the same directory as the script
            # Ensure proper encoding (utf-8 is common for Chinese characters)
            with open(fund_key, 'r', encoding='utf-8') as f:
                csv_content = f.read()
            df = pd.read_csv(io.StringIO(csv_content))
            df['净值日期'] = pd.to_datetime(df['净值日期'])
            df = df.sort_values('净值日期').set_index('净值日期')
            df_monthly_nav = df['单位净值'].resample('M').last().dropna()
            fund_data[fund_name] = df_monthly_nav
        except FileNotFoundError:
            print(f"错误: 基金数据文件 '{fund_key}' 未找到。请确保文件与脚本在同一目录下。")
            exit()
        except Exception as e:
            print(f"读取或处理文件 '{fund_key}' 时发生错误: {e}")
            exit()
    
    # Define the output CSV file path
    # Renamed output file to reflect 'year-only' rebalancing and specific rule
    output_csv_file = "investment_report_year_buy_only_to_highest.csv"
    
    # Run the simulation
    try:
        simulation_results_df = simulate_investment_with_real_data(
            fund_nav_data=fund_data,
            initial_investment_per_fund=1000,
            simulation_start_date_str="2015-01-01"  # Changed back to original simulation start date
        )
        
        # Save the DataFrame to a CSV file
        simulation_results_df.to_csv(output_csv_file, index=False, encoding='utf-8-sig')
        print(f"模拟报告已成功保存到文件: {output_csv_file}")
        
        # Also print to console in Markdown format for immediate review
        print("\n--- 模拟报告 (Markdown 格式) ---")
        print(simulation_results_df.to_markdown(index=False))
    
    except ValueError as e:
        print(f"模拟过程中发生错误: {e}")
2.2.1 2015年至2024年的模拟

以下是2015年至2024年间,采用“年末只买不卖强制再平衡”策略的模拟结果:

在这里插入图片描述

时间易方达上证50增强A易方达安心回报债券B国泰纳斯达克100ETF最后价值总成本总价值收益率
151107.551318.4510001145.6534263436.950.3%
161261.891536.1110001291.337983873.91.9%
171261.891923.241229.611824.544414.745473.6223%
181647.852171.691229.611887.545049.155662.6212%
191647.852752.451409.752804.625810.058413.8644%
201647.853587.811608.384033.516844.0412100.5376%
213171.074545.661608.384968.979325.1114906.9159%
223725.754545.665494.914611.4213766.3213834.260.49%
236570.157075.215494.917136.4419140.2721409.3211%
247552.38344.15494.918985.7521391.3126957.2526%
18%7%63%
2.2.2 2020年至2025年的模拟

为了与“每月动态补齐”策略进行更直接的对比,我们也模拟了“年末只买不卖强制再平衡”策略在2020年至2025年间的表现:

时间易方达上证50增强A易方达安心回报债券B国泰纳斯达克100ETF最后价值总成本总价值收益率
2010001297.851070.821438.163368.674314.4828%
211543.111639.381070.821771.714253.315315.1324%
221740.891639.382456.581644.225836.854932.66-15%
232755.072541.32456.582544.537752.957633.59-1.5%
243105.262993.732456.583203.918555.579611.7312%
3%7%30%

我们将模拟两种主要的投资策略:每月动态补齐(月平)**和**年末只买不卖强制再平衡(年平)。所有模拟均从每只基金初始投入1000元开始。

3、两种策略的对比分析

通过对比2020年至2025年的数据,我们可以发现两种策略在收益表现上存在一些差异:

  • 月平策略 (2020-2024):最终总收益率约为 10.6%
  • 年平策略 (2020-2024):最终总收益率约为 12%

从模拟结果来看,在2020-2024这个特定时期内,“年末只买不卖强制再平衡(年平)”策略似乎略优于“每月动态补齐(月平)”策略。这可能表明,在市场波动较大且存在明显上涨趋势的时期,过于频繁的月度补齐可能导致投入成本增加,从而稀释了部分收益。而年末的集中补齐,可能更有效利用了市场年度趋势。

从十年(2015-2024)的跨度看,“年末只买不卖强制再平衡”策略的总收益率达到 13%。虽然整体收益是正的,但如您所观察,其收益率并未达到非常高的水平(例如,不足5%的年复合回报率在某些年份出现),并且在2022年经历了显著的亏损(-11%)。这体现了再平衡策略在应对市场下行风险时的局限性,以及仅依赖买入操作可能无法及时止损。

个别基金的表现也对整体收益产生了重要影响。无论是哪种策略,国泰纳斯达克100ETF (国外指数) 在单独的收益率表现上都显著高于国内的股票增强型基金和债券基金,这反映了近年来美股市场的强劲表现。易方达安心回报债券B作为债基,其收益率相对稳定,但长期增长潜力有限

4、结论与投资启示

这份投资决策的魅力在于其纪律性和系统性。它强制投资者进行定期的组合审查与调整,避免了追涨杀跌的情绪化操作。

从模拟数据来看,即使采用“只买不卖,补齐至最高基金价值”的年末再平衡策略,长期来看也能获得正向收益。但需要注意的是:

  1. 市场周期性影响巨大:如2022年市场下行时,即使是再平衡策略也无法避免账面价值的短期亏损。
  2. “只买不卖”的局限性:在市场整体下跌时,该策略会持续买入低估资产,虽然有助于长期成本摊平,但短期内可能导致亏损扩大,并且无法像“买卖双向”的再平衡那样主动锁定部分收益。
  3. 核心资产选择的关键:国泰纳斯达克100ETF的突出表现,强调了选择具有长期增长潜力且与全球经济趋势相符的资产的重要性。分散化投资虽然能降低风险,但投资组合的整体增长引擎往往来源于少数高增长资产。

最终,选择哪种再平衡频率和方式,取决于投资者的风险承受能力、对资金流动性的需求以及对市场走势的判断。对于追求简单、纪律性强且不愿主动卖出盈利资产的投资者,年末的“只买不卖”策略不失为一种选择,它能避免错过潜在的长期反弹。然而,投资者应始终保持对市场基本面的理解,并对潜在的短期波动保持理性预期。

我的感触:股市有风险、投资需谨慎。投资还是需要长期的价值投资,短期内的不确定性太大

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值