☰
Python自动量化交易系统:数据层、回测引擎与实盘执行避坑
2026/10/28 11:07:49 网站建设 项目流程

1. 先想清楚:这套系统到底要解决什么问题

我自己搭第一套 Python 自动量化交易系统的时候,出发点特别朴素——手工复盘太慢了。每天收盘后打开软件,一屏一屏翻自选标的,把均线、成交量、涨跌幅记到表格里,再凭感觉决定第二天买还是卖。这种做法最大的问题是没法验证:你觉得某个规则好用,其实只是最近几个月恰好顺手,换个时间段就完全失效。而且人一累就会偷懒,回测这件事根本没有“手感”可依赖,只有代码能一秒不差地跑完十年数据。

所谓 Python 自动量化交易系统,说白了就是把“看数据、出信号、算仓位、下单、记账、复盘”这一整条链路,从人脑搬到程序里。它包含三块核心:数据层负责把原始行情洗成干净可用的时间序列;回测引擎负责在一个尽量贴近真实的环境里验证策略;执行层负责把策略输出的信号变成真实的委托,并且管住风险。这三块里最容易出问题、也最容易被新手低估的,是中间那块——回测。

适合谁看?如果你会写点 Python,能读懂 pandas 的 DataFrame,对交易有点基本概念(知道什么叫持仓、什么叫委托),那这篇内容基本可以照着抄。如果你完全没写过代码,也不建议直接跳过基础去搞自动交易,因为这套系统里每一个环节的坑,都需要你有能力自己 debug。至于已经有几年手工交易经验、想把手感变成可验证规则的朋友,这套东西对你的价值最大——你缺的不是市场理解,是把理解翻译成代码的工程能力。

我打算按实际搭建顺序讲:先定边界和选型,再啃数据,然后手写一个不骗人的回测引擎,接着实现一个可复用的策略骨架,最后讲从回测走向执行时那些没人提前告诉你的工程细节。中间会穿插我自己踩过的坑,尤其是回测跑出漂亮曲线、实盘一上就废的那几次教训。

1.1 系统边界:哪些交给代码,哪些必须人来做

很多人一开始的设想是“全自动”,机器自己找策略、自己调参、自己交易。我劝你先把这个念头按住。代码擅长的是执行的一致性和速度,不擅长的是判断规则本身是否还有效。我的划分是这样的:

  • 完全交给代码:数据抓取与清洗、信号计算、仓位换算、下单与撤单、成交回报记录、每日对账、异常告警、绩效统计。这些工作重复度高、容错低,人做必然出错。
  • 半自动,人来拍板:参数调整、策略上下线、单一标的的黑白名单维护。我给自己定的规矩是,任何参数变更必须走一次完整的样本外验证,验证不通过就不许改,哪怕当天亏得很难受。
  • 人来判断:整体仓位中枢、是否暂停全部策略、极端行情下的人工干预。程序可以告警,但“今天不做了”这个决定,我还是留给自己。

这个边界的意义在于,它决定了你要写多少代码。如果你一上来就做“全自动策略工厂”,代码量会膨胀到你根本维护不动,最后连一个能跑的策略都没有。先做一个只会执行单一规则的笨系统,跑通全链路,再谈扩展,这是我从三次推倒重来里学到的最值钱的一条。

1.2 技术栈选型:够用就好,别追新

选型这件事,我的原则是“三年后我还能看懂自己写的代码”。下面是我实际在用的组合,以及每一项为什么选它:

环节选型选它的理由什么情况下该换
数据处理pandas + numpy时间序列对齐、rolling 计算极其方便,社区资料多单次处理超过千万行、内存吃紧时考虑 polars
数值计算加速numba(可选)循环写法的指标计算能提速十几倍纯向量化够快就不需要
数据存储SQLite / Parquet 文件单机足够,免运维,Parquet 读取快且省空间多进程并发写入频繁时换 PostgreSQL
回测引擎自研事件驱动逻辑透明,出问题能定位到行需要高频撮合模拟时考虑成熟开源框架
定时调度cron / APScheduler简单可靠,出错能立刻看出来多机部署时上任务队列
可视化matplotlib一行出图,够看就行需要交互式分析时用 plotly
日志logging + 文件轮转标准库,不引入依赖多策略并行时接集中式日志

这里我要专门解释一下为什么回测引擎建议自研,而不是直接拿来一个开源框架用。开源框架当然成熟,但它的撮合假设、成本模型、复权处理方式都是别人定的,你如果没读透源码,回测结果偏差多少你根本不知道。我吃过这个亏:早期用一个框架跑出年化很高的曲线,后来发现它的成交假设是“信号出现当天以收盘价成交”,而我的信号恰恰是用当天收盘价算出来的——这就是典型的未来函数,曲线当然好看。自研引擎代码量其实不大,核心两三百行,但每一个假设都是你亲手写的,你能清楚知道自己在骗自己哪里。

2. 数据层:把脏数据挡在门外

策略再好,喂进去的数据有错,出来的结果一定是垃圾。数据层的工作不是“抓下来存起来”这么简单,它至少要保证三件事:字段规范统一、时间对齐正确、异常值可识别。我见过太多人栽在第一步——用不同来源的数据拼一张表,结果某只标的中途复权因子变了,回测里凭空多出一段暴涨,策略信号全乱。

2.1 字段规范与统一口径

我给自己定的最小字段集是这样的:

# 每行代表一个标的在一个交易日的数据 # dt: 交易日(datetime,统一到日频,不带时分秒) # symbol: 标的代码 # open / high / low / close: 原始价格 # volume: 成交量 # amount: 成交额 # adj_factor: 复权因子 # is_suspended: 是否停牌(0/1)

这里有两个细节值得展开。第一,时间字段一定要统一成不带时区的日期对象,不要用字符串。我早期用字符串存日期,做 merge 的时候格式不一致(有的是2023-01-05,有的是2023/1/5),静默匹配失败,回测里某个标的直接少了一半数据,我盯了两天才发现。第二,复权因子要单独存,不要只存复权后的价格。原因是你可能需要在回测中同时看到“真实价格”(用于判断最小价格变动单位和委托合法性)和“复权价格”(用于计算收益率和技术指标)。只存复权价,某天真实价格是 3 块多,你的系统按复权价算出 30 块去下单,委托直接被拒。

处理复权我用的是后复权思路:以最早一日为基准,把后续价格按因子缩放。这样历史价格永远不变,新数据进来只是追加,不会导致整个历史序列被重算。前复权每次除权都要重算全部历史,对回测缓存极不友好。

def apply_adj(df): df = df.sort_values("dt").copy() base = df["adj_factor"].iloc[0] df["close_adj"] = df["close"] * df["adj_factor"] / base df["open_adj"] = df["open"] * df["adj_factor"] / base return df

2.2 停牌、异常价格与缺失值

停牌的处理方式直接决定回测的真实性。我的做法是:volume == 0或open <= 0的交易日标记为停牌,停牌日不允许产生任何成交,持仓按最后有效价格估值。这一条如果漏掉,回测会出现“停牌期间还能买卖”的幻觉,尤其在小市值标的上偏差极大。

价格异常值我设了三道检查,每天跑完数据后自动输出报告:

  • 单日涨跌幅绝对值超过阈值(比如 21%)的记录,逐条人工确认是真实波动还是数据错误;
  • high < low或close落在[low, high]之外的记录,直接判为脏数据丢弃;
  • 成交额为 0 但成交量大于 0,或反之,标记可疑。
def validate(df): bad = df[ (df["high"] < df["low"]) | (df["close"] > df["high"] * 1.001) | (df["close"] < df["low"] * 0.999) | ((df["volume"] > 0) & (df["amount"] <= 0)) ] return bad

提示:脏数据不要“自动修正”,要“自动报警”。我试过用前值填充异常价格,结果填充出来的假数据被策略当成真实信号,白白亏了一次。宁可当天这只标的停用,也不要喂假数据。

2.3 一个被低估的坑:幸存者偏差

如果你的标的池是“当前还在交易的标的”,那你的回测天然带着幸存者偏差——那些退市、被合并、长期停牌的标的根本不在你的数据里。这会系统性地高估策略表现,因为表现最差的那批标的被你悄悄剔除了。

我现在的做法是,在数据层维护一张标的生命周期表,记录每个标的的上市日、退市日、存续状态。回测时按日期动态生成当日可交易列表,而不是用一个固定的静态列表。这张表维护起来确实麻烦,尤其是历史退市标的的数据获取,但如果你的策略是选股类而非单标的择时类,这一步不做,回测结论基本不可信。对于只做单一宽基指数或少数几个大标的的策略,这个问题影响小得多,可以暂时简化。

3. 回测引擎:写一个“不骗人”的撮合器

回测引擎的本质是一个时间推进器 + 一个记账本。时间每往前走一天,你把当天能看到的数据喂给策略,策略吐出信号,撮合器按预设规则决定成不成交、成交多少、扣多少成本,记账本更新持仓和现金。听起来简单,但魔鬼全在“当天能看到什么”和“按什么价格成交”这两句话里。

3.1 事件驱动还是向量化:我为什么选前者

向量化回测写起来快,一行signal.shift(1) * ret就能出净值曲线,适合快速验证想法。但它有三个致命短板:无法表达仓位约束(比如同时最多持有 5 只,向量化很难处理)、无法表达资金约束(现金不够时买不了)、无法表达路径依赖(比如移动止损,取决于持仓期间的最高价)。

事件驱动回测慢一些,但逻辑透明、可扩展。我的引擎核心结构长这样:

class BacktestEngine: def __init__(self, data, strategy, cash=100_000, cost=None): self.data = data # dict: symbol -> DataFrame self.strategy = strategy self.cash = cash self.positions = {} # symbol -> 持仓数量 self.cost = cost or CostModel() self.trades = [] self.equity_curve = [] def run(self, dates): for dt in dates: bar = self._slice_bar(dt) # 当天可见数据 signals = self.strategy.on_bar(dt, bar, self.positions) orders = self._to_orders(signals, bar) # 信号转委托 fills = self._match(orders, bar) # 撮合 self._apply(fills, dt) # 更新账本 self.equity_curve.append(self._equity(bar, dt))

关键在_slice_bar:它只返回dt当天及以前的数据,策略绝对看不到未来。这个函数是我整个引擎的安全阀,任何“策略为什么表现这么好”的疑问,第一件事就是回来检查这里有没有被绕过。

3.2 撮合规则与成本模型:参数怎么定

撮合规则我定得比较保守,宁可低估收益:

  • 信号在T 日收盘后计算,T+1 日开盘价成交。这样彻底杜绝“用收盘价算信号又用收盘价成交”的未来函数;
  • 开盘价高于当日涨停价或低于跌停价时无法成交(用前收盘价乘以涨跌幅限制估算),直接顺延到下一日重试;
  • 停牌日不成交;
  • 单笔委托数量向下取整到最小交易单位。

成本模型我拆成四块,每一项都要在回测里扣掉,否则你的年化收益会虚高出一大截:

class CostModel: def __init__(self, commission_rate=0.0003, min_commission=5.0, stamp_tax=0.0005, slippage_bps=2.0): self.commission_rate = commission_rate self.min_commission = min_commission self.stamp_tax = stamp_tax # 仅卖出方向收取 self.slippage_bps = slippage_bps def buy_cost(self, price, qty): amt = price * qty slip = amt * self.slippage_bps / 10000 comm = max(amt * self.commission_rate, self.min_commission) return slip + comm def sell_cost(self, price, qty): amt = price * qty slip = amt * self.slippage_bps / 10000 comm = max(amt * self.commission_rate, self.min_commission) tax = amt * self.stamp_tax return slip + comm + tax

我拿 10 万本金、单笔买入 2 万元举例算一遍:佣金 20000 × 0.0003 = 6 元(高于最低 5 元,按 6 元计),滑点 20000 × 0.0002 = 4 元,买入合计 10 元。卖出时再加 20000 × 0.0005 = 10 元的税费,合计 20 元。一次完整买卖的摩擦成本约 30 元,占本金 2 万的 0.15%。如果你的策略平均持仓周期是 5 天,一年交易约 50 次,光成本就吃掉 7.5% 的收益。这就是为什么高频调仓的策略在回测里必须把成本设足,否则实盘必崩。

注意:滑点参数不要拍脑袋。我建议的做法是,用你自己的历史成交记录反推:统计同一标的、同一方向、相似金额的委托,实际成交均价与信号日收盘价的偏离幅度,取中位数再往上加一点作为保守值。我实测下来,中小金额委托的滑点比想象中小,但流动性差的标的能到万分之十几。

3.3 三类“作弊”:未来函数、幸存者偏差、过拟合

未来函数是最隐蔽的。除了前面说的“收盘价信号配收盘价成交”,还有几种变体:用了未来才公布的财务数据(财报有披露滞后)、用了全样本的标准化参数(比如用全历史数据的均值做 Z-Score 标准化)、用了未来才知道的标的池。我判断有没有未来函数的方法很土但有效:把回测拆成“逐日增量计算”和“全量一次性计算”两遍,如果两者结果不一致,一定有问题。因为全量计算很容易不小心用到未来信息,而逐日计算天然只能看到过去。

幸存者偏差前面讲过,靠标的生命周期表解决。这里补充一个更隐蔽的变体:指数成分股偏差。如果你用“当前某指数的成分股”作为历史标的池,那些曾经在指数里、后来被调出的标的不在池子里,同样是幸存者偏差。正确做法是用历史时点的成分名单,这个数据一般不太好拿,如果拿不到,就老实承认这个偏差,在结论上打个折扣。

过拟合是三条里最难根治的。参数一多,你总能找到一组“历史上完美”的参数。我用的防御手段是三重验证:

  1. 样本内 / 样本外切分:前 70% 时间调参,后 30% 只验证,一次都不许回头改;
  2. 参数敏感性检查:把关键参数在合理区间内扫描一遍,如果只有某一个精确值表现好、邻近值都崩,那基本是过拟合,直接放弃;
  3. 滚动前推验证(Walk-Forward):每 250 个交易日为一个窗口,用前一段优化、后一段验证,滚动推进,看整体表现是否稳定。

第 2 条我用得最多。举个真实例子:某策略的最优参数是 20 日均线配 60 日均线,年化很好看。我把参数扫了一遍,发现 18/58 的年化掉了一半,22/62 直接亏钱。这种“针尖上的最优”我全都不采纳,因为它说明策略根本没有稳健性,只是拟合了噪声。

3.4 绩效指标:别只看年化收益

年化收益是结果,不是全部。我每次回测后必看的指标有这么几组:

指标计算口径我看它来判断什么
年化收益率(1+总收益)^(252/交易日数) - 1策略的整体赚钱能力
最大回撤净值序列的最大峰谷跌幅我最难受的时候会亏多少
夏普比率mean(日超额收益)/std(日超额收益) * sqrt(252)单位风险换来的收益,跨策略可比
卡玛比率年化收益 / 最大回撤收益与回撤的性价比
胜率盈利交易数 / 总交易数心理承受度参考,高胜率不等于赚钱
盈亏比平均盈利 / 平均亏损与胜率配合看期望值
换手率年成交额 / 平均资产成本和容量评估

我用 Python 实现的时候,日收益序列一定要用净值的变化率,而不是“每日盈亏 / 初始本金”。后者在净值增长后会低估收益率波动,算出来的夏普偏高。这个细节我踩过一次,两版算法差距能到 0.3 以上。

def max_drawdown(nav): peak = nav.cummax() dd = nav / peak - 1.0 return dd.min(), dd.idxmin() def sharpe(daily_ret, rf_annual=0.0, periods=252): excess = daily_ret - rf_annual / periods if excess.std() == 0: return 0.0 return excess.mean() / excess.std() * (periods ** 0.5)

还有一组指标经常被忽略但实际很关键:分年度收益、月度收益分布、最长连续亏损天数。一个策略年化 20%、最大回撤 15% 看着不错,但如果它的收益全靠某一年爆发,其余年份在横盘甚至阴跌,那你在实盘里大概率熬不到爆发那一年。我会把分年度收益打出来看,如果三年里只有一年正收益,这个策略我不上。

4. 策略实现:写一个能复用的骨架

策略代码最容易写成一坨面条——所有逻辑塞在一个函数里,加个条件就改一次,改到后来自己都看不懂。我的做法是先抽象出固定接口,让每个策略都像插件一样插进引擎里。

4.1 策略接口设计

class BaseStrategy: def __init__(self, params): self.params = params self.state = {} # 每个标的的中间状态 def prepare(self, history): """回测开始前一次性准备,例如预计算指标""" raise NotImplementedError def on_bar(self, dt, bar, positions): """返回 {symbol: target_weight},权重基于组合总资产""" raise NotImplementedError

关键设计是策略只输出目标权重,不输出买卖多少股。仓位换算、取整、资金检查这些活全部交给执行层。这样策略代码和环境解耦,回测和实盘共用同一份策略,减少“回测代码和实盘代码不一致”这个经典事故源。

我第二版系统就栽在这上面:回测里用的是target_weight,实盘里我图省事直接写了“买 1000 股”,结果两边逻辑分叉,回测年化 15%,实盘跑了三个月,交易记录对不上,查了一周才发现是取整规则不同导致的复利偏差。

4.2 一个可运行的均线动量策略

拿双均线做个示例,重点不在策略本身,而在于它展示了完整的结构:预计算指标、信号生成、仓位约束。

import pandas as pd class MaMomentum(BaseStrategy): def prepare(self, history): # history: dict, symbol -> 全历史 DataFrame(已复权) self.ind = {} for sym, df in history.items(): close = df["close_adj"] short = close.rolling(self.params["fast"]).mean() long_ = close.rolling(self.params["slow"]).mean() self.ind[sym] = pd.DataFrame({"fast": short, "long": long_}) def on_bar(self, dt, bar, positions): target = {} for sym, row in bar.iterrows(): ind = self.ind[sym] if dt not in ind.index: continue fast = ind.at[dt, "fast"] long_ = ind.at[dt, "long"] if pd.isna(fast) or pd.isna(long_): continue if fast > long_ * (1 + self.params["buffer"]): target[sym] = self.params["weight"] elif fast < long_ * (1 - self.params["buffer"]): target[sym] = 0.0 else: # 缓冲带内维持原仓位,减少来回打脸 target[sym] = positions.get(sym, {}).get("weight", 0.0) return target

这里加了个buffer缓冲带,是我实战里最有价值的一个小改动。不加缓冲带的时候,均线在临界点附近来回穿越,策略天天买进卖出,成本把收益吃光。加了 0.5% 的缓冲带之后,交易次数直接降了六成,年化反而提高了。均线类策略的大部分亏损不是方向判错,而是临界点反复横跳。这个经验在任何基于阈值的信号系统里都通用。

4.3 仓位管理与参数寻优的正确姿势

仓位管理我分两层。第一层是单标的上限,比如单个标的不超过总资产的 20%;第二层是总仓位上限,比如总仓位不超过 80%,永远留点现金。这两条约束在执行层强制生效,策略输出什么权重都要被裁剪一遍。

def clamp_weights(target, max_single=0.2, max_total=0.8): target = {k: min(v, max_single) for k, v in target.items()} total = sum(target.values()) if total > max_total: scale = max_total / total target = {k: v * scale for k, v in target.items()} return target

参数寻优这块,我现在的做法是先把参数空间缩小到物理意义明确的几个,再做网格搜索。比如双均线,快线我只看 5/10/20 三个值,慢线只看 40/60/120,最多 9 组组合。那些“最优参数是 17 和 43”的结论我直接扔掉,因为 17 和 43 没有交易逻辑上的意义,大概率是噪声拟合。

搜索完之后我还要看一张表:每个参数组合在样本外区间的收益、回撤、换手。只有当某个组合在样本内和样本外都排在前三分之一,我才认为它值得进入候选。这比单纯挑样本内第一名靠谱得多。

5. 从回测到执行:工程化才是真正的难点

回测跑通只是开始,真正让我睡不着觉的是执行环节。回测里“成交”是一个瞬间完成的函数调用,实盘里它是一条会失败、会延迟、会重复的链路。

5.1 调度、状态与幂等

调度我用最朴素的 cron,每个交易日收盘后固定时间触发,跑数据更新和信号计算,第二天开盘前触发执行。简单可靠,出错能一眼看出来。

状态管理我用 SQLite 存三类表:持仓表、委托表、成交表。每次执行前先读持仓表,而不是靠内存变量。程序崩溃重启是常态,任何只存在内存里的状态都等于不存在。

幂等是我吃过最大亏的地方。有一次脚本异常退出后我手动重跑,结果同一批委托被下了两次,仓位直接翻倍。现在的做法是每个委托带一个确定性 ID:

def make_order_id(strategy, symbol, dt, side, seq=0): return f"{strategy}_{symbol}_{dt:%Y%m%d}_{side}_{seq}"

下单前先查委托表里有没有这个 ID,有就跳过。这个规则看起来笨,但它救过我至少三次。

5.2 对账与异常处理

每天收盘后必须做一次三方对账:本地持仓表 vs 委托成交回推的持仓 vs 账户实际持仓。三者不一致就告警,人工介入。我第一年没做这件事,某次发现持仓差了 300 股,追溯到两周前的一笔部分成交没被记录,导致后续所有仓位计算全错。

异常处理我按“可重试 / 不可重试”分类:

  • 网络超时、接口限流:可重试,指数退避,最多 3 次;
  • 委托被拒(价格越界、数量不符):不可重试,记录原因,跳过并告警;
  • 部分成交:不重试,按实际成交量更新持仓,剩余部分根据策略意图决定是否继续挂单;
  • 账户状态异常、风控拦截:立即停止全部后续委托,只允许平仓操作。
def safe_submit(order, retry=3, backoff=1.5): for i in range(retry): try: return submit(order) except RetryableError as e: time.sleep(backoff ** i) log.warning("retry %s %s", order.order_id, e) except NonRetryableError as e: log.error("reject %s %s", order.order_id, e) alert(e) return None alert(f"submit failed after retries: {order.order_id}") return None

5.3 风控:写在下单前的最后一道闸

我在下单函数的最前面放了一个风控检查,任何委托都必须过这道闸,包括平仓单(平仓单只放宽部分条件)。规则不多,但每条都是硬约束:

规则阈值触发后的动作
单标的持仓占比超过 25%拒绝加仓,允许减仓
总仓位超过 90%拒绝买入,允许卖出
单日累计亏损超过 3%当日停止新开仓
连续失败委托3 次暂停该策略,人工确认
账户可用资金低于预留下限拒绝买入
数据时效行情数据未更新全系统停止下单

最后一条特别重要。行情没更新就下单,等于闭着眼睛开车。我在调度脚本里加了一个前置检查:如果当天数据的最新日期不等于预期交易日,直接终止流程并发告警。

6. 常见问题与排查速查表

这一节是我这两年攒下来的问题清单,基本都是“回测很好、实盘不对”的排查路径。排错的顺序建议从数据到撮合到执行,不要一上来就怀疑策略失效,因为八成的问题都出在工程侧。

6.1 回测与实盘差异的排查路径

先比“同一段时间、同一批信号”,看两边的成交价、成交量、成本是否一致。我做过一个对照脚本,把回测引擎在实盘区间重跑一遍,然后逐笔比对:

现象最可能的原因排查动作
实盘成交价系统性高于回测回测用收盘价成交,实盘用开盘价检查撮合规则,统一为次日开盘
实盘交易次数明显多于回测缓冲带 / 阈值在实盘代码里没生效打印两边信号序列逐日比对
实盘收益低一截但方向一致成本模型低估了滑点或税费用真实成交记录反推滑点
持仓数量对不上部分成交未记录,或取整规则不一致做三方对账,检查取整函数
某标的完全没交易数据缺失 / 停牌判断过于宽松检查该标的数据完整性和volume字段
回测曲线异常平滑未来函数或数据穿越用逐日增量与全量两种算法交叉验证
参数微调后表现断崖过拟合做参数敏感性扫描,看邻域是否稳定

再补充三组我自己反复遇到的细节问题:

第一,除权日附近持仓市值跳变。如果你用真实价格算市值、用复权价格算信号,除权当天会出现“价格突然跌一大截但持仓数量没变”的假亏损。我的处理是:市值估值统一用复权价格,只有在生成委托时才换算成真实价格,并且换算时用当日的复权因子。

第二,最小价格变动单位导致的取整偏差。计算出目标价格是 10.237 元,但市场只能按 0.01 元报价,四舍五入到 10.24 元后,长期累积下来会导致实际仓位持续偏离目标。我的做法是取整后重新计算目标数量,而不是先算金额再算价格,避免二次舍入。

第三,多标的同时下单的顺序问题。先买后卖和先卖后买,在可用资金不足时结果完全不同。我的规则是先执行卖出、再执行买入,且买入按信号强度排序,资金不够就按顺序截断。这个细节在回测里也必须有,否则回测和实盘的资金约束不一致。

6.2 几条用真金白银换来的心得

第一条,先让系统空跑至少一个月。信号照算、委托照生成、就是不真正发出去,然后把“如果发了会怎样”和实际行情逐日比对。这一步能筛掉绝大多数工程 bug,成本为零。

第二条,小资金实盘跑三个月再谈加仓。回测和实盘的差距,很多不是逻辑问题,而是流动性、成交滑点、委托被拒这些“摩擦”。这些摩擦只有真金白银才能暴露出来,而且和资金规模强相关。10 万块跑得通的策略,100 万可能就完全不一样了。

第三条,把日志写到能复盘的程度。我现在每次决策都记下:当时看到的输入数据、计算出的指标值、输出的目标权重、裁剪后的权重、生成的委托、最终的成交。出问题的时候,能一路倒推回去。日志不够详细,排查就是猜谜。

第四条,别在亏损当天改参数。亏损带来的情绪会驱动你做出最糟糕的决定——把参数调成刚好能躲过这次亏损的样子,然后下一次亏得更狠。我的规矩是:任何参数变更都必须写进变更记录,附上样本外验证结果,冷却 24 小时后再执行。这个规矩拦下过我至少五次冲动操作。

第五条,策略失效是常态,不是意外。任何一个策略都有它的适用环境,环境变了它就会失效。我现在的做法是给每个策略设一个“健康度”监控:滚动 60 个交易日的实际表现与回测同期预期的偏离度,偏离超过阈值就自动降仓,而不是等到亏到忍不住才手动关掉。

这套系统搭下来,我最深的体会是:量化交易的胜负手不在策略有多聪明,而在于你的工程实现有多诚实。回测里每一处美化,实盘都会加倍还回来。把撮合假设写保守一点、把成本估高一点、把参数选得笨一点,最后跑出来的结果可能没那么好看,但它是能活下来的。

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

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

立即咨询