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

Pandas入门教程(二十二):股票数据分析实战——yfinance 获取数据、技术指标与收益率分析 把 Pandas 用在真实场景,才能体会它的价值。本文以茅台股票为例,用 yfinance 获取历史行情,完成缺失值处理与列筛选,绘制收盘价曲线,计算 SMA 移动平均线与 RSI 相对强弱指数,并通过日收益率、累计收益率和波动率完成完整的量化分析流程。 阅读详情

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

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

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的突出表现,强调了选择具有长期增长潜力且与全球经济趋势相符的资产的重要性。分散化投资虽然能降低风险,但投资组合的整体增长引擎往往来源于少数高增长资产。

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

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

5大策略!量化投资中如何降低非系统性风险 在量化投资领域,风险控制是决定策略长期收益的核心要素。非系统性风险(Unsystematic Risk),又称特定风险或可分散风险,源自单个证券或行业的特有事件(如公司财务造假、行业政策变化),与市场整体波动无关。如何通过组合构建降低个股/行业过度集中风险?多因子模型中因子配置的风险分散机制如何设计?动态再平衡策略的触发条件与执行逻辑如何量化?基本面数据与量化模型如何深度融合识别潜在风险? 阅读详情

相关推荐

量化价值投资领域:投资组合优化的实用指南

随着金融市场效率提升,传统价值投资正从定性分析向量化建模转型。本文聚焦投资组合优化如何通过数学模型量化价值投资的"安全边际"与"预期收益"?不同市场环境下(牛市/熊市/震荡市)的优化策略差异实时风险控制与组合再平衡的工程化实现目标是为具备金融基础的量化分析师、基金经理提供可复用的技术框架,涵盖方法论解析、代码实现模板、实盘部署建议。章节核心内容核心概念解析MPT、风险平价、Black-Litterman模型的理论基础与相互关系算法实现。

2501_92132293的博客 544

深度解析PyPortfolioOpt交易成本模型:如何在投资组合优化中精准计算买卖价差与流动性影响

PyPortfolioOpt作为Python中最强大的投资组合优化库,不仅提供了经典的有效前沿分析,还内置了完整的交易成本模型来帮助投资者更准确地评估买卖价差和流动性对投资组合的实际影响。在现实世界的投资决策中,忽略交易成本往往会导致理论收益与实际收益之间的巨大差距。 ## 为什么传统优化模型需要交易成本修正? 传统投资组合优化理论基于完美市场假设,忽略了交易成本、买卖价差和流动性冲击等现实因

gitblog_00944的博客 416

cadence数模混合仿真反标sdf

数模混合IC设计时经常要调用一些Verilog模块(最简单的例如SPI接口),在前仿时一般直接用rtl代码来仿真,但是后仿时要关注因寄生带来的延迟,避免这些额外的延迟导致时序错误,因此不能简单地采用RTL来仿真。 一般有两种办法:1是采用由RTL综合得到的门级电路来仿真,这种方法比较简单,但是缺点是规模较大,而且只包括门延迟不包括因版图导致的线延迟;如果想要仿真更精细,就要对该这部分门电路提取寄生参数; 另一种方式是利用sdf文件(standard delay format)来进行仿真。后端工具在布局布

长河柱天的博客 7274

Python投资组合分析:从数据到决策的量化投资实战指南

在量化投资领域,如何从海量金融数据中提取有价值的洞察,并转化为科学的投资决策?这是每个量化分析师面临的挑战。传统的投资分析方法往往依赖直觉和经验,而现代金融数据科学要求基于统计验证和系统性分析的方法。Python投资组合分析库Pyfolio正是为解决这一挑战而生,它提供了一套完整的工具链,帮助投资者从收益数据中提取关键指标,评估风险特征,并优化投资组合表现。 ## Python金融数据科学工具生

gitblog_01128的博客 679

跟着团子学SAP CO:熟练使用结果分析码实现各个场景下收入成本的匹配(一) KKA2/CJ88

PS模块中对财务顾问最难的点在于项目结算,特别是销售型的项目结算。针对客户型项目结算我列举了以下常见的收入成本匹配的一些场景: (1)在项目执行过程中会多次开票,但由于各期成本浮动很大(如前期可能只涉及一些人工、中期全是比较金额较大的材料费),所以希望开票的时候能通过合理的一些指标进行主营业务成本与主营业务收入的匹配。 (2)项目过程中会多次开票,开票确认主营业务收入时,直接将累计发生的实际成本结转为主营业务成本。 (3)项目过程中虽多次开票,但不确认收入,所有发生的实际成本月末计算为在制品,最后项目一次性

Lucas1211的博客 9533

CNN经典论文(一)LeNet

Gradient-Based Learning Applied to Document Recognition(梯度学习在文档识别中的应用) Yann LeCun, Leon Bottou, Yoshua Bengio, and Patrick Haffner 发表时间:December 1998 期刊:Proceedings of the IEEE 影响:世界上第一个卷积神经网络 论文地址:Gradient-Based Learning Applied to Document Recognition

loveling丶的博客 3597

PyPortfolioOpt交易成本模型深度解析:从理论到实践的投资组合优化

在金融投资组合优化领域,PyPortfolioOpt作为Python中最专业的开源库,提供了完整的交易成本建模能力。传统投资组合优化往往假设完美市场,忽略了**交易成本**、**买卖价差**和**流动性冲击**等现实因素,导致理论最优与实操结果存在显著差异。本文将深入解析PyPortfolioOpt中的交易成本模型实现原理,探讨如何在投资组合优化中合理考虑交易成本,并提供从基础应用到高级配置的完整

gitblog_00679的博客 461

磁悬浮轴承控制策略设计及性能对比

摘要:针对高速磁悬浮转子控制问题,本文提出多方案对比分析。建立含陀螺效应的5自由度动力学模型,分析陀螺耦合、质量不平衡等关键挑战。对比四种控制策略:基础PID解耦、交叉反馈+自适应陷波、H∞鲁棒+前馈补偿、滑模控制+观测器,从实时性、鲁棒性等维度评估优劣。推荐采用交叉反馈+H∞+自适应陷波的混合方案,通过内环电流环与外环位置环协同,实现陀螺效应动态解耦和扰动抑制。实施步骤包括系统辨识、控制器设计、硬件部署等,需配合实验验证与参数整定。该方案综合性能最优,适用于高转速、强扰动场景。

技术工作者 1773

美赛C题交易策略解析:从资产配置到投资组合管理的实战建模

在量化投资领域,资产配置与投资组合管理是构建稳健交易策略的核心基础。其原理在于通过数学模型优化不同资产间的权重分配,旨在实现风险与收益的最佳平衡,从而提升资金的使用效率。这一技术的核心价值在于能够系统性地控制下行风险,并通过分散化降低整体波动,为投资者提供科学的决策框架。典型的应用场景包括多资产投资、养老金管理以及量化对冲基金等。本文以2022年美赛C题为具体案例,深入剖析了如何将现代投资组合理论(MPT)与动态风险平价等经典框架应用于实战,并重点探讨了在包含加密货币与传统股票的混合资产中,如何进行有效的特

weixin_34252090的博客 405

Meta开源Ego-Exo4D数据集:如何用1400小时多模态视频训练你的AI模型

Meta开源的Ego-Exo4D数据集包含1400小时多模态视频,涵盖第一人称和第三人称视角、眼动追踪、IMU传感器等多种模态,是AI模型训练的宝贵资源。本文详细介绍了从数据集下载、预处理到模型训练的全流程,帮助研究者高效利用这一多模态数据集进行AI模型开发。

weixin_29159441的博客 342

点云处理指南介绍

指南目录,Open3D与PCL内容介绍

NNNNNathan的博客 1万+

量化交易策略全流程解析:从数据分析、模型构建到回测评估

量化交易策略是金融科技领域的核心,它通过数学模型和计算机程序,系统化地执行投资决策。其基本原理在于利用历史数据识别市场规律,构建可重复验证的交易规则,以克服人为情绪干扰,实现风险可控下的收益优化。在技术价值层面,量化策略通过严谨的回测框架,将投资理念转化为可量化的绩效指标,如夏普比率和最大回撤,为资产管理提供了科学依据。典型的应用场景包括趋势跟踪、均值回归及统计套利等。本文以一道经典竞赛题目为例,深入剖析了交易策略的开发全流程,涵盖了从数据特征工程、策略范式选择(如配对交易),到避免未来函数和过拟合等关键实

weixin_33882452的博客 390

(4-2)A*

实际已经花费的代价(g(n)):从起点到当前节点n的实际路径代价。启发式估计的剩余代价(h(n)):从当前节点n到目标节点的估计路径代价。这是A算法的关键之一,启发式函数要足够接近实际代价,但又要高效计算。如果启发式函数等于0,A算法就相当于Dijkstra算法。实现A*算法的核心步骤如下所示。(1)初始化:将起点加入开放列表(open list),并将其f值设为g值和h值的和。(2)循环:从开放列表中选择具有最小f值的节点作为当前节点。如果当前节点是目标节点,算法结束;

码农三叔 1806

AI全栈实战 | 3.9-01 爬虫入门:requests 加 BeautifulSoup 三分钟跑通,但 90% 的人栽在这 4 个“礼貌“问题上

⚠️:本文内容仅用于。请遵守目标站点的服务条款与相关法律法规,不得对目标服务造成负担,不得采集、传播个人隐私数据。技术是中性的,边界在你手里。爬虫的入门技术(requests 发请求 + BeautifulSoup 解析)三分钟就能学会。真正区分"会用爬虫"和"会做爬虫的人"的,是这篇我们跑通"请求→解析→存储"的最小闭环,然后重点把"合规与礼貌"这根弦给你绷上。

源码获取 | 运行部署 | 定制二开修改,如果回复不及时请耐心等待,请见谅。 403

制作3D地形图的方法-使用GlobalMapper和3ds Max软件

打开GlobalMapper软件,在菜单栏中选择“文件”>“打开数据文件”,然后选择相应的高程数据文件。在菜单栏中选择“渲染”>“渲染设置”,根据需要进行设置,如分辨率和输出格式。选择地形对象,点击菜单栏中的“渲染”>“材质编辑器”,然后根据需要调整材质属性,如颜色、纹理和光照等。在菜单栏中选择“工具”>“高程栅格和网格”>“创建地形网格”。在菜单栏中选择“文件”>“导出”>“导出栅格格式”。打开3ds Max软件,在菜单栏中选择“文件”>“导入”,然后选择之前导出的地形数据文件。步骤3:导出地形网格。

JqlScala的博客 1535

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

从一段视频到可漫游的 4D 有声场景:思路拆解与最小复现手记

只需一段手机视频,你就能生成可"走进去"的 4D 有声场景:转头时狗叫声方向随之变化,走近时声音更清晰。本文拆解 Video-to-4DAVScene 的核心思路,手把手带你用几十行 Python 搭出最小管线,把视频中的运动物体变成带空间位置的声源,并附上三个真实踩坑的排障经验,代码可直接跑。🎧

helloagent的博客 232

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

【每天一个 Python 小案例30】Python 基础知识综合小项目:我的工具盒

作为系列收官,本篇把前 29 篇的知识点合成一个能直接用的命令行工具"我的工具盒":计算器(用 ast 解析表达式,不碰 eval)、单位换算与温度转换(一张换算表 + 特例分支)、记账与分类统计(Decimal + JSON 持久化)、以及用"字典当命令表"把菜单和业务逻辑解耦的主循环。文中对比了 eval 的安全隐患与 ast 白名单的做法,解释了 lambda 闭包在字典里的写法与晚绑定陷阱,并在最后给出整个系列的知识地图,把变量、数据结构、控制流、函数、类、异常、文件、标准库八块内容对应到具体篇目。

xinjiyaya的博客 423

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Python+Agent入门笔记(二):列表推导式、模块和异常,这些写 Agent 前得会

接上一篇《Python+Agent入门笔记(一)》。基础语法跑通之后,这篇聊几块日常写脚本绕不开的东西。还是按 JS/Go 开发者的视角写,尽量记录我实际卡过的地方。

qq_41241612的博客 430

详解Python条件语句

在 Python 中,if...else 是一种条件语句,可以根据给定的条件执行不同的操作。这个语句通常用于控制程序的流程。

2601_96798737的博客 205

Python3 协程:异步编程的核心机制与实战

协程是解决I/O密集型任务等待瓶颈的高效机制,相比线程更轻量、无锁开销。从yield到async/await的演进,使异步编程语义清晰、易于理解。通过async/await可实现并发执行多个任务(如gather、create_task),显著提升网络请求、爬虫等场景性能。关键在于使用异步库(如aiohttp)替代同步库,避免阻塞事件循环。合理控制超时与并发数,才能发挥协程最大优势。

Lxn2zh的博客 159

python如何控制浏览器

Python 可以通过 Selenium、Playwright 等工具实现浏览器自动化,从打开网页到执行复杂交互操作均可脚本化完成。Selenium生态成熟

2601_96797912的博客 76

torch.compile 差异仅 1e-6,边界样本预测为何翻转

torch.compile 加速后输出与 eager 不一致,部分预测翻转。逐层对比与 graph break 分析定位到动态分支翻转根因。容差回归仅适用于无分支翻转场景,零容忍场景应禁用编译。

昨日种种,皆成今我。今日种种,皆成新我。 159

二进制木马免杀-Python 分离免杀与 Web 挂马

二进制木马免杀-Python 分离免杀与 Web 挂马

2503_94637398的博客 267

Python 100讲课程导航:第08篇《Python input 与类型转换:构建可靠的交互程序》

本文详解Python中input()的返回类型为字符串,强调必须通过int()、float()等函数进行类型转换。重点讲解了输入验证的重要性,结合try-except处理ValueError等异常,实现可靠的交互程序。通过购物预算计算器实战,演示如何防范非法输入、处理边界值,并推荐使用Decimal避免浮点精度问题,提升程序健壮性。

铭渊老黄 134

【Python】国家中小学智慧教育平台电子教材批量下载与 PDF 合成实战

成果:两本语文电子课本(一年级上册 118 页 / 23MB、三年级上册 130 页 / 30.3MB),合计 248 页、约 54MB,单本下载加合成 40~60 秒,PDF 页数校验全部通过。经验教训SPA 站点的数据接口优先从 bundle 里挖,比抓页面稳得多;资源链接要按 private/public 分组看待,只取平台公开部分;页数、目录名这类“可推断”的信息,永远用实际响应验证一次;受限环境里“能不能跑浏览器”要在方案定稿前确认。后续计划:这版 PDF 是图片版,没有文字层;

木头大左的博客 130

魔塔社区微调训练Qwen2-1.5B-Instruct模型并烧录到‌RK3568开发板教程

本文基于Ubuntu 22.04 + CUDA 13.0.3环境,使用LLaMA-Factory框架对Qwen2-1.5B-Instruct模型进行LoRA微调,训练儿童故事生成专用模型。通过配置qwen模板、制作alpaca格式数据集、执行sft训练并合并LoRA权重,最终将模型转换为GGUF格式,并量化为Q4_K_M以适配RK3568嵌入式平台。全程支持国产镜像加速下载与显存优化,实现高效轻量化部署。

浅清秋慕夏的博客 185
上一篇: windows沉浸式安装postgresql-%99成功
下一篇: spiderdemo第二十五题:控制台检测
AngrySnack
博客等级 码龄9年 78粉丝 14原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值