资讯动态

用Python搭建自己的交易系统:从规则矩阵到回测实盘

发布时间:2026/9/19 13:04:20 来源:尧图企业网站定制
简介一份系统阐述股票交易系统构建方法的完整版电子文档面向希望摆脱随意买卖、建立稳定盈利模式的投资者。内容围绕交易系统的定义、自我控制、亏损处理三大核心展开明确提出成功交易的关键是找到一套适合自己个性、非机械的交易系统并讲解了市场分析、风险控制、交易思想、整体操作方案等多个维度。文档结合实际案例详细说明了如何处理预测错误、设定止损、控制亏损、耐心等待机会以及制定和修正交易计划等方法帮助读者完善交易纪律弥补仅靠预测或单一指标获利的不足。无论使用波浪理论还是其他技术分析投资者都可借此反思自身交易流程构建完整可执行的决策框架。该文档为单份PDF文件大小约130KB内容完整规整目前已有620人学习下载。1. 建立自己的交易系统先想清楚完整到底指什么做交易三五年的人大多经历过同一个场景看对了方向却没拿住或者拿住了又不知道该什么时候走最后赚一点就跑、亏了反而死扛。问题通常不在分析水平而在每一次决策用的理由都不一样。所谓自己的交易系统就是把分析、决策、执行、复盘四个环节全部变成看得见、能复现、可改进的规则而不是一套准星时灵时不灵的指标。下面按规则定框架、代码落实现、回测算验证、日志做闭环的顺序展开落脚到可以直接抄走的最小实现规则矩阵怎么写信号和仓位怎么用 Python 算回测参数怎么设置才不被最优值骗实盘前要过哪些检查。适合两类人一是已经做过一段交易、想把自己的经验固化成统一规则的人二是有编程基础、想用代码把交易流程标准化、减少盘中冲动决策的开发者。完整的关键不是复杂而是把最容易被跳过的环节全部补齐。2. 交易系统骨架怎么搭规则矩阵覆盖入场、出场、仓位与风控2.1 交易系统不是选股公式四个模块缺一不可很多人把交易系统等同于入场信号。金叉买入、死叉卖出再配一个成功率就觉得系统完成了。真正跑起来会发现同样的信号仓位不一样亏损速度完全不一样止损位置不一样同样一波行情的最终结果也完全不同。入场只是起点一个完整的交易系统至少要覆盖四个模块入场信号在什么条件下开仓条件必须能回答 yes/no。出场规则什么条件下止盈、止损或者在一段时间内没达到预期也离场。仓位管理单次投入多少依据是风险金额而不是感觉。风控与流程连续亏损、单日大亏、账户回撤到一定程度时怎么办开仓动作本身有没有防呆。这四个模块之间有先后依赖。先有入场位置才能算出止损距离先有止损距离才能用风险百分比倒推仓位。所以搭建顺序一般是先定入场和止损再定仓位最后补风控。反过来先把资金管理和风险上限定死再去找不违反约束的信号也是一种常见做法而且更适合风险偏好低的人。提示判断一套思路配不配叫交易系统最快的方法是问四个问题什么情况买、什么情况卖、买多少、连续亏五次怎么办。任何一个答不上来就先别急着写代码。2.2 规则矩阵写法把感觉翻译成可执行条件并量化规则矩阵的用处是把模糊想法变成可以逐行验证的表格。列分别是模块、要回答的问题、模糊说法、可执行规则和参数。下面是一个可以直接套格式的例子模块要回答的问题模糊说法可执行规则示例参数入场何时开仓感觉跌不下去了收盘价站上20日均线且今日真实波幅小于前5日均值的1.5倍均线周期、波动倍数出场何时离场涨多了就跑收盘价跌破10日均线离场或从最高点回撤超过2倍ATR离场均线周期、ATR倍数仓位买多少看着买单笔风险为总资金的1%数量风险金额/(入场价-止损价)风险比例风控最坏情况怎么处理别亏太多当日亏损达2%停止开新仓账户回撤超过10%仓位减半单日上限、回撤上限写规则时有三条硬标准。第一是客观性任何一条规则都不能依赖现场判断感觉差不多再看看要全部删掉。第二是可复现性同一份行情数据喂进去两个人算出来的信号必须完全一样。第三是参数与逻辑分离逻辑是均线多头排列才开仓参数是用20日还是30日均线两者分开后面调参才不会把逻辑也一起改坏。实际整理时建议把过去三个月的交易记录翻出来每笔交易标上当时的买入理由。归类后你会发现多数理由可以归并成两三类把每一类写成一条候选规则放进矩阵这就是后面回测的起点。没必要一开始追求规则覆盖所有行情先覆盖你最有把握、做得最多的那一类。为了让矩阵能在回测和实盘之间共用我会把它直接写成结构化数据# 规则矩阵的代码化存法逻辑描述和参数分开维护 rules { entry: { logic: ma_fast ma_slow 且 ATR 未异常放大, params: {fast: 10, slow: 30, atr_n: 14, filter_k: 1.5}, description: 趋势启动且波动收敛时开仓, }, exit: { logic: close ma_slow 或 回撤超过 2 倍 ATR, params: {atr_n: 14, atr_stop: 2.0}, description: 趋势破坏或回撤失控时离场, }, position: { logic: 风险倒推, params: {risk_pct: 0.01}, description: 单笔风险固定为总资金 1%, }, }这样一份字典就是规则的单一数据源回测脚本和实盘巡检脚本都从它读取参数。逻辑说明放在logic字段里与参数分离改参数时不会误改逻辑出问题时也能直接追溯到写这条规则时的原始意图。2.3 从经验出发还是从数据出发两条路线怎么选构建规则有两条常见路线。一条是从经验出发把自己做过的交易总结成规则优点是规则自带逻辑、容易理解缺点是样本太少而且大脑会选择性记住赚钱的交易把运气当成能力。另一条是从数据出发在历史行情里搜索规律优点是样本量大缺点是很容易挖出一堆只是运气好的规则也就是统计上说的过拟合。比较稳妥的做法是两条路结合先凭经验写下候选规则并把每条规则背后的逻辑用一句话写清楚比如趋势行情中回调有限所以波动收敛后突破更可靠然后用历史数据检验只保留那些在样本内有效、且参数小幅变动后仍然有效的规则。纯粹从数据里冒出来的规则如果说不清逻辑即使回测收益再高也建议先放一边。它大概率只是在拟合历史噪音而不是在描述市场规律。这一章的产出物是一张规则矩阵表和每条规则的逻辑说明。有了它下一步就是把这个矩阵翻译成代码。3. 用 Python 把交易系统跑起来信号生成与仓位计算的最小实现3.1 数据处理先避坑复权、未来函数与时间对齐写代码之前数据处理有三个坑必须先处理否则后面所有回测结论都不可信。第一个是复权。股票除权除息后价格会跳空如果直接拿不复权价算均线一次分红就会产生一个假的下跌信号而用前复权价历史价格会被持续修正不同时间下载的同一条 K 线数据对不上。常见做法是信号计算用后复权价保证历史连续性实际下单用不复权价因为那是真实成交价。两者分开记录不要混用。第二个是未来函数。最常见的代码错误是用当日收盘价计算出信号同时用当日收盘价成交。真实情况是收盘后才能算出信号最早只能次日开盘成交。所以代码里必须把信号和成交错开一天。第三个是时间对齐。K 线周期、指标周期、资金账户的日期索引要统一。跨日回测时停牌日期要保留空行而不是直接删掉否则信号会被错误地顺延到下一个交易日。下面是一个最小的数据准备函数import pandas as pd def load_kline(path: str) - pd.DataFrame: 读取K线统一列名和索引按时间升序排列 df pd.read_csv(path, parse_dates[date]) df df[[date, open, high, low, close, volume]] df df.set_index(date).sort_index() # close 是不复权成交价close_adj 用于指标计算 if adj_close in df.columns: df[close_adj] df[adj_close] else: df[close_adj] df[close] return df这段代码把原始数据整理成统一结构。parse_dates把日期列解析成时间类型set_index().sort_index()保证索引有序后面 rolling 等窗口计算才不会错位。close_adj只用于指标计算close只用于成交价格从源头避免复权价混用。如果数据源没有adj_close字段直接退化为用不复权价但此时要清楚除权跳空会影响信号准确性。3.2 写一个可调试的信号模块双均线加 ATR 波动过滤把规则矩阵里的入场规则翻译成代码常见做法是向量化计算一次性生成整段信号序列而不是逐行循环。逐行循环容易写但速度慢而且状态变量容易串。下面是一个最小信号模块import numpy as np def generate_signals(df: pd.DataFrame, fast: int 10, slow: int 30, atr_n: int 14, filter_k: float 1.5) - pd.DataFrame: df df.copy() close df[close_adj] # 双均线快线上穿慢线视为趋势启动 df[ma_fast] close.rolling(fast).mean() df[ma_slow] close.rolling(slow).mean() df[trend] np.where(df[ma_fast] df[ma_slow], 1, -1) # ATR用真实波幅衡量当前波动水平 prev_close close.shift(1) tr np.maximum(df[high] - df[low], np.maximum((df[high] - prev_close).abs(), (df[low] - prev_close).abs())) df[atr] tr.rolling(atr_n).mean() # 信号趋势由空翻多的当天且波动没有突然放大 cross_up (df[trend] 1) (df[trend].shift(1) 0) df[signal] np.where(cross_up (df[atr] df[atr].shift(1) * filter_k), 1, 0) return df逻辑说明ma_fast与ma_slow分别计算快慢均线trend把市场状态压成 1 或 -1。tr是三段求最大值的真实波幅atr是它在atr_n周期内的均值。signal只在空翻多当天为 1并且要求当日 ATR 不超过昨天 ATR 的filter_k倍避免在波动突然放大、行情进入剧烈震荡时追进去。参数含义如下参数含义取值方向fast / slow快慢均线周期差值越大信号越少、持仓越久atr_nATR 计算周期常用 14数据频率不变时无需频繁改filter_k波动过滤倍数越小过滤越严信号越少这段代码的调试要点是逐列检查先单独看trend的序列对不对再看signal有没有出现连续两个 1。如果同一段行情里信号扎堆通常是trend在 1 和 -1 之间反复横跳这时应该加一个最小持仓周期而不是调均线参数硬压。3.3 仓位计算用风险倒推ATR 定止损出场价定了再算手数入场信号只是告诉你方向买多少由风险和止损距离决定。思路是先定每笔交易愿意亏总资金的多少比如 1%再定止损位比如入场价下方 2 倍 ATR然后用公式仓位 风险金额 / (入场价 - 止损价) 倒推手数。波动越大的标的自动买得越少这是风险倒推法相对于固定金额买最大的优势。def calc_position(account: float, risk_pct: float, entry: float, stop: float, price_step: int 100) - int: 按风险金额倒推买入数量并向下对齐到最小成交单位 risk_cash account * risk_pct # 单笔最大亏损金额 loss_per_unit abs(entry - stop) # 每单位承受的亏损 if loss_per_unit 1e-9: return 0 units int(risk_cash / loss_per_unit) # 理论股数 units units // price_step * price_step # 对齐到整手 return units参数说明account是当前总资金risk_pct是单笔风险比例常见取值在 0.5% 到 2% 之间波动大、胜率低的系统取下限。entry是计划入场价stop是止损价常用入场价减去 2 倍 ATR这类规则计算。price_step是最小成交单位A 股是 100 股。函数返回的units是最终手数。这里有一个容易被忽略的校验倒推出来的仓位不能超过单标的上限和账户可用资金的约束。实际使用时要在返回前加两道判断一道是市值不超过账户资金的一定比例另一道是数量不超过持仓上限。否则资金全押在单一标的上仓位管理的意义就没了。最后提一句凯利公式它给出的最优仓位比例依赖胜率和盈亏比两个估计值估计一偏就会过度下注。如果一定要用取计算值的一半甚至四分之一并设置绝对上限。多数人的问题不是仓位算得不够精细而是没有纪律执行算出来的结果。4. 回测参数怎么设不踩坑手续费、滑点与参数稳健性检验4.1 回测引擎四个必设参数手续费、滑点、停牌与最小成交单位规则和仓位模块写完下一步是回测。回测结果可不可信很大程度取决于四个参数有没有按真实市场设置。回测参数参考取值常见陷阱手续费双边佣金万2.5到万3卖出加印花税和过户费只算佣金漏掉卖出印花税高频策略收益被高估滑点至少按 ATR 的 0.1 倍或固定价差计入成交价按信号价零滑点成交小资金可能没事大资金完全对不上停牌停牌日不产生成交信号顺延到复牌日直接跳过停牌日会让信号错误提前成交最小成交单位A股为100股整数倍资金小时尾差反复累积回测收益和实盘差距明显设置逻辑要放在成交函数里统一处理每次撮合时成交价等于信号价加上滑点成交数量向下取整到最小单位手续费由费率累加。滑点既可以是固定值也可以是 ATR 比例的动态值我一般两种都测一遍取结果更差的版本作为净值曲线的下沿。回测里对参数仁慈实盘里市场会加倍还回来。4.2 参数孤岛与参数平原用参数敏感性网格判断是否过拟合回测出一个高收益参数组合不算本事难点在于搞清楚这个最优值是真的规律还是恰好踩中的噪音。判断方法是在最优参数周围做网格扫描观察参数变化时收益和回撤怎么变。def grid_sensitivity(raw_df, param_grid): rows [] for fast in param_grid[fast]: for slow in param_grid[slow]: df generate_signals(raw_df, fastfast, slowslow) stat run_backtest(df, fee0.0008, slippage0.001) rows.append({fast: fast, slow: slow, return: stat[return], drawdown: stat[max_drawdown]}) res pd.DataFrame(rows) return res.sort_values(return, ascendingFalse)这段代码对fast和slow做二维扫描run_backtest是前面回测参数的统一封装。运行后把结果按收益排序重点看最优参数周围的格子。如果收益普遍接近形成一片高原说明参数取 10 还是 12 差别不大系统是稳健的。如果只有单独一个格子收益突出四周立刻掉下去这就是参数孤岛。实盘里参数会漂移孤岛上的参数一旦偏离就失效。判断准则就一条平原上的参数可以放心用孤岛上的参数需要回到逻辑层重新审视而不是继续在孤岛附近加密扫描。网格加密永远能找到更尖的孤岛但那只说明你在拟合噪音拟合得更精细了。4.3 样本外与随机基准给交易系统的信息量做显著性检验参数敏感性过了关还不能直接上实盘还要回答一个问题这套信号比随机开仓强吗两类检验常用。第一类是时间切分。按时间顺序把数据切成前八后二前段用来调参后段完全不动最后把优化好的参数在后段跑一遍。注意只能按时间切不能用随机抽样切因为行情序列本身有自相关性。第二类是随机基准检验。把信号序列随机打乱后重算收益重复几千次得到随机信号收益的分布再看真实信号的收益落在分布的什么位置。落在尾部说明信号确实携带信息落在中间说明信号和掷硬币没有本质区别。rng np.random.default_rng(42) real run_backtest(df, **params)[return] nulls [] for _ in range(2000): shuffled df.copy() shuffled[signal] rng.permutation(shuffled[signal].fillna(0).values) nulls.append(run_backtest(shuffled, **params)[return]) nulls np.array(nulls) p_value (np.sum(nulls real) 1) / (len(nulls) 1) print(p_value , p_value)rng.permutation构造的是拿到同样多的信号次数但顺序随机的对照组p_value表示随机信号达到真实收益的概率。如果这个值大于 0.05说明策略没有统计上的信息量这时候先别急着加过滤器或换参数回到规则矩阵重新修正假设。注意这个检验只打乱顺序不改变信号数量所以它衡量的是信号的时间选择能力。5. 实盘执行闭环交易日志、上线清单与盘前巡检脚本5.1 交易日志记什么记录执行偏差而不是记录盈亏交易系统上线后最先出问题的往往不是系统本身而是执行。要监控执行日志就要围绕计划与实际的差来记。每笔交易至少记五个字段触发信号的日期和参数、计划的入场价和数量、实际成交价和数量、偏差原因、备注。每周统计一次执行偏差率也就是实际成交与计划动作不一致的笔数占总交易笔数的比例。偏差率持续高于 20%先不要怪策略去查流程是信号通知不及时还是下单环节手滑还是资金没有提前备好。把偏差原因归归类你会很快看到自己的系统断在哪一环。5.2 上线前检查清单回测结论到实盘的最后一道闸检查项通过标准未来函数检查所有信号基于 T 日收盘计算成交在 T1 日成本检查回测包含手续费、印花税、滑点且滑点取偏高值测过参数稳健性最优参数位于参数平原而非孤岛样本外检验后段样本外收益与样本内同向随机基准p_value 小于 0.05资金约束倒推仓位不超过单标的上限和可用资金这份清单在第一次实盘前逐项打勾之后每次改参数或改规则只重跑受影响的检查项不用全量重来。5.3 盘前巡检小脚本让数据检查和仓位校验自动化实盘阶段容易漏的是每天开盘前的数据确认。手写一个十几行的巡检脚本把数据新鲜度、信号状态、仓位上限三个检查放进去每天开盘前跑一次比依赖闹钟和备忘录可靠得多。def morning_check(symbols, params): for sym in symbols: df load_kline(fdata/{sym}.csv) # 检查行情是否更新到最近交易日 if df.index[-1].date() ! last_trade_date(): raise RuntimeError(f{sym} 数据未更新) df generate_signals(df, **params) if df[signal].iloc[-1] 1: print(f{sym} 今日出现买入信号) # 检查现有持仓是否越界 for sym, qty in get_positions().items(): if qty position_limit(sym): raise RuntimeError(f{sym} 持仓超过上限)last_trade_date()返回最近交易日用来确认行情文件没有停留在昨天generate_signals沿用回测同一套函数保证实盘信号和回测信号同源position_limit读取规则矩阵里的仓位上限。脚本按顺序执行完只输出异常和信号不替你做交易决定。每周五收盘后把执行偏差率、实际胜率、盈亏比三个数填进同一张表连续观察一个月你会看清交易系统的短板到底在规则、在参数还是在执行流程。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价