1. 从零开始:为什么选择AKShare和MFI指标?
如果你对量化交易感兴趣,但又觉得那些复杂的金融数据接口和晦涩的技术指标让人望而却步,那你来对地方了。今天我想跟你分享一个我用了很久的“懒人”组合:AKShare 加 MFI指标。这个组合特别适合刚入门的朋友,因为它既解决了数据从哪里来的问题,又提供了一个逻辑清晰、容易上手的交易策略构建思路。
我刚开始接触量化的时候,最头疼的就是数据。很多数据源要么收费昂贵,要么接口复杂,要么数据质量参差不齐。后来发现了AKShare,它就像一个免费的“金融数据百宝箱”,覆盖了股票、基金、期货、期权等各个市场,数据直接通过Python就能获取,简直是国内量化爱好者的福音。你不用再去各个网站手动下载CSV,也不用担心API调用次数限制,几行代码就能把历史行情数据拉到本地,省时省力。
那为什么是MFI指标呢?在众多技术指标里,MFI(Money Flow Index,资金流量指标)算是一个比较“实在”的指标。它不像MACD、KDJ那样纯粹基于价格,而是把价格和成交量结合起来考虑。你可以把它理解成RSI指标的“升级版”,RSI只关心价格涨跌的强度,而MFI还关心推动这个涨跌用了多少“钱”(即成交量)。这背后的逻辑很直观:如果股价上涨,并且伴随着巨大的成交量(真金白银的流入),那么这个上涨的可靠性就更高;反之,如果只是无量空涨,可能就值得警惕了。
MFI的值在0到100之间波动,通常我们用20和80作为超卖和超买的阈值。当MFI从下方突破20,被认为是资金开始流入的超卖买入信号;当MFI从上方跌破80,则被认为是资金流出的超卖卖出信号。这个逻辑非常直接,不像一些复杂的策略需要理解一大堆假设。对于初学者来说,从一个逻辑简单、易于理解的指标入手,能够快速建立起策略构建、回测和评估的完整闭环,获得正反馈,这对保持学习热情至关重要。接下来,我们就手把手,从搭建环境开始,一步步构建并分析一个属于你自己的MFI交易策略。
2. 实战第一步:搭建你的量化分析环境
工欲善其事,必先利其器。在开始写策略代码之前,我们需要先把“厨房”收拾好,把必要的“工具”备齐。别担心,整个过程就像安装几个手机APP一样简单,我会把每一步都拆解清楚。
首先,你需要一个Python环境。我强烈推荐使用 Anaconda 来管理你的Python环境。它是一个集成了Python和大量科学计算库(如pandas, numpy)的发行版,还能方便地创建独立的环境,避免不同项目之间的库版本冲突。去Anaconda官网下载安装包,一路点击“下一步”安装即可。安装好后,打开“Anaconda Prompt”(Windows)或终端(Mac/Linux),我们就可以开始安装必要的库了。
我们的核心工具是以下几个库:
- AKShare: 免费、开源的金融数据接口库,是我们的数据来源。
- TA-Lib: 技术指标计算库,非常高效和权威,我们用它来计算MFI。
- Pandas & NumPy: 数据处理和分析的基石,就像Excel的超级增强版。
- Matplotlib: 画图库,用来可视化我们的股价走势、买卖信号和收益曲线。
在Anaconda Prompt里,依次输入以下命令进行安装:
# 安装AKShare,使用清华镜像源会更快
pip install akshare -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装TA-Lib,这个稍微麻烦一点,因为需要编译
# 对于Windows用户,最简单的方法是去 https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib 下载对应你Python版本和系统位数的whl文件
# 例如,对于64位Python 3.8,就下载 TA_Lib‑0.4.24‑cp38‑cp38‑win_amd64.whl
# 然后 pip install 你下载的文件路径
# 对于Mac用户,可以用 Homebrew: brew install ta-lib,然后再 pip install TA-Lib
# 对于Linux用户,通常可以用包管理器,如 sudo apt-get install ta-lib (Ubuntu)
# 安装其他依赖库,Anaconda通常已自带,但为了保险可以再安装一次
pip install pandas numpy matplotlib
安装过程中如果遇到任何报错,别慌。最常见的就是TA-Lib安装失败,仔细对照上面的注释,找到适合你系统的方法。我当初在Windows上装TA-Lib也折腾了小半天,这都是必经之路。安装成功后,我们可以创建一个新的Python脚本文件(比如叫 mfi_strategy.py),然后开始导入这些库。
#!/usr/bin/env python
# coding: utf-8
# 引入数据分析核心库
import pandas as pd
import numpy as np
# 引入绘图库,并设置中文字体显示(避免图表出现乱码)
import matplotlib.pyplot as plt
from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体
mpl.rcParams['axes.unicode_minus'] = False # 正常显示负号
# 引入技术指标计算库
import talib as ta
# 引入时间处理模块
import time
from datetime import datetime, timedelta
# 引入我们的数据获取神器
import akshare as ak
print("所有库已成功导入!环境准备就绪。")
运行这段代码,如果没有报错,恭喜你,你的量化分析环境已经搭建成功了!这一步可能有点枯燥,但就像盖房子打地基,地基稳了,后面砌墙盖楼才快。有了这个环境,你不仅能做MFI策略,以后想尝试MACD、布林带等各种指标,或者分析不同的股票、基金,都在这同一个平台上进行,非常方便。
3. 核心操作:用AKShare获取数据并计算MFI
环境搞定,我们就要动真格的了。量化策略的核心是数据,没有数据,再好的想法也是空中楼阁。这一节,我们就用AKShare把数据“抓”下来,并计算出关键的MFI指标。
首先,我们得确定要分析什么。这里我们以上证指数(代码:sh000001)为例,因为它代表大盘走势,流动性好,数据完整。我们获取从2010年1月1日到最近一个交易日的数据。AKShare的 stock_zh_a_daily 接口可以完美胜任这个任务。
# 设置时间范围
start_date = '2010-01-01'
end_date = time.strftime("%Y-%m-%d", time.localtime()) # 获取当前日期作为结束日期
# 使用AKShare获取上证指数日线数据
print(f"正在获取上证指数数据,时间范围:{start_date} 至 {end_date}")
df = ak.stock_zh_a_daily(symbol='sh000001', start_date=start_date, end_date=end_date)
# 查看一下数据的前几行
print("数据预览:")
print(df.head())
运行后,你会看到一个包含 date(日期)、open(开盘)、close(收盘)、high(最高)、low(最低)、volume(成交量)等字段的表格。AKShare返回的数据已经非常规整了,我们只需要做一点简单的处理:把日期列设置为索引,并按照时间排序。这样后续按时间序列计算会非常方便。
# 数据预处理
df['date'] = pd.to_datetime(df['date']) # 将日期字符串转为日期时间格式
df.set_index('date', inplace=True) # 将日期设为索引
df.sort_index(inplace=True) # 按日期升序排列
# 计算每日收益率,这在后续计算策略收益时会用到
df['ret'] = df['close'] / df['close'].shift(1) - 1
print("数据预处理完成!")
print(f"数据总行数:{len(df)}")
print(df[['open', 'high', 'low', 'close', 'volume', 'ret']].head())
数据准备好了,接下来就是计算MFI指标。这里我们直接使用TA-Lib库中的 MFI 函数。这个函数需要四个价格序列(最高价、最低价、收盘价、成交量)和一个时间周期参数。MFI的经典周期是14天,所以我们传入 timeperiod=14。TA-Lib会帮我们完成所有复杂的中间计算,直接返回MFI值序列。
# 提取计算MFI所需的序列,并转换为numpy数组(TA-Lib要求的格式)
high = df['high'].values
low = df['low'].values
close = df['close'].values
volume = df['volume'].values
# 计算MFI指标
df['mfi'] = ta.MFI(high, low, close, volume, timeperiod=14)
# 由于MFI需要前14天的数据才能计算,所以前14行的MFI值是NaN,我们看看第15行之后的数据
print("MFI指标计算完成!")
print(df[['close', 'volume', 'mfi']].iloc[10:20]) # 查看第10到20行,观察MFI从NaN到有值的变化
为了更直观地感受MFI指标和股价的关系,我们可以把它们画在同一张图上。通常用上下两个子图,上面画股价走势,下面画MFI指标。
# 绘制股价与MFI指标
plt.figure(figsize=(16, 10))
# 子图1:股价走势
plt.subplot(2, 1, 1)
plt.plot(df.index, df['close'], color='royalblue', linewidth=1.5)
plt.title('上证指数走势图 (2010至今)', fontsize=15, fontweight='bold')
plt.ylabel('收盘价', fontsize=12)
plt.grid(True, linestyle='--', alpha=0.5)
# 子图2:MFI指标
plt.subplot(2, 1, 2)
plt.plot(df.index, df['mfi'], color='darkorange', linewidth=1.5)
# 在MFI图上添加20和80的阈值线
plt.axhline(y=20, color='green', linestyle='--', alpha=0.7, label='超卖线 (20)')
plt.axhline(y=80, color='red', linestyle='--', alpha=0.7, label='超买线 (80)')
plt.fill_between(df.index, 20, df['mfi'], where=(df['mfi'] <= 20), color='green', alpha=0.3)
plt.fill_between(df.index, 80, df['mfi'], where=(df['mfi'] >= 80), color='red', alpha=0.3)
plt.title('MFI资金流量指标', fontsize=15, fontweight='bold')
plt.ylabel('MFI值', fontsize=12)
plt.xlabel('日期', fontsize=12)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout() # 自动调整子图间距
plt.show()
运行这段代码,你会得到一张清晰的图表。你可以观察在股价大幅波动时,MFI指标是如何反应的。当MFI进入红色区域(>80),往往对应着一段上涨后的高位;当MFI进入绿色区域(<20),则可能对应着下跌后的低位。这种视觉上的关联,能帮你更好地理解这个指标的含义,也为下一步制定交易规则打下基础。
4. 策略逻辑与信号生成:把指标变成买卖指令
看到MFI的曲线在图上上下穿梭,你可能已经在想:到底什么时候该买,什么时候该卖呢?这就是策略逻辑要解决的问题。我们不能凭感觉在图表上指指点点,必须有一套明确、可量化的规则,让计算机能够自动执行。对于MFI指标,最经典也最直接的应用规则就是“阈值突破法”。
它的逻辑非常简单:
- 买入信号:当MFI从低于20的区域(超卖区)向上突破20线时,我们认为市场可能已经过度卖出,资金开始回流,因此产生一个买入信号。
- 卖出信号:当MFI从高于80的区域(超买区)向下跌破80线时,我们认为市场可能已经过度买入,资金开始流出,因此产生一个卖出信号(或平仓信号)。
注意,这里强调的是“穿越”,而不是“处于”。MFI小于20只代表超卖,但未必是买入时机,要等它重新站上20,才意味着下跌动能减弱,可能迎来转折。卖出信号同理。我们用代码来实现这个逻辑,在数据框df中新增两列:“收盘信号”记录买卖点,“当天仓位”记录每天是否持有股票。
# 初始化信号列
df['收盘信号'] = 0 # 0表示无信号,1表示买入,-1可以表示卖出(这里我们用0代表卖出信号,因为我们要控制仓位)
df['当天仓位'] = 0.0 # 0代表空仓,1代表满仓持有
# 循环遍历数据,生成买卖信号
# 从第15个数据点开始(因为MFI前14天是NaN)
for i in range(14, len(df)):
# 买入条件:当天MFI > 20 且 前一天MFI < 20
if df['mfi'].iloc[i] > 20 and df['mfi'].iloc[i-1] < 20:
df.loc[df.index[i], '收盘信号'] = 1 # 在当天收盘位置标记买入信号
print(f"买入信号:日期 {df.index[i].date()}, MFI值 {df['mfi'].iloc[i]:.2f}")
# 卖出条件:当天MFI < 80 且 前一天MFI > 80
if df['mfi'].iloc[i] < 80 and df['mfi'].iloc[i-1] > 80:
df.loc[df.index[i], '收盘信号'] = 0 # 在当天收盘位置标记卖出信号(我们用0表示)
print(f"卖出信号:日期 {df.index[i].date()}, MFI值 {df['mfi'].iloc[i]:.2f}")
print("\n信号生成完毕!")
生成了“收盘信号”,但这还不是仓位。“收盘信号”发生在当天收盘时,我们无法以收盘价成交。一个更实际的假设是:在出现买入信号的下一个交易日开盘时,我们以开盘价买入;在出现卖出信号的下一个交易日开盘时,我们以开盘价卖出。所以,我们需要根据“收盘信号”来推算每天的持仓状态。
# 计算仓位:将前一天的信号,作为今天的仓位(因为信号基于收盘价,第二天才能行动)
df['当天仓位'] = df['收盘信号'].shift(1)
# 初始仓位为0(空仓),并且向前填充仓位值。
# 这意味着一旦买入(仓位变为1),将一直保持持仓,直到遇到卖出信号(仓位变为0)
df['当天仓位'].fillna(method='ffill', inplace=True)
# 将最初的NaN值(信号产生前)填充为0
df['当天仓位'].fillna(0, inplace=True)
# 为了方便后续计算,我们从第一个买入信号的前一天开始截取数据
# 因为在这之前仓位始终为0,没有交易发生
if (df['当天仓位'] == 1).any(): # 检查是否有过买入
first_buy_date = df[df['当天仓位'] == 1].index[0]
start_date_for_calc = first_buy_date - timedelta(days=1)
df_strategy = df.loc[start_date_for_calc:].copy()
else:
df_strategy = df.copy()
print("警告:在整个回测期内未产生任何买入信号!")
# 将回测起始日的收益率和仓位强制设为0,代表从这一天开始计算资金曲线
df_strategy.iloc[0, df_strategy.columns.get_loc('ret')] = 0
df_strategy.iloc[0, df_strategy.columns.get_loc('当天仓位')] = 0
print("仓位计算完成,回测数据准备就绪。")
print(df_strategy[['close', 'mfi', '收盘信号', '当天仓位']].head(20))
现在,df_strategy 这个数据框里,我们已经有了每一天的收盘价、MFI值、产生的信号以及我们实际持有的仓位(0或1)。接下来,我们就可以基于这个仓位,来模拟我们的投资组合净值如何变化了。但在看枯燥的数字之前,我们先让图表来说话,把买卖信号直观地标记在股价走势图上。
# 绘制带买卖信号的股价图
plt.figure(figsize=(16, 8))
plt.plot(df_strategy.index, df_strategy['close'], color='steelblue', linewidth=1.2, label='上证指数收盘价')
# 标记买入点(绿色向上箭头)
buy_signals = df_strategy[df_strategy['收盘信号'] == 1]
plt.scatter(buy_signals.index, buy_signals['close'], color='limegreen', marker='^', s=100, zorder=5, label='买入信号')
# 标记卖出点(红色向下箭头)
sell_signals = df_strategy[df_strategy['收盘信号'] == 0]
# 注意,我们只标记那些因为MFI下穿80而产生的卖出信号,而不是所有仓位为0的点
# 简化处理:标记仓位从1变为0的那一天(即前一天仓位为1,当天信号为0)
sell_dates = df_strategy[(df_strategy['收盘信号'] == 0) & (df_strategy['当天仓位'].shift(1) == 1)].index
sell_prices = df_strategy.loc[sell_dates, 'close']
plt.scatter(sell_dates, sell_prices, color='crimson', marker='v', s=100, zorder=5, label='卖出信号')
plt.title('上证指数MFI策略买卖信号示意图', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('收盘价', fontsize=12)
plt.legend(loc='upper left')
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
这张图会让你对策略的触发频率和时机有一个整体的感觉。你会发现,信号并不是频繁出现的,这符合MFI作为摆动指标的特性——它试图捕捉阶段性的极端情绪点。信号点都出现在股价波动相对剧烈的区域。现在,我们已经把冰冷的指标数值转化为了明确的交易指令,下一步就是看看,如果严格执行这些指令,我们的钱包会发生什么变化。
5. 策略回测与绩效评估:你的策略真的赚钱吗?
信号有了,仓位也确定了,是骡子是马,得拉出来溜溜。策略回测就是根据历史数据,模拟真实交易,计算出策略的收益、风险等各项指标,看看这个策略在过去表现如何。这是量化交易中最关键也最令人兴奋的环节之一。
首先,我们根据每天的仓位和指数的涨跌幅,来计算策略的资金净值曲线。这里我们做一个简化假设:策略要么满仓持有上证指数(仓位=1),要么空仓(仓位=0)。交易成本(手续费、印花税等)我们先忽略,但在实际应用中,这部分必须考虑。
# 计算策略净值
# 策略每日收益率 = 指数每日收益率 * 当日仓位
# 策略净值 = (1 + 策略每日收益率)的累计乘积
df_strategy['策略每日收益率'] = df_strategy['ret'] * df_strategy['当天仓位']
df_strategy['策略净值'] = (1 + df_strategy['策略每日收益率']).cumprod()
# 同时,我们也计算一个简单的买入持有(Buy and Hold)策略的净值作为基准
df_strategy['指数净值'] = (1 + df_strategy['ret']).cumprod()
# 选取最近几年的数据来展示,图形会更清晰(例如从2018年开始)
df_recent = df_strategy.loc['2018-01-01':]
# 绘制策略净值与基准净值对比图
plt.figure(figsize=(16, 8))
plt.plot(df_recent.index, df_recent['策略净值'], color='darkorange', linewidth=2.5, label='MFI策略净值')
plt.plot(df_recent.index, df_recent['指数净值'], color='steelblue', linewidth=1.5, alpha=0.8, label='上证指数净值(买入持有)')
plt.title('MFI策略 vs 买入持有策略 净值对比 (2018至今)', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('净值(起始为1)', fontsize=12)
plt.legend(loc='upper left')
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
光看净值曲线还不够,我们需要一些量化的指标来客观评价策略的好坏。一个合格的策略评估至少要包含收益和风险两个维度。下面我们来计算几个最常用的绩效指标。
# 计算关键绩效指标
# 1. 总收益率
total_return_strategy = df_strategy['策略净值'].iloc[-1] - 1
total_return_index = df_strategy['指数净值'].iloc[-1] - 1
# 2. 年化收益率(假设一年250个交易日)
num_days = len(df_strategy)
annual_return_strategy = (1 + total_return_strategy) ** (250 / num_days) - 1
annual_return_index = (1 + total_return_index) ** (250 / num_days) - 1
# 3. 最大回撤(Max Drawdown) - 衡量历史最大亏损风险
# 计算历史净值曲线的前缀最大值
rolling_max = df_strategy['策略净值'].cummax()
# 计算当前净值距离前缀最大值的回撤
drawdown = (rolling_max - df_strategy['策略净值']) / rolling_max
max_drawdown_strategy = drawdown.max()
# 同样计算基准的最大回撤
rolling_max_index = df_strategy['指数净值'].cummax()
drawdown_index = (rolling_max_index - df_strategy['指数净值']) / rolling_max_index
max_drawdown_index = drawdown_index.max()
# 4. 夏普比率(Sharpe Ratio) - 衡量风险调整后的收益
# 假设无风险年化收益率为3%
risk_free_rate_annual = 0.03
risk_free_rate_daily = risk_free_rate_annual / 250
# 策略超额收益率(日度)
excess_return_daily = df_strategy['策略每日收益率'] - risk_free_rate_daily
# 计算夏普比率(年化)
sharpe_ratio = np.sqrt(250) * excess_return_daily.mean() / excess_return_daily.std()
# 5. Alpha & Beta (需要基准的日收益率序列)
df_strategy['策略收益率'] = df_strategy['策略净值'].pct_change()
df_strategy['指数收益率'] = df_strategy['ret'] # 这里直接用ret,因为指数净值是(1+ret)的累积,其日收益率就是ret
# 计算协方差和方差
covariance = df_strategy[['策略收益率', '指数收益率']].cov().iloc[0,1]
variance_index = df_strategy['指数收益率'].var()
beta = covariance / variance_index
# Alpha = 策略年化收益 - Beta * 基准年化收益
alpha = annual_return_strategy - beta * annual_return_index
# 格式化输出结果
print("="*60)
print("MFI策略绩效评估报告")
print("="*60)
print(f"回测期:{df_strategy.index[0].date()} 至 {df_strategy.index[-1].date()}")
print(f"总交易日数:{num_days}")
print("-"*60)
print(f"{'指标':<15} {'MFI策略':<20} {'买入持有基准':<20}")
print("-"*60)
print(f"{'累计收益率':<15} {total_return_strategy*100:>6.2f}%{'':<14}{total_return_index*100:>6.2f}%")
print(f"{'年化收益率':<15} {annual_return_strategy*100:>6.2f}%{'':<14}{annual_return_index*100:>6.2f}%")
print(f"{'最大回撤':<15} {max_drawdown_strategy*100:>6.2f}%{'':<14}{max_drawdown_index*100:>6.2f}%")
print("-"*60)
print(f"策略Alpha(超额收益): {alpha:.4f}")
print(f"策略Beta(市场风险暴露): {beta:.4f}")
print(f"夏普比率(年化): {sharpe_ratio:.4f}")
print("="*60)
运行这段代码,你会得到一份详细的策略“体检报告”。你需要重点关注这几个指标:
- 年化收益率:策略每年平均能赚多少钱?是否跑赢了简单买入持有(基准)?
- 最大回撤:历史上你最多会亏掉本金的百分之多少?这个数字直接关系到你的心理承受能力。一个年化收益20%但最大回撤50%的策略,很可能在下跌中途就被你放弃了。
- 夏普比率:每承担一单位风险,能获得多少超额回报?通常大于1算不错,大于2就算优秀了。它综合衡量了收益和波动。
- Alpha和Beta:Beta表示策略收益对市场波动的敏感程度(Beta=1表示跟市场同步)。Alpha是扣除市场波动影响后,策略自身的“真本事”,正的Alpha是每个主动管理者追求的目标。
通过这份报告,你就能客观地判断,这个基于MFI的简单策略,在历史数据上是否有效,它的收益风险特征是否符合你的预期。我回测过很多次,在不同的时间段和参数下,结果差异可能很大,这引出了我们下一个要讨论的重要话题:策略的参数和稳健性。
6. 参数优化与策略思考:让策略更上一层楼
看到回测结果,你可能会有疑问:为什么MFI的阈值偏偏是20和80?用15和85行不行?用30和70呢?回测期从2010年开始,如果从2015年牛市开始结果会怎样?这些问题都指向量化策略的两个核心概念:参数优化和过拟合。
首先,我们来尝试优化一下MFI的买卖阈值。我们把买入阈值(L)和卖出阈值(H)作为变量,测试多组参数,看看哪一组在历史上表现最好。这里我们用一个简单的网格搜索来实现。
# 定义一个函数,方便我们测试不同参数
def backtest_mfi_strategy(data, buy_threshold=20, sell_threshold=80):
"""
回测MFI策略
data: 包含'high','low','close','volume'列的DataFrame
buy_threshold: 买入信号阈值(MFI上穿此值)
sell_threshold: 卖出信号阈值(MFI下穿此值)
返回:策略净值序列,以及最终的年化收益率和最大回撤
"""
df = data.copy()
# 计算MFI
high = df['high'].values
low = df['low'].values
close = df['close'].values
volume = df['volume'].values
df['mfi'] = ta.MFI(high, low, close, volume, timeperiod=14)
# 生成信号
df['signal'] = 0
for i in range(14, len(df)):
if df['mfi'].iloc[i] > buy_threshold and df['mfi'].iloc[i-1] < buy_threshold:
df.loc[df.index[i], 'signal'] = 1
if df['mfi'].iloc[i] < sell_threshold and df['mfi'].iloc[i-1] > sell_threshold:
df.loc[df.index[i], 'signal'] = 0
# 计算仓位
df['position'] = df['signal'].shift(1).fillna(method='ffill').fillna(0)
# 计算策略净值(简化版,从第一个有效信号开始)
if (df['position'] == 1).any():
start_idx = df[df['position'] == 1].index[0]
df = df.loc[start_idx:].copy()
df.iloc[0, df.columns.get_loc('ret')] = 0
df.iloc[0, df.columns.get_loc('position')] = 0
df['strategy_ret'] = df['ret'] * df['position']
df['strategy_net'] = (1 + df['strategy_ret']).cumprod()
# 计算绩效
total_ret = df['strategy_net'].iloc[-1] - 1
annual_ret = (1 + total_ret) ** (250 / len(df)) - 1
rolling_max = df['strategy_net'].cummax()
drawdown = (rolling_max - df['strategy_net']) / rolling_max
max_dd = drawdown.max()
return df['strategy_net'], annual_ret, max_dd
# 准备数据(用之前获取的df)
data_for_test = df[['high', 'low', 'close', 'volume']].copy()
data_for_test['ret'] = df['ret']
# 测试多组参数
results = []
buy_thresholds = [15, 20, 25, 30]
sell_thresholds = [70, 75, 80, 85, 90]
print("开始参数扫描...")
for L in buy_thresholds:
for H in sell_thresholds:
if L >= H: # 买入阈值必须小于卖出阈值
continue
_, ann_ret, max_dd = backtest_mfi_strategy(data_for_test, L, H)
results.append({
'买入阈值(L)': L,
'卖出阈值(H)': H,
'年化收益率(%)': ann_ret * 100,
'最大回撤(%)': max_dd * 100,
'收益回撤比': (ann_ret / max_dd) if max_dd != 0 else 0 # 一个简单的综合指标
})
# 将结果转为DataFrame并排序
results_df = pd.DataFrame(results)
results_df.sort_values(by='收益回撤比', ascending=False, inplace=True)
print("\n参数优化结果(按收益回撤比排序):")
print(results_df.head(10).to_string(index=False))
运行这个优化脚本,你会看到一个表格,列出了不同参数组合下的策略表现。你可能会发现,(20, 80) 这组经典参数未必是最优的,在某些时间段,(15, 90) 或者 (25, 75) 可能表现更好。这就是参数优化的魅力,也是陷阱所在。
警惕过拟合! 如果我们仅仅选择历史回测中表现最好的那组参数,很可能只是它“恰好”完美拟合了历史数据的噪音,而不是抓住了市场的本质规律。这样的策略在未来实盘时很容易失效。为了避免过拟合,我们可以采取以下方法:
- 样本外测试:把数据分成两段,前70%用来优化参数(训练集),后30%用来测试用最优参数的表现(测试集)。如果策略在测试集上表现大幅下滑,可能就是过拟合了。
- 多市场/多品种验证:用同样的参数去测试不同的股票指数(如沪深300、创业板指)或者个股,看策略是否依然有效。一个稳健的策略应该具有一定的普适性。
- 简化逻辑:与其追求复杂的参数组合,不如坚持简单、逻辑清晰的规则。MFI的(20,80)阈值虽然不一定是最优的,但它是经过长期市场检验的经典值,其稳健性可能比回测出来的“最优”参数更好。
除了参数,这个简单的MFI策略本身也有很多可以思考和改进的地方:
- 它适合什么样的市场? 在趋势明显的牛市或熊市,MFI可能会在超买/超卖区钝化,发出错误信号。它更适合震荡市。
- 可以结合其他指标吗? 比如,只有当MFI发出买入信号,并且价格站在某条均线之上时,才真正买入,这样可以过滤掉下跌趋势中的反弹假信号。
- 仓位管理:我们一直是满仓或空仓。是否可以引入仓位控制?比如MFI越低,买入仓位越重?
- 止损止盈:策略本身没有止损。如果MFI发出买入信号后市场继续下跌,会造成较大亏损。可以加入一个基于ATR(平均真实波幅)的移动止损。
这些思考远比找到一个“圣杯参数”更重要。量化交易是一个不断迭代、思考和优化的过程。这个基于AKShare和MFI的策略,为你提供了一个完整的、可运行的起点。你可以把它当成一个“模板”,尝试修改其中的规则,加入新的想法,用历史数据去验证,感受策略开发的完整流程。记住,回测的目的不是证明一个策略永远有效,而是理解其背后的逻辑、优势和局限性,从而在实盘中更好地管理风险和预期。
3409



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



