☰
基于Zipline的A股回测改造:从数据链路到撮合规则
2026/10/12 1:51:49 网站建设 项目流程

简介:一套面向A股市场的Python开源量化平台框架,基于Quantopian的ZipLine深度本地化改造,解决原版偏重美股、无法适配A股T+1规则与交易机制的问题,适合量化投资者、研究员以及具备Python/pandas基础的策略开发学习者。压缩包共366个文件、约3.42MB,以225个py源码文件为核心,辅以xlsx数据文件、txt说明文档、yaml配置文件、csv与ipynb示例,以及sh/bat运行脚本和state版本文件,覆盖数据接入、策略编写、回测执行等模块,结构完整。目前已有4382人学习下载。框架针对A股特性做了多项优化,包括接入交易所及第三方数据源、处理分红配股事件、考虑交易费用与滑点;用户可利用pandas完成行情清洗与因子计算,通过matplotlib绘制价格走势、策略收益曲线和交易信号图。包内附带数据源对接示例、策略模板及可视化脚本,便于快速开展本地回测与研究验证。

1. 基于 ZipLine 修改的开源量化平台框架:为什么 A 股回测结果总跟实盘对不上

在 A 股做策略回测,最容易被坑的往往不是策略本身,而是框架把数据算错了。常见的现象是:你拿了现成的 ZipLine 示例策略,把股票代码换成 A 股、数据文件换成国内行情,跑出来的净值曲线在除权日出现一根假暴跌,在涨停日出现一笔现实中根本不可能成交的买单。这个开源量化平台框架就是基于 ZipLine 做的一轮 A 股化改造:本地行情 CSV 灌入、交易日历替换、T+1 可卖校验、涨跌停过滤和 A 股佣金模型都被纳入回测循环。它适合两类人:一类是准备把美股示例迁移到 A 股、想保留事件驱动回测框架的开发者;另一类是被现成平台黑匣子坑过、想自己掌握数据链路和撮合细节的从业者。这篇把改造主线、关键参数和翻车点拆开讲,照着就能在本地把整条链路跑通。

2. 行情接入与交易日历:把 A 股 CSV 灌入 ZipLine 的四个关键改造点

2.1 改造主线:从 Bundle 到 DataPortal 的数据链路

先看清楚 ZipLine 原版的数据链路长什么样,才知道 A 股化到底改在哪一层。原版流程是:bundle ingest把历史行情下载并转成列存储文件,然后DataPortal按交易日历切出每天的行情窗口,策略层通过data.current或者Pipeline拿数据,最后交给撮合引擎产生 order 和 transaction。

这个设计的好处是数据管道和策略逻辑解耦,坏处是它默认的一切都是美股口径:默认日历是纽约交易所的,默认数据源是一份美股 bundle,默认滑点和佣金也是按美股规则写的。所以 A 股化改造的主线不是改策略 API,而是把整条链路的前半段换掉:自定义 bundle、替换日历、在数据落地前把复权和涨跌停价格算好。

常见做法是拿到源码后先跑一遍自带的美股 demo,确认环境本身没坏,再动数据层。这样后面出问题,能快速定位是数据问题还是撮合问题。我一般会把改造拆成四个点:CSV 灌入、交易日历、复权口径、涨跌停价。前两个解决「能不能跑」,后两个解决「跑得对不对」。

2.2 自写 bundle:CSV 灌入与字段规整

A 股行情最普适的格式是行情软件导出的 CSV,列不外乎 date、code、open、high、low、close、volume,外加复权因子或者直接给前复权价格。ZipLine 原版要求数据接入到一个带 asset 元信息的存储结构里,所以第一步是把 CSV 按 code 分组,写成框架能读的列存储。

# 简化示意:bundle 里的核心 ingest 逻辑,路径和目录结构以改动版源码为准 import pandas as pd import bcolz def ingest_a_share_csv(csv_path, output_dir): df = pd.read_csv(csv_path) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values(["code", "date"]) # 按股票代码分组,每组写一张连续行情表 for code, group in df.groupby("code"): cols = ["open", "high", "low", "close", "volume"] table = bcolz.carray( group[cols].values, names=cols ) table.flush() # assets 元信息表:代码、名称、上市日、退市日,供 symbol() 映射用 meta = { "code": code, "start_date": group["date"].min(), "end_date": group["date"].max(), } # 写入 output_dir 下约定的目录结构 # 最后在源码的 bundles 模块里用 register() 注册该 ingest 函数

这个代码的要点有三处。第一,分组写入时不要用df.groupby()的默认排序,提前sort_values(["code", "date"])能避免同一只股票的 bar 顺序错乱,这在分钟级数据上尤其重要。第二,bcolz.carray的names参数必须和框架里EquityDailyBar的字段名一致,否则 DataPortal 取数时报字段缺失,这类报错往往在 ingest 阶段不会出现,要等到策略里第一次data.current才暴露。第三,asset 元信息表是必须的,symbol("600000.SH")靠它才能映射成内部 asset 对象。

如果你拿到的改动版里已经带了 CSV 导入入口,直接用就行,不用自己写 bcolz 层。判断标准只有一个:data.current(asset, "close")能取到数,链路就是通的。

2.3 交易日历:不要用算法推算休市日

A 股交易日历是改造里最容易想当然、也最坑的一环。很多人图省事,直接用pd.bdate_range生成工作日当交易日,跑完回测再看,净值曲线错位得离谱。原因很简单:春节、国庆、中秋调休让 A 股休市安排不是一个纯算法问题,必须按交易所每年公布的休市安排表来。

import pandas as pd # 读取交易所公布的真实交易日序列,而不是用 bdate_range 推算 def make_a_calendar(trading_days_path="data/trading_days.csv"): days = pd.read_csv(trading_days_path, dtype=str)["date"].tolist() dt = pd.DatetimeIndex(pd.to_datetime(days)).normalize() return dt # 用法:把返回的 DatetimeIndex 传入改动版框架的日历参数 # 参数说明:交易日文件里每一行是一个交易日,格式 YYYY-MM-DD,按升序排列

normalize()的目的是把时间部分全部规整到零点。如果不做这一步,日历里混入00:00:00和15:00:00两种时间戳,DataPortal 在做分钟 bar 切分时会按时间戳匹配,出现同一天两个 session 的错觉。

这里有一个容易忽略的关联点:分钟 bards 的 session 切分依赖日历,日线回测只依赖日期序列,但分钟回测还要求框架知道每个交易日的开市时间和闭市时间。A 股是 09:30–11:30、13:00–15:00,中午休市一小时,原版美股日历是连续交易时段。很多改动版会把 session 的开始和结束时间写死在日历类里,改成 A 股时段,这一点在跑分钟级回测前必须确认。

提示:把交易日文件当作配置项,而不是把日期硬编码在代码里。每年开年更新一次文件即可,不要动框架代码。

2.4 复权价格与涨跌停价:回测可信的前提

数据能不能跑通只是第一步,数据算得对不对决定回测是否可信。A 股现金分红和送转股频繁,如果 CSV 里存的是不复权价格,除权除息日当天股价会跳空,策略的收益曲线会在那一天出现一根莫名其妙的暴跌或者暴涨。常见做法是在灌数据前做前复权,把历史价格统一到当前口径。

# 在 ingest 前对每只股票做前复权和涨跌停价计算 def preprocess(df, code_name="code", limit_pct=0.10): # factor 由行情数据源给出:factor = 复权收盘 / 不复权收盘 df["adj_open"] = df["open"] * df["factor"] df["adj_high"] = df["high"] * df["factor"] df["adj_low"] = df["low"] * df["factor"] df["adj_close"] = df["close"] * df["factor"] # 涨跌停价基于不复权昨收计算,供撮合层过滤使用 df["limit_up"] = (df["prev_close"] * (1 + limit_pct)).round(2) df["limit_down"] = (df["prev_close"] * (1 - limit_pct)).round(2) return df

这里的limit_pct参数要按板块区分:主板默认 10%,创业板和科创板 20%,ST 股 5%。我一般会在配置文件里放一个板块映射表,而不是在 CSV 预处理里写死。

另外一个容易踩的实际问题是:前复权价格会导致近期价格等于真实市场价,但历史价格变成小数,如果直接把adj_close当成交价传入撮合,手续费和滑点会按错误的金额计算。因此更稳妥的做法是「收益和因子用复权价,实际下单价格用不复权价」,两者分开存,撮合时取真实价格,绩效计算时用复权口径。这个技术细节是很多回测结果虚高的根源,后面避坑章还会再展开。

3. 策略骨架与 Pipeline 因子:把美股示例改成 A 股能跑的样子

3.1 从 initialize 到 schedule_function 的 A 股差异

数据链路接好后,接下来是策略层。ZipLine 的策略框架是initialize负责设置上下文,schedule_function负责定时触发,Pipeline 负责因子计算。原版美股示例里常见的set_benchmark(symbol("SPY"))这类语句,在 A 股改动版里必须换掉,否则 benchmark 取不到数,回测启动就报错。

from zipline import run_algorithm from zipline.pipeline import Pipeline from zipline.api import ( set_benchmark, attach_pipeline, schedule_function, date_rules, time_rules, order_target_percent, symbol ) def initialize(context): context.stocks = [ symbol("600000.SH"), symbol("000001.SZ"), ] # A 股基准常见做法是使用沪深300指数 set_benchmark(symbol("000300.SH")) attach_pipeline(make_pipeline(), "my_pipeline") # 每天收盘前 5 分钟调仓 schedule_function(rebalance, date_rules.every_day(), time_rules.market_close(minutes=5))

market_close(minutes=5)在原版和 A 股版里的含义不一样——原版按美股收盘时间 16:00 倒推,A 股改动版会按 15:00 倒推。这一步能对,说明日历替换成功了。如果跑出来调仓时间还是 16:00,大概率是日历没生效,优先检查日历对象是不是被正确传进了 TradingAlgorithm,而不是怀疑 schedule_function 写错了。

股票池的构建也建议显式声明。context.stocks列表里的symbol()调用依赖 asset 元信息表,如果你在ingest阶段漏了某只股票的上市日或者退市日,这行代码会在策略启动阶段直接抛 KeyError,错误信息还很像「找不到 symbol」,容易误导排查方向。

3.2 自定义 Pipeline 因子:动量因子的标准写法

Pipeline 是 ZipLine 里做因子计算的核心 API,原版自带USEquityPricing数据集,A 股改动版一般会把数据集替换成本地口径。自定义因子的写法差别不大,核心是继承Factor类并实现compute方法。

from zipline.pipeline import Factor from zipline.pipeline.data import USEquityPricing # A股改动版通常替换为本地数据集 class Momentum21(Factor): # 21 个交易日,覆盖约一个自然月 window_length = 21 inputs = [USEquityPricing.close] def compute(self, today, assets, out, close): # close 的形状是 (window_length, n_assets) # 用第 0 天和第 -1 天的收盘价计算区间收益率 out[:] = close[-1] / close[0] - 1.0

window_length指因子回溯的交易日数量,不是自然日;inputs声明因子依赖哪些行情字段,框架会自动把 21 天的 close 切片传进compute。out是输出数组,形状和assets对齐,赋值给out[:]即可。这个写法看起来短,背后有个重要行为:对于上市不足 21 天的股票,框架会自动把对应位置置为 NaN,不需要自己处理。

A 股股票池里次新股占比不小,上市不足 21 天的股票会在动量因子输出中自动缺席。这是特性不是 bug,但会让每日可交易标的池发生变化,如果调仓逻辑里nlargest(10)直接从因子结果里选股,选出来的股票数量可能不够。因此我一般在make_pipeline里再接一个过滤层,把close.last是 NaN 的股票也剔掉,保证候选池干净。

3.3 下单函数与成交回报

Pipeline 算出评分后,调仓函数里常用order_target_percent把仓位调整到目标权重。这个 API 的优势是不用自己算差额,框架会根据当前持仓自动生成买单或者卖单。

def rebalance(context, data): results = pipeline_output("my_pipeline") if results is None: return # 剔除 NaN,按动量打分取前 10 candidates = results["momentum"].dropna() target = candidates.nlargest(10).index # 先清掉不在目标列表里的持仓,再买入目标标的 for stock in context.stocks: if stock not in target: order_target_percent(stock, 0) weight = 1.0 / len(target) for stock in target: order_target_percent(stock, weight)

pipeline_output("my_pipeline")拿到的 DataFrame 索引是 asset 对象,列名对应make_pipeline里定义的列名。nlargest(10)返回的是评分最高的 10 个资产,weight做成等权。这里最需要注意的不是下单函数本身,而是context.stocks和target的范围不一致:如果context.stocks里漏掉了某只评分高的股票,它的权重永远分配不进去,回测结果会比实际策略少持有一些仓位。我习惯把股票池的定义也做成配置项,和因子评分范围保持一致,避免两处口径漂移。

框架生成的order对象不会立刻成交,它要经过撮合引擎。所以「下单后马上查data.current」是看不到成交结果的,要等transaction事件触发。这一点在美股原版里和 A 股改动版里逻辑相同,只是 A 股版本加了更多撮合限制,下一章详细拆参数。

4. 回测参数设置:A 股手续费、滑点、T+1 与基准的默认值

4.1 佣金与印花税模型

很多从美股迁移过来的策略默认佣金为零,跑出来的收益曲线在真实 A 股环境里会被手续费吃掉一大块。A 股的费用结构和美股不一样:佣金双边收取,有最低 5 元;印花税只在卖出时收取;过户费按笔或按成交金额,比例很低但也不是零。

from zipline.finance.commission import CommissionModel, Commission # 路径以改动版为准 class AShareCommission(CommissionModel): def __init__(self, rate=2.5e-4, min_fee=5.0, stamp_duty=5e-4): self.rate = rate # 佣金费率,默认万 2.5 self.min_fee = min_fee # 单笔最低佣金 5 元 self.stamp_duty = stamp_duty # 卖出印花税,按政策调整 def calculate(self, order, transaction): amount = abs(transaction.amount * transaction.price) fee = max(amount * self.rate, self.min_fee) if transaction.amount < 0: # 卖出才收印花税 fee += amount * self.stamp_duty return Commission(fee, 0.0)

参数含义拆开讲:transaction.amount是成交股数,正数买入、负数卖出;amount是成交金额;max(amount * rate, min_fee)保证不足 5 元时按 5 元收。印花税比例我不建议写死在代码里,因为它经历过多次调整,回测时按当时政策配置,做成参数传进来比改类更省事。

注意Commission对象返回两个值,第一个是佣金费用,第二个是额外费用。如果你拿到的是老版本改动包,构造签名可能是Commission(commission_cost, tax_cost),跑之前先print一次确认。

4.2 滑点模型:模拟成交价偏差

滑点模型解决的是「想以某个价格成交,实际成交价偏离多少」的问题。美股原版常用固定滑点FixedSlippage,直接在当前价上加减一个固定值。A 股场景里这个模型有两个问题:没有考虑涨跌停限制,也没有考虑流动性不足导致的价格冲击。

# 滑点模型示意:按当前价格的一定比例计算成交价,并对涨跌停做过滤 class AShareSlippage: def __init__(self, spread_rate=0.001): self.spread_rate = spread_rate # 买卖价差比例,默认千分之一 def process_order(self, order, price, volume): if order.direction == 1: # 买入 fill_price = price * (1 + self.spread_rate) else: fill_price = price * (1 - self.spread_rate) # 如果 fill_price 超过涨停价或跌停价,按涨跌停价截断 return fill_price

spread_rate不要设太乐观。分钟级回测里千分之一在大部分大市值股票上已经够用,但小市值股票盘中价格波动大,冲击成本更容易超过这个比例。实际做法是把历史涨跌停价传进滑点模型,fill_price超过涨停价就按涨停价成交或者直接拒绝。这个过滤逻辑在改动版源码里可能已经写了,关键是确认它用的是「当前价 ± 涨跌停昨收价」的规则,而不是美股那种「按 N 倍日均量截断成交」的规则。

4.3 T+1 与停牌校验:撮合层必须加的规则

这是 A 股化改造和原版差异最大的一处。美股支持日内回转,买入的股票当天可以卖出;A 股 T+1,当日买入的持仓不可卖出。原版撮合引擎完全不理会这个限制,所以改造版通常会在撮合前加一层可卖数量的校验。

# 每个交易日开盘前清零当日买入计数 def check_sellable(context, asset, amount): hold = context.portfolio.positions[asset].amount bought_today = context.today_buys.get(asset, 0) sellable = hold - bought_today # 超过可卖数量时截断,而不是放任下单 return max(min(amount, sellable), 0)

逻辑很简单:hold是当前总持仓,bought_today是当日累计买入量,两者相减才是实际可卖数量。context.today_buys需要在每次成交时更新,买入成交就累加,第二天开盘前清零。如果你拿到的改动版没有这个逻辑,策略会在卖出持仓时「透支」当日买入的部分,回测结果看起来收益更高,但实盘根本无法执行。

停牌校验也在这层做。A 股停牌股票没有行情也没有成交,data.can_trade在停牌日返回 False。下单函数在下单前必须过滤掉这些股票,否则撮合模型会在没有 bar 的数据上强行成交,产生幽灵订单。

4.4 基准与业绩指标

基准选错会让 alpha、beta 这类指标完全失真。原版默认基准是美股指数,A 股版改成沪深300已经是通用做法,但要注意指数代码的映射方式在不同改动版里并不统一,有的是symbol("000300.SH"),有的是symbol("399300.SZ"),取决于打包时 asset 表里的代码后缀规则。

# 在 initialize 中设置 set_benchmark(symbol("000300.SH"))

除了基准,还要确认绩效计算的时间口径。原版业绩指标里的sharpe、max_drawdown默认按美股交易日数量做年化分母,比如 252 或 365。A 股每年交易日数大约在 242 天左右,改动版如果沿用美股口径,夏普比率会被轻微放大。这类差异在单次回测里看不出来,但做多策略横向对比时会出现系统性偏差。我建议在跑正式回测前,先打印一个基准指数的年化收益,和公开数据对比,对不上就说明年化分母或者复利算法有问题,优先排查,而不是急着优化策略参数。

5. 避坑注意:A 股回测里五个典型翻车现场与排查方法

5.1 除权日出现假暴跌

现象:回测净值曲线在某个日期出现一根单独的大阴线,跌幅超过 10%,且当日没有任何市场系统性下跌。点开该日的交易明细,发现某只持仓股价格从前一天的 20 元直接变到 16 元。

原因:CSV 存的是不复权价格,除权除息日股价跳空,策略计算的收益率包含了这次跳空,而实盘持仓并不会因为除权损失市值,因为股数或者现金分红已经做了补偿。原版 ZipLine 的 adjustment writer 就是处理这个的,但本地灌 CSV 时如果不写 adjustment,数据层就没有任何补偿信息。

解决:在 ingest 之前统一做前复权,用复权价格算收益和因子;如果框架支持 adjustment writer,把复权因子按日期写进去,让框架在数据读取阶段自动调整。最直接的验证方法:找一只刚除过权的股票,看回测里除权日前后收益是否接近零,如果是大幅跳空,说明复权没生效。

5.2 当日买入的股票被 T+0 卖出

现象:持仓明细里出现同一只股票当天买入又卖出,成交时间间隔不到一个交易日,收益曲线比实盘好一截。

原因:原版撮合引擎没有 T+1 概念,买入成交后,持仓里立刻就有可卖数量。策略的调仓逻辑如果写成「先清掉不在目标里的持仓,再买目标」,当天新买入的股票在下一次调仓触发时就会被当成旧持仓卖掉。

解决:检查撮合层有没有可卖数量校验,参照 4.3 的逻辑补上;同时在下单函数里,卖出时传check_sellable返回值而不是原始目标数量。跑完回测后,把买卖交易按股票代码和时间排序,凡是同一交易日内既有买入又有卖出的记录,逐条检查是不是当日买入被卖掉。我习惯把这个检查写成一个固定脚本,每次回测完自动跑一遍,输出所有嫌疑记录。

5.3 涨停一字板还能买到

现象:回测报告显示某只股票在涨停一字板当天以涨停价成交了大量买单,但盘中根本没有卖单,实际上不可能成交。

原因:撮合模型只按价格撮合,没有检查涨跌停状态。A 股在触及涨停价时,买盘排队但卖盘极少,一字涨停时成交量趋近于零。原版的撮合模型不会感知涨停板,所以买单按收盘价或当前价成交,造成了「想做多就一定能买进」的乐观假设。

解决:在滑点模型或者撮合前置过滤里,读取当日的limit_up和volume,涨停价时买入单直接拒绝,除非当日成交量不为零,说明涨停曾打开。跌停同理,跌停价时卖出单需要格外谨慎,因为跌停排队卖单极多,成交概率极低。检查方法也简单:回测结果里找last_price等于limit_up的买入交易,一条条看成交量条件是否符合实际。

5.4 停牌股把因子全变成 NaN

现象:某天调仓时pipeline_output结果里大量股票评分是 NaN,导致nlargest(10)选出来不足 10 只,甚至为空。

原因:A 股停牌期间没有行情数据,DataPortal 返回的 close 是 NaN,Pipeline 因子在计算中自动把 NaN 传播开。原版美股数据里停牌场景非常少,所以这个问题不突出,A 股停牌重组很常见,尤其小市值股票一停就是一两个月,因子面板经常被毒化。

解决:在make_pipeline里加过滤层,把最近 N 个交易日内有缺失数据的股票排除在候选池之外;调参逻辑里用data.can_trade做二次确认,只在can_trade为 True 的股票上下单。注意不要用fillna(0)这种处理方式,NaN 变成 0 后会把停牌股票当成「低动量」标的,导致调仓时莫名买入一只还在停牌的股票,成交不了还会占用资金额度。

5.5 分钟 bar 的时区错位

现象:分钟级回测里,策略在 14:55 触发的下单,成交记录的dt时间戳却显示 06:55 或者 16:55,日期也对不上。

原因:ZipLine 内部时间戳统一用 UTC 存储,原版美股时区是纽约,A 股时区是东八区。改动版如果在日历定义或者数据读入阶段没有把本地时间显式转成 UTC 并记录时区偏移,分钟 bar 的时间索引就会整体错位 8 小时,导致 09:30 的 bar 被当作凌晨 01:30 处理。

解决:确认日历类的session时间戳基于东八区生成,在写入 bcolz 时把时间统一转成UTC存储,读取时由DataPortal按日历对象转换回本地时间。不要在策略代码里手动加减 8 小时,那样只能修正展示时间,内部撮合的时间索引仍然是乱的。快速自检方法:跑一个最简单的「每根 bar 记录当前时间」的策略,输出日志里检查每根 bar 的时间差是否均匀,不均匀就说明日历切分有问题。

6. 进阶:盘中信号盘后确认与回测日志复核

把策略从日线升级到分钟信号后,最容易出现的回测虚高来源是「盘中信号立刻成交」的假设。A 股买入时需要用对手盘价格成交,盘中信号刚出来那一刻,往往伴随着实时价格跳动,你看到的回测成交价是信号产生瞬间的 bar 收盘价,但真实盘里下单到成交会有延迟和价差。一个实用的处理方法是把信号生成和下单拆成两个时间点:上午用开盘后的数据算信号,只存结果不下单;等到收盘前几分钟再按信号调仓。这样信号用的是全天信息,执行时点固定在尾盘,流动性也相对充裕。

# 上午算信号,尾盘下单的拆分写法 def compute_signals(context, data): results = pipeline_output("my_pipeline") if results is None: context.signal_list = [] return context.signal_list = results["momentum"].dropna().nlargest(10).index def close_rebalance(context, data): targets = getattr(context, "signal_list", None) if targets is None: return for stock in context.stocks: if stock in targets: order_target_percent(stock, 1.0 / len(targets)) else: order_target_percent(stock, 0) # 执行完后清空,避免次日重复调仓 context.signal_list = None # initialize 里注册两个定时器 # schedule_function(compute_signals, date_rules.every_day(), time_rules.market_open(hours=1)) # schedule_function(close_rebalance, date_rules.every_day(), time_rules.market_close(minutes=5))

market_open(hours=1)表示开盘一小时后跑信号筛选,market_close(minutes=5)表示收盘前五分钟执行下单。这个拆分的价值不仅是减少盘口波动的影响,更重要的是让回测结果对「信号到执行的时滞」更诚实。原来信号产生和成交在同一点,回测天然偏向乐观;拆开后,信号基于上午的数据,执行基于尾盘的价格,两者之间隔着半天市场信息,策略的真实 alpha 会被更严格地考验。

回测跑完后,不要只看净值曲线,交易日志才是验证框架有没有 A 股化的关键证据。我会把transactions导出成 DataFrame,然后用三个条件自动筛查:同一交易日内买入又卖出、涨停价买入、停牌日成交。这三个条件任何一个命中,都说明撮合层的 A 股规则没有完全生效。

这个项目里最大的一块可复用资产是整套 A 股化的配置模板和数据预处理流程,直接替换 CSV 路径就能跑。拿到源码后建议先把自带 demo 原样跑通,再换成自己的数据,这样能把框架问题和自己数据问题隔离开。

以前我改完一版参数,习惯直接盯净值曲线,觉得收益上去了就收工,后来发现回测里有一笔当日买当日卖的成交记录,而实盘根本下不了这种单。从那以后我每次回测完都强制走一遍交易日志复核流程,先看规则再看收益。希望这篇里拆出的改造主线和排错顺序能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询