ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Python搭建可落地的量化交易系统:数据、回测与实盘执行

用Python搭建可落地的量化交易系统:数据、回测与实盘执行

量化交易这四个字,被短视频讲得太玄了。动不动就是"躺赚""年化翻倍""全自动印钞机",我身边真在这行里做事的,没一个这么说话。真实的量化系统,绝大部分时间都花在数据清洗、撮合假设、回测偏差排查这些枯燥得让人打哈欠的环节上。我前后花了大半年时间,用Python把一套覆盖数据拉取、策略信号、回测引擎、订单执行的流程跑通,代码写了删、删了改,踩过的坑比留下的代码多得多。

这篇东西不聊玄学,只讲一条能落地的路径:行情数据怎么拿、脏数据怎么洗、回测引擎怎么设计才不至于自欺欺人、策略信号怎么翻译成真正的下单动作、实盘跑起来之后哪些异常会咬人。适合两类人看:一是刚接触量化、会点Python但不知道从哪儿下手的读者;二是已经写过几行回测代码、发现结果好得不真实、想搞清楚哪里出问题的人。全文基于常见工程实践做合理推演,代码是骨架级的,参数和阈值你们按自己标的的波动特性去调,不要照抄。

1. 系统整体架构与设计取舍

动手写第一行代码之前,先把模块边界划清楚,这件事比选什么策略重要得多。我最初的版本是一锅粥:拉数据、算指标、画图、判断买卖全塞在一个main.py里,两百多行,跑起来没问题,但只要改一个参数就得从头重跑,想对比两个策略更是灾难。第二次重构才拆成四层:数据层、策略层、回测层、执行层。每层之间只通过约定的数据结构通信,互不越界。

1.1 为什么我建议自己搭一遍而不是直接套框架

市面上成熟的开源回测框架不少,功能完整、社区活跃,直接拿来用能省很多事。但我还是建议至少自己手写一版最小可用的回测引擎,理由很实在:你不知道引擎内部怎么撮合、怎么算手续费、怎么处理停牌,回测曲线再漂亮也只是个数字。自己写一遍,哪怕只有两百行,你会被迫回答几个问题——信号在哪根K线上生效?成交价用开盘价、收盘价还是次日开盘价?手数不够一手怎么办?这些问题答不清楚,后面上实盘一定翻车。

当然,我不是让你造轮子造到底。常见的组合是这样:数据侧用常用的数据处理库(pandas、numpy)做清洗和特征计算,回测和撮合自己写,绘图用matplotlib,实盘对接走券商或期货公司提供的交易接口。框架只在你已经理解每个环节之后,用来提效,而不是用来替代理解。

1.2 四层架构的数据流与职责边界

把职责理清楚,后面所有代码都有地方放。

层级核心职责输入输出常见坑
数据层拉取、清洗、复权、对齐原始行情、交易日历标准化K线表复权方式不统一、停牌未标记
策略层计算指标、生成信号标准K线表信号序列(-1/0/1)未来函数、指标窗口错位
回测层撮合、计费、统计信号序列 + K线净值曲线、交易明细成交价假设过于乐观
执行层下单、撤单、状态同步目标持仓实际持仓、成交回报重复下单、部分成交漏处理

数据流是单向的:数据层产出干净的宽表,策略层只读不写,回测层按时间顺序逐根推进,执行层拿回测层验证过的信号去做实际动作。这条单向链路看着死板,但调试的时候极其省心——出问题从上游往下游一段段查,很快能定位。

1.3 时间维度上的一个关键决定

有个细节值得单独说:整个系统的时间粒度。日线级别回测简单、数据量小,但信号频率低,一年可能就几十次交易机会,统计上很容易被少数几笔交易主导;分钟级回测更贴近真实,但数据量暴涨,一个标的五年分钟线轻松上千万行,pandas处理起来内存吃紧,而且分钟级的撮合假设更难写准。

我的做法是先用日线把策略逻辑和回测框架全部跑通、验证正确,再逐步下沉到分钟级。逻辑没跑通就上高频数据,只会让你在数据问题和逻辑问题之间反复横跳。这个顺序我是踩过坑才明白的——最早直接拿分钟线做,花了两周时间才发现信号里有个未来函数,等于白干。

2. 数据层:从原始行情到可用数据集

数据是量化的地基,这句话听起来像废话,但你真做过就知道,能把数据洗干净的团队,其实已经赢了一半。我见过不少策略想法本身不错,最后毁在复权没对齐、停牌日没剔除、交易日历对不上这些"技术性问题"上。

2.1 数据源选型与拉取策略

数据源大致分三类:免费的公开接口、付费的商业数据库、券商或期货公司提供的行情服务。免费接口上手快,缺点是字段少、历史深度有限、偶尔限频;付费数据质量高,但成本不低;券商端的优势是实时性好,适合执行环节。

选型的时候我关注三件事:字段是否够用(开高低收、成交量、成交额、复权因子、停牌标记)、历史深度(能回溯多少年)、更新频率和稳定性。免费源适合做策略原型,一旦策略逻辑定型、准备做严格验证,就该换更稳定的源。这里有个经验:不要混用多个源的数据,A源算复权和B源算收益率,差异可能比策略本身的alpha还大。

拉取代码大致长这样,重点是批量、缓存、幂等:

import pandas as pd from datetime import datetime def fetch_daily(symbols, start, end, cache_dir="./cache"): """按标的批量拉取日线,带本地缓存避免重复请求""" frames = [] for sym in symbols: cache_path = f"{cache_dir}/{sym}_{start}_{end}.parquet" try: df = pd.read_parquet(cache_path) except FileNotFoundError: # 实际调用你选定的数据接口,这里用伪函数占位 df = data_api.get_daily(symbol=sym, start=start, end=end) df = normalize_columns(df) # 字段名统一 df.to_parquet(cache_path, index=False) frames.append(df.assign(symbol=sym)) return pd.concat(frames, ignore_index=True) def normalize_columns(df): rename_map = { "trade_date": "date", "open": "open", "high": "high", "low": "low", "close": "close", "vol": "volume", "amount": "amount", } df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns}) df["date"] = pd.to_datetime(df["date"]) return df.sort_values("date").reset_index(drop=True)

缓存这一步别省。量化开发阶段会反复重跑同一段历史,每次都去请求接口,轻则浪费时间,重则被限频。用本地文件缓存,配合幂等的字段标准化,后面换数据源的时候改动量也小。

2.2 数据清洗的四条硬性规则

清洗这一步没有标准答案,但有几条我自己立下的规矩,建议直接抄:

  • 字段类型全部显式转换。价格用float64,成交量用整数或float64,日期用datetime64,一次转不干净,后面聚合比较会各种报错。
  • 缺失值不轻易填补。行情数据缺失通常意味着停牌、休市或源端问题,用前值填充(forward fill)是最常见的做法,但必须同时记录填充标记,回测时据此决定能不能成交。直接把NaN填成 0,等于凭空造出一根跌停线。
  • 异常值用规则筛,不用均值砍。某天涨幅超过正常涨跌停幅度的,通常是复权或拆股导致的,先查原因再处理,别一刀切当成脏数据删掉。
  • 交易日历独立维护。不要用行情数据里出现过的日期反推交易日,标的停牌那几天就消失了,日历会对不上。单独拉一份交易日历,用它的日期序列来做时间轴。

关于最后一条,我吃过亏。早期我用某标的的行情日期当索引,后来换成一个长期停牌的标的,日期序列缺了一大块,策略里的滚动窗口算出来的指标完全是错的,但代码不报错,回测结果看起来还挺正常。这种错误最致命。

2.3 复权、停牌与对齐:最容易翻车的地方

复权这块,核心区别就一个:前复权让最新价格不变、历史价格被调整;后复权让最早价格不变、后续价格被调整。回测必须用后复权或等比前复权,绝对不能用"除权当天价格跳空"的原始数据直接算收益率,否则分红送转那天你的策略会莫名其妙赚一大笔,纯属幻觉。

判断方法很简单:拿一只分红较多的标的,用原始收盘价算一次日收益率,看最大值和最小值是不是远超合理区间;再用后复权价算一遍,两者对比,差异在哪里一目了然。

停牌处理是另一个高频坑。停牌期间没有成交,意味着你既不能买也不能卖。回测引擎如果不知道这件事,会在停牌日正常撮合,结果就是回测能成交、实盘干瞪眼。解决办法是在数据表里加一列tradable布尔标记,撮合前先查这个标记,False直接跳过。

多标的对齐则是第三个坑。不同标的停牌日期不同,直接concat会产生大量NaN行。我的处理方式是:先按交易日历建立统一索引,把每个标的的数据reindex上去,缺失的位置标记为不可交易,而不是简单填充。这样在组合层面算净值时,停牌标的市值冻结,资金不会凭空变化。

3. 策略层:把想法翻译成可计算的信号

策略层是很多人最兴奋、也最容易自嗨的地方。我的观点可能有点扫兴:策略本身的技术含量,在整个工程里占比不到三成,剩下七成是数据、撮合、风控和执行。但策略层有一个不可替代的作用——它是你验证自己"到底有没有edge"的唯一入口,所以写的时候要格外克制。

3.1 策略接口的统一设计

我最重要的一条经验:所有策略必须实现同一个接口。不管内部是均线、动量、波动率还是别的什么,对外只暴露一个方法,输入标准K线表,输出信号序列。这样做的好处是回测引擎只认接口不认实现,换策略不用改引擎,批量对比也方便。

from abc import ABC, abstractmethod import pandas as pd import numpy as np class BaseStrategy(ABC): @abstractmethod def generate_signals(self, bars: pd.DataFrame) -> pd.Series: """ bars: 索引为交易日, 至少含 open/high/low/close/volume 列 return: 与 bars 等长的 Series, 取值 -1(空) / 0(平) / 1(多) """ raise NotImplementedError class DualMovingAverage(BaseStrategy): def __init__(self, fast=10, slow=40): assert fast < slow, "快线窗口必须小于慢线窗口" self.fast, self.slow = fast, slow def generate_signals(self, bars: pd.DataFrame) -> pd.Series: close = bars["close"] ma_fast = close.rolling(self.fast, min_periods=self.fast).mean() ma_slow = close.rolling(self.slow, min_periods=self.slow).mean() raw = np.where(ma_fast > ma_slow, 1, -1) sig = pd.Series(raw, index=bars.index) # 窗口未填满前不下判断, 置为 0 避免误触发 sig[ma_slow.isna()] = 0 return sig

注意最后那行sig[ma_slow.isna()] = 0。不加这句,NaN参与比较会得到False,被映射成 -1,等于在数据不足的前几十根K线上强行开空仓。这种隐蔽错误在回测里可能反而贡献了正收益,然后上实盘发现完全复现不了。

3.2 信号与持仓的映射关系要写清楚

新手最容易混淆的一点:信号不等于持仓。信号是"我希望处于什么状态",持仓是"我实际持有什么"。中间还隔着下单、成交、费用、最小交易单位这一道工序。

我习惯用"目标仓位"的表述:策略输出1表示目标满仓多头,-1表示目标满仓空头,0表示空仓。回测层拿目标仓位和当前持仓对比,差值就是需要交易的方向和数量。这样写的好处是,无论标的数量怎么变、资金怎么调,逻辑都不用改。

当前持仓目标仓位动作说明
01买入开仓全额建仓
11持有不动无交易,不产生费用
10卖出平仓清空持仓
1-1卖出平仓 + 反手开空两步操作,费用翻倍
-11买入平仓 + 反手开多同上

反手这一行要特别留意。很多回测代码把1 → -1当成一次交易,手续费只扣一次,实际上这是两笔独立成交,费用和滑点都得算两遍。高频反手的策略,光是这个细节就能让回测收益腰斩。

3.3 参数选择与过拟合的自救

参数是策略的灵魂,也是过拟合的温床。双均线就是最典型的例子:快线10、慢线40,换一个标的、换一段区间,最优参数可能变成5和20,也可能变成20和60。你要问哪个"对",答案是:在样本外还站得住的才对。

我的做法固定成三步:训练段选参、验证段看效果、测试段做最终判断,三段严格按时间切分,不能重叠。比如五年数据,前三年选参数,第四年验证,第五年只跑一次看结果。选参阶段可以扫参数网格,但只用验证段的结论做决策,测试段的结果绝对不能反哺回去调参,否则那就不叫测试了。

def walk_forward_split(index, train_years=3, valid_months=12): """按时间切分, 返回 (训练段, 验证段) 的索引区间列表""" splits = [] start = index.min() while True: train_end = start + pd.DateOffset(years=train_years) valid_end = train_end + pd.DateOffset(months=valid_months) if valid_end > index.max(): break splits.append(( index[(index >= start) & (index < train_end)], index[(index >= train_end) & (index < valid_end)], )) start = start + pd.DateOffset(months=valid_months) return splits

滚动切分能变相增加样本量,也能看出策略在不同市场环境下的稳定性。如果一个策略在某个参数下特别猛、换区间就崩,那基本可以判死刑了。

4. 回测引擎:那些年让我们亏钱的细节

到这一步,策略想法已经有了,接下来是让它接受审判。回测引擎的价值不在于跑出一条好看的曲线,而在于尽可能诚实地还原真实交易的摩擦成本。判断一个回测引擎写得好不好,有个简单标准:同一套信号,跑出来的收益是不是"没那么好看但更可信"。

4.1 撮合逻辑:信号在哪根K线生效

这是整个引擎里最核心、也最容易被糊弄过去的设定。假设你的信号是用收盘价算的,那么这根K线收盘时你才知道信号,最早也只能在下一根K线开盘时成交。如果你让信号在当根K线收盘价成交,就构成了未来函数。

几种常见的成交价假设,从乐观到保守排一下:

成交价假设适用场景乐观程度备注
当根收盘价收盘前几分钟下单高最容易引入未来函数
次根开盘价收盘后下单、次日开盘成交中我默认采用这个
次根均价(VWAP近似)大资金分批中低需要分钟数据才能算准
次根开盘价 + 固定滑点流动性一般的标的低更贴近真实成交

我自己固定用"次根开盘价 + 滑点"这一套。实现上就是在信号序列上整体shift(1),然后和次日的实际开盘价做撮合。注意别 shift 两次也别漏 shift,这个细节我调试了大半天。

def backtest_with_next_open(bars, signals, fee_rate=0.0003, slippage=0.0005): """ 简化版逐日推进撮合: - 信号在 T 日收盘生成, T+1 日开盘成交 - 成交价 = 开盘价 * (1 ± slippage) - 按目标仓位与当前仓位差额交易 """ position = 0.0 # 当前持仓(以份数计) cash = 1_000_000.0 # 初始资金 equity_curve, trades = [], [] target = signals.shift(1).fillna(0) # 关键: 信号延迟一根 pending_target = 0 for dt, row in bars.iterrows(): # 第一步: 在开盘按昨日信号调仓 desired = pending_target if desired != position and row["tradable"]: delta = desired * (cash + position * row["open"]) / row["open"] - position price = row["open"] * (1 + slippage if delta > 0 else 1 - slippage) cost = abs(delta) * price * fee_rate if abs(delta) * price + cost <= cash: cash -= delta * price + cost position += delta trades.append({"date": dt, "price": price, "qty": delta, "fee": cost}) # 第二步: 记录当日净值, 并准备次日目标 equity = cash + position * row["close"] equity_curve.append({"date": dt, "equity": equity}) pending_target = target.loc[dt] return pd.DataFrame(equity_curve).set_index("date"), pd.DataFrame(trades)

这段代码是骨架,真实场景要处理保证金、做空约束、最小交易单位取整、资金不足时的降级方案。但它把最关键的一件事表达清楚了:信号和成交之间隔着一个时间差。

4.2 手续费、滑点、冲击成本,一个都不能少

手续费好算,按成交金额乘费率,双边收取。滑点也好处理,在成交价上加一个百分比偏移。真正难估的是冲击成本——你的订单本身会推动价格,单子越大推得越狠。

对个人投资者来说,冲击成本在中小资金量下通常可以忽略,但有两种情况必须认真对待:一是标的流动性差(日均成交额只有几千万),二是策略换手率极高。我做过一个粗略估算,日均成交额五千万的标的,下单金额一百万的冲击成本大约在万分之三到万分之五之间,比手续费还高。

一个务实的处理办法是:滑点不完全按固定值,而是按"下单金额 / 日均成交额"的动态比例来算,设个上限。

def dynamic_slippage(order_amount, avg_turnover, base=0.0003, cap=0.002): """冲击成本随下单占比线性增长, 并设上限防止过度悲观""" ratio = order_amount / max(avg_turnover, 1.0) return min(base + ratio * 0.1, cap)

这个公式当然不精确,但它能把你从"换手率越高收益越高"的幻觉里拉出来。很多策略一加上真实换手成本,超额收益就蒸发了,早知道比晚知道好。

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

回测跑完,新手第一眼看年化,老手第一眼看回撤和夏普。原因很现实:年化是可以用杠杆和集中押注堆出来的,回撤才反映你扛不扛得住。下面几个指标,我建议每次回测都输出,缺一个都不完整。

指标计算口径关注点参考区间
年化收益率净值年化绝对收益水平视基准而定
最大回撤净值峰谷最大跌幅心理承受极限日线策略一般控制在20%以内
夏普比率超额收益 / 收益波动单位风险回报1以上算合格
卡玛比率年化收益 / 最大回撤收益回撤比1以上算不错
胜率盈利交易数占比交易质量不是越高越好
盈亏比平均盈利 / 平均亏损与胜率配合看与胜率乘积 > 1 才有正期望

这里必须提醒一句:胜率和盈亏比要一起看。胜率八成的策略可能因为两笔大亏直接归零,胜率三成的策略也可能靠高盈亏比活得很滋润。单看任何一个都是耍流氓。

import numpy as np def perf_stats(equity: pd.Series) -> dict: ret = equity.pct_change().dropna() annual = (equity.iloc[-1] / equity.iloc[0]) ** (252 / len(ret)) - 1 drawdown = (equity / equity.cummax() - 1).min() sharpe = ret.mean() / ret.std() * np.sqrt(252) if ret.std() > 0 else np.nan return { "年化收益率": round(annual, 4), "最大回撤": round(drawdown, 4), "夏普比率": round(sharpe, 2), "卡玛比率": round(annual / abs(drawdown), 2) if drawdown else np.nan, }

5. 执行层:从回测到实盘的落地

回测漂亮只是入场券,能不能在真实环境里稳定跑起来,取决于执行层。这一层最无聊、最不性感,但它决定了你的系统是玩具还是工具。我起初以为执行层就是"调接口下单",后来发现下单只是其中最简单的一步,麻烦全在状态管理上。

5.1 交易接口对接的落地方式

对接交易接口之前,先把三件事定下来:下单频率上限、单笔最大委托量、撤单规则。这三个约束来自交易端,不是策略端,策略再激进也得服从。

封装一层我称为"执行器"的模块,把接口细节挡在外面,上层只调用submit_target(target_position)。这样换接口的时候只改执行器,策略和风控代码一行不动。

class OrderExecutor: def __init__(self, client, max_retry=3): self.client = client self.max_retry = max_retry def submit_target(self, symbol, target_qty, price_hint=None): """把目标持仓转换为委托, 返回委托结果""" current = self.query_position(symbol) delta = target_qty - current if abs(delta) < self.min_lot(symbol): return {"status": "skipped", "reason": "不足最小交易单位"} side = "buy" if delta > 0 else "sell" for attempt in range(self.max_retry): try: return self.client.place_order( symbol=symbol, side=side, qty=abs(delta), price_hint=price_hint ) except (TimeoutError, ConnectionError) as e: if attempt == self.max_retry - 1: return {"status": "failed", "error": str(e)} self.sleep_backoff(attempt)

重试逻辑很重要,但重试不能盲目。下单请求超时,最忌讳直接重发——第一次可能已经成交了,重发就是双倍仓位。正确做法是超时后先查委托状态和持仓,确认没成交再重试,这段逻辑我写在place_order的外层,不在接口层。

5.2 状态同步:实盘和数据库对不上是常态

跑实盘的头一个月,我每天最焦虑的事情是:本地记录的持仓和真实持仓对不上。原因五花八门——部分成交、撤单延迟、手工干预、接口返回字段解析错误。后来我定了一条铁律:每天开盘前和收盘后各做一次全量对账,以交易端返回的持仓为准,本地状态全部覆盖。

对账逻辑不复杂,但必须自动化,靠人眼比对迟早出事。

def reconcile(local_positions: dict, broker_positions: dict) -> dict: """以交易端数据为准, 输出差异项用于日志与告警""" diffs = {} symbols = set(local_positions) | set(broker_positions) for sym in symbols: local = local_positions.get(sym, 0) broker = broker_positions.get(sym, 0) if local != broker: diffs[sym] = {"local": local, "broker": broker} return diffs

差异不为空时,不要自动"修正"然后继续跑,先告警、人工确认。因为差异本身可能是某种你没意识到的业务逻辑(比如冻结、待交收),自动覆盖可能掩盖更大的问题。

5.3 风控:写在代码里的纪律

风控不是给领导看的,是给未来的自己踩刹车用的。策略在回测里再稳,实盘遇到极端行情照样可能失控,这时候唯一能救你的是硬编码的规则,而不是当时的判断力。

我固定配了这几道闸:

风控项阈值设定触发动作设计理由
单日最大亏损账户净值的2%停止开新仓防止单日情绪化加码
单标的最大仓位账户净值的20%拒绝新开仓避免单一标的黑天鹅
连续亏损笔数5笔降低仓位至50%策略可能阶段性失效
单笔委托金额日均成交额1%自动拆单控制冲击成本
系统异常接口连续失败3次全部撤单并冻结防止异常状态扩大

配置项全部放在外部文件里,别写死在代码里。原因是止损阈值这种东西,行情环境变了要调,写死在代码里就得改代码、测试、重启,容易在犹豫中错过时机。

6. 常见问题排查与避坑记录

写了这么多,最后这部分才是真正的干货浓缩。下面这些问题是我在实际跑的过程中真实遇到过的,按出现频率从高到低排。

6.1 高频问题速查表

现象可能原因排查思路处理方式
回测收益好得离谱未来函数、用当根收盘价成交信号整体 shift 一根再看强制延迟成交,重跑对比
实盘成交价远差于回测滑点低估、下单时机不符对比实际成交价与假设价提高滑点,改成交时点
净值曲线某天跳变复权方式错误、拆分未处理检查该日是否有除权事件统一用后复权数据
部分标的长期不交易停牌标记缺失检查 tradable 标记停牌日禁止撮合
持仓与本地记录不一致部分成交、撤单未回调逐笔核对委托明细定时全量对账
指标前几十根异常滚动窗口未满参与计算检查 min_periods 设置未满窗口信号置0
回测内存溢出分钟数据全量载入用分块或按标的加载分批回测后合并结果

6.2 三个亲眼见过的坑

第一个坑叫幸存者偏差。早期我做多标的回测,直接用当前还在交易的标的列表去跑历史,结果那些已经被摘牌、退市的标的根本没进样本。回测收益漂亮得不像话,因为活下来的都是赢家。修正办法是使用历史成分股名单,或者至少把退市标的的历史数据也纳入,退市前按最后价格强制平仓。这一改,年化直接掉了一大截,但那才是真实的。

第二个坑叫参数偷看。有段时间我习惯回测跑完觉得不满意,就顺手调个参数再跑一遍,来回十几次之后找到了"黄金参数"。这套参数在历史上完美,实盘一上就原形毕露。问题不在于调参本身,而在于用了同一段数据反复调。后来我强制自己:任何参数的最终确认,必须在一个从未参与过决策的时间段上验证一次,通过了才能上线。

第三个坑是回测和实盘的交易日历不一致。回测里用的是连续自然日构造的索引,实盘按交易所日历走,节假日前后信号错位,导致回测买了、实盘没买。听起来不可思议,但就是发生过,因为我早期图省事用pd.date_range生成了日期序列。现在统一从交易日历表读取,从源头上堵死。

6.3 几条用血换来的实操心得

日志比调试器有用。实盘跑起来之后你没法断点调试,唯一的信息来源就是日志。每笔委托、每次回报、每次对账结果、每个异常,全部落盘,字段包含时间戳、标的、动作、数量、价格、状态。日志不怕多,就怕缺关键字段。我现在的习惯是,任何可能出问题的分支,第一行代码先写日志。

先用小资金跑通全流程。再完美的回测,也不如拿真金白银小仓位跑一个月。真实环境里的网络抖动、接口限频、部分成交、结算延迟,这些在回测里永远模拟不出来。小资金跑通、跑稳、跑出和回测接近的结果,再考虑放大。

回测框架要定期"反向验证"。拿一个故意亏钱的策略(比如随机信号加高换手)喂进去,如果回测结果显示稳定盈利,说明引擎有问题;如果显示稳定亏损且亏损幅度接近手续费成本,说明撮合和计费逻辑基本靠谱。这个方法我每重构一次引擎就跑一遍,比看代码靠谱。

策略和执行的代码要分仓库。策略是研究性质的,改得频繁;执行是生产性质的,改动要谨慎。混在一起,某天调试策略顺手改了个参数,可能就把正在跑的实盘搞崩了。这个教训来得有点贵。

接受不完美。量化系统永远有改进空间,数据永远有脏的地方,回测永远有没考虑到的细节。追求一个"绝对正确"的系统,最后往往一事无成。我的做法是先跑起来,用真实反馈驱动迭代,而不是在开发阶段无限打磨。跑起来的系统才有资格谈优化,躺在本地文件夹里的完美代码一文不值。

这套系统我前后重构了三轮,现在跑的是第三版,代码量不大,但每一行我都知道为什么这么写。如果你正准备动手,我的建议是:先把数据层和回测层做扎实,策略反而是最后才需要考虑的事情。地基不牢,上面盖什么都是空中楼阁。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进