深度解析分散化与再平衡策略的实战奥秘
最近看到一本书很有意思,讲到一个投资的决策、拿到了“加拿大国家杂志奖”。投资决策如下,以及年复合回报率:


投资策略对此我不再介绍,本文将借鉴这一核心理念,结合国内投资者可及的基金产品,选取国内指数基金、国内债券基金和国外指数基金各一只(且均拥有十年以上历史数据),通过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 | 最后价值 | 总成本 | 总价值 | 收益率 |
|---|---|---|---|---|---|---|---|
| 15 | 1687.72 | 1875.67 | 1536.03 | 1731.24 | 5099.42 | 5193.72 | 1.8% |
| 16 | 2343.15 | 2685.82 | 1975.76 | 2435.7 | 7004.73 | 7307.1 | 4.3% |
| 17 | 2578.13 | 3695.99 | 2689.17 | 3758.03 | 8963.29 | 11274.09 | 25% |
| 18 | 4559.51 | 5282.95 | 3784.9 | 4899.66 | 13627.36 | 14698.98 | 7% |
| 19 | 5692.03 | 7798.65 | 5142.55 | 8472.5 | 18633.23 | 25417.5 | 36% |
| 20 | 9886.57 | 15423.24 | 10453.57 | 17808.13 | 35763.38 | 53424.39 | 49% |
| 21 | 22218.8 | 25354.54 | 15181.76 | 27293.52 | 62755.1 | 81880.56 | 30% |
| 22 | 41151.5 | 41783.77 | 57399.42 | 41430.51 | 140334.69 | 124291.53 | -11% |
| 23 | 75729.96 | 72389.49 | 64337.26 | 71790.32 | 212456.71 | 215370.96 | 1% |
| 24 | 115898.89 | 114582.55 | 92834.38 | 121790.85 | 323315.82 | 365372.55 | 13% |
| 收益率 | 5% | 6% | 31% |
2.1.2 2020年至2025年的模拟数据
| 时间 | 易方达上证50增强A | 易方达安心回报债券B | 国泰纳斯达克100ETF | 最后价值 | 总成本 | 总价值 | 收益率 |
|---|---|---|---|---|---|---|---|
| 20 | 1444.11 | 1835.03 | 1572.49 | 2030.37 | 4851.63 | 6091.11 | 25% |
| 21 | 2850.15 | 2967.34 | 2111.57 | 3111.83 | 7929.06 | 9335.49 | 17% |
| 22 | 5008.74 | 4840.49 | 6924.95 | 4723.64 | 16774.18 | 14170.92 | -15% |
| 23 | 8951.15 | 8329.96 | 7715.96 | 8185.07 | 24997.07 | 24555.21 | -1.7% |
| 24 | 13530.95 | 13140.54 | 10965.02 | 13885.81 | 37636.51 | 41657.43 | 10.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 | 最后价值 | 总成本 | 总价值 | 收益率 |
|---|---|---|---|---|---|---|---|
| 15 | 1107.55 | 1318.45 | 1000 | 1145.65 | 3426 | 3436.95 | 0.3% |
| 16 | 1261.89 | 1536.11 | 1000 | 1291.3 | 3798 | 3873.9 | 1.9% |
| 17 | 1261.89 | 1923.24 | 1229.61 | 1824.54 | 4414.74 | 5473.62 | 23% |
| 18 | 1647.85 | 2171.69 | 1229.61 | 1887.54 | 5049.15 | 5662.62 | 12% |
| 19 | 1647.85 | 2752.45 | 1409.75 | 2804.62 | 5810.05 | 8413.86 | 44% |
| 20 | 1647.85 | 3587.81 | 1608.38 | 4033.51 | 6844.04 | 12100.53 | 76% |
| 21 | 3171.07 | 4545.66 | 1608.38 | 4968.97 | 9325.11 | 14906.91 | 59% |
| 22 | 3725.75 | 4545.66 | 5494.91 | 4611.42 | 13766.32 | 13834.26 | 0.49% |
| 23 | 6570.15 | 7075.21 | 5494.91 | 7136.44 | 19140.27 | 21409.32 | 11% |
| 24 | 7552.3 | 8344.1 | 5494.91 | 8985.75 | 21391.31 | 26957.25 | 26% |
| 18% | 7% | 63% |
2.2.2 2020年至2025年的模拟
为了与“每月动态补齐”策略进行更直接的对比,我们也模拟了“年末只买不卖强制再平衡”策略在2020年至2025年间的表现:
| 时间 | 易方达上证50增强A | 易方达安心回报债券B | 国泰纳斯达克100ETF | 最后价值 | 总成本 | 总价值 | 收益率 |
|---|---|---|---|---|---|---|---|
| 20 | 1000 | 1297.85 | 1070.82 | 1438.16 | 3368.67 | 4314.48 | 28% |
| 21 | 1543.11 | 1639.38 | 1070.82 | 1771.71 | 4253.31 | 5315.13 | 24% |
| 22 | 1740.89 | 1639.38 | 2456.58 | 1644.22 | 5836.85 | 4932.66 | -15% |
| 23 | 2755.07 | 2541.3 | 2456.58 | 2544.53 | 7752.95 | 7633.59 | -1.5% |
| 24 | 3105.26 | 2993.73 | 2456.58 | 3203.91 | 8555.57 | 9611.73 | 12% |
| 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、结论与投资启示
这份投资决策的魅力在于其纪律性和系统性。它强制投资者进行定期的组合审查与调整,避免了追涨杀跌的情绪化操作。
从模拟数据来看,即使采用“只买不卖,补齐至最高基金价值”的年末再平衡策略,长期来看也能获得正向收益。但需要注意的是:
- 市场周期性影响巨大:如2022年市场下行时,即使是再平衡策略也无法避免账面价值的短期亏损。
- “只买不卖”的局限性:在市场整体下跌时,该策略会持续买入低估资产,虽然有助于长期成本摊平,但短期内可能导致亏损扩大,并且无法像“买卖双向”的再平衡那样主动锁定部分收益。
- 核心资产选择的关键:国泰纳斯达克100ETF的突出表现,强调了选择具有长期增长潜力且与全球经济趋势相符的资产的重要性。分散化投资虽然能降低风险,但投资组合的整体增长引擎往往来源于少数高增长资产。
最终,选择哪种再平衡频率和方式,取决于投资者的风险承受能力、对资金流动性的需求以及对市场走势的判断。对于追求简单、纪律性强且不愿主动卖出盈利资产的投资者,年末的“只买不卖”策略不失为一种选择,它能避免错过潜在的长期反弹。然而,投资者应始终保持对市场基本面的理解,并对潜在的短期波动保持理性预期。
我的感触:股市有风险、投资需谨慎。投资还是需要长期的价值投资,短期内的不确定性太大
720

被折叠的 条评论
为什么被折叠?



