news 2026/10/13 7:09:28

恒生自动交易系统实战:从源码部署到策略风控全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
恒生自动交易系统实战:从源码部署到策略风控全解析

简介:AutoTrader是一套面向恒生电子交易接口的自动交易系统开源实现,主要服务量化交易开发者、金融IT人员和相关专业学生,用于解决策略下单、行情接入、交易流程管理等自动化需求,适合作为恒生交易系统二次开发的起点。资源包共84个文件,整体约69.94MB,以C++源代码为主,包括32个.h头文件、22个.cpp源文件,并配有两个Visual Studio解决方案及工程配置文件、SQL数据库初始化脚本和动态库文件;目录中另有.gitignore等版本管理辅助项,整体结构清晰,易于按模块阅读。资源已有1826人学习/下载。通过阅读源码和工程,可以了解自动交易系统与恒生柜台系统的对接方式,掌握行情订阅、策略执行、订单管理、仓位控制及异常保护等模块的实现细节;两个解决方案和SQL脚本也便于对照理解系统分层与数据表设计,对自主搭建或改造同类交易平台具有较高的参考价值。

1. 恒生自动交易系统:把人工盯盘变成一条确定性的执行链路

做过实盘的人都知道,柜台软件的手工下单界面看起来功能齐全,但真到了行情剧烈波动那几分钟,从你看到盘口到敲完委托单,往往已经错过最佳价位。更麻烦的是,手工操作没法保证纪律——该止损的时候犹豫两秒,结果就完全不一样。AutoTrader 这类恒生自动交易系统,本质上是把“看盘、决策、下单、撤单、查持仓”这一整套动作从人肉操作变成代码执行:策略拿到行情快照,算好价格和数量,直接通过恒生交易接口把委托单送进柜台,全程不需要人工干预。对做日内高频、套利、算法拆单的团队来说,这是刚需;对刚接触程序化交易的个人来说,它是理解“行情怎么进来、订单怎么出去”最直接的入口。这篇笔记就基于一个可行的开源 AutoTrader 项目,把源码结构、下单链路、策略写法、风控参数和几个真实踩过的坑一次讲清楚。

2. 把开源 AutoTrader 跑起来:目录结构、配置替换与启动顺序

2.1 拿到源码后,先弄清楚这三个目录的分工

开源 AutoTrader 项目的代码目录虽然各有差异,但只要它对接的是恒生柜台,基本都跑不出这三个模块:接入层、策略层、公共层。

接入层通常叫gateway或者api,里面是恒生交易接口的封装,负责登录、委托、撤单、查询资金持仓这些基础操作。这一层是离柜台最近的代码,也是最不能乱改的部分。策略层一般叫strategy或trader,它只管“看到什么条件,下什么单”,完全不碰通讯细节。公共层则是一些工具函数,比如日志、配置文件解析、时间处理、订单状态机。

我一般建议拿到源码后先不要急着编译,而是把这三个目录的边界画清楚。理由是:你在实盘中遇到的绝大多数问题,最后都出在“策略层觉得自己已经下单了,但接入层根本没发出去”这种边界模糊的地方。

# 以典型的 Python 项目为例,目录结构长这样 AutoTrader/ ├── gateway/ │ ├── hundsun_client.py # 恒生柜台接口封装 │ ├── config.py # 连接参数加载 │ └── order_helper.py # 订单状态转换工具 ├── strategy/ │ ├── base_strategy.py # 策略基类,定义 on_tick / on_order_report │ ├── ma_cross.py # 简单均线策略示例 │ └── arbitrage.py # 套利策略模板 ├── common/ │ ├── logger.py # 日志模块 │ ├── time_utils.py # 交易时间判断 │ └── risk_check.py # 风控前置检查 ├── config/ │ ├── connection.yaml # 柜台连接配置 │ └── strategy.yaml # 策略参数配置 └── main.py # 启动入口

逻辑说明:gateway/hundsun_client.py是整套代码里唯一跟恒生接口打交道的地方,它把登录、报单、撤单这些操作封装成login()、send_order()、cancel_order()这样的方法。策略层调用这些方法时,不需要关心底层是 TCP 长连接还是 API 调用。main.py负责读配置文件、初始化日志、启动策略循环。

参数说明:connection.yaml里的host、port、user、password分别对应恒生交易网关的地址、端口、资金账号和密码。这里要注意,开源项目里默认给的往往是仿真环境或开发环境的地址,直接拿真实账号去连大概率失败,因为生产环境的网关地址和通信密钥是完全不同的。

2.2 替换仿真账号:别拿生产环境当试验场

某开发者第一次部署 AutoTrader 的时候就踩了这个坑:他图省事,直接用实盘账号去连项目文档里给的网关地址,结果登录直接被拒,还差点触发柜台的风控锁。后来才知道,恒生柜台对“哪个账号允许从哪个 IP 连哪个网关”是有绑定的。

拿到源码后的第一件事,是去申请一套仿真账号。国内的期货、期权仿真环境一般都可以通过券商或交易所的仿真通道申请,拿到后会有一组独立的 IP、端口、账号和密码。把这些信息填进connection.yaml:

gateway: host: "192.168.1.100" # 仿真网关地址 port: 8001 # 仿真网关端口 user: "008888" # 资金账号 password: "your_password" # 密码 app_id: "auto_trader_001" # 应用标识,柜台用来识别客户端 auth_code: "xxxxxx" # 认证码,注意不是登录密码

逻辑说明:app_id和auth_code是恒生接口里特有的双因子认证字段,很多初学者会把auth_code当成密码的重复输入,实际上它是柜台下发的一个独立认证码,在仿真环境里跟密码同时开通。丢了要联系系统管理员重置,没法自己在客户端改。

参数说明:host和port不要只信配置注释,拿到账号后先问一句“网关地址有没有变更”,因为仿真和实盘往往是两套物理隔离的网络,IP 段完全不一样。我习惯把仿真配置单独存一份文件,不进 git 仓库,防止哪天不小心用实盘配置覆盖了仿真配置。

2.3 构建与启动:日志先通,再谈策略

编译安装依赖这一步没什么玄学,但有一个顺序问题值得注意:恒生接口的底层通讯库有些是 C++ 写的,需要通过接口供应商提供的 SDK 编译安装。开源项目一般会在requirements.txt里列出 Python 依赖,但那些非 Python 的底层库,需要你手工安装。

# 建议按这个顺序走 pip install -r requirements.txt python main.py --config config/connection.yaml --strategy config/strategy.yaml

逻辑说明:main.py启动后做了三件事:加载配置文件,建立到恒生交易网关的连接,然后进入策略主循环。主循环里做的事其实很朴素——读取行情、调用策略逻辑、下发订单、处理回报。

参数说明:--config和--strategy分别是连接配置和策略配置的路径。拆开的好处是,你可以在不重启策略的情况下切换连接环境,调试时尤其方便。

启动之后第一件事不是看订单,而是看日志里是否出现“登录成功”和“资金查询完成”这两条记录。只有资金和持仓查到了,才能确认这个账号能用、连接是通的。很多新手上来就写策略、跑订单,结果订单回报一直不来,回头查才发现登录根本没成功。

3. 订单链路原理:从策略到柜台,中间到底经历了什么

3.1 恒生交易接口的两种工作模式

恒生柜台对外提供的接口,常见的说法叫“OpenAPI”或“交易网关”,底层是 TCP 长连接。这种长连接跟你在浏览器里用 HTTP 请求完全是两回事:HTTP 请求是一次性的,请求完就断开;而交易接口是连接建立后长期保持,客户端跟网关之间互相发心跳包,行情和回报都是通过这同一条连接推给客户端的。

理解了这一点,很多奇怪的现象就解释得通了。为什么某天早上连不上?可能是网关昨晚重启了,你的连接已经断了但你不知道。为什么下了单没回报?可能是你的客户端根本没收到推送,而不是订单丢了。

AutoTrader 这类开源项目,一般会把两种模式都封装好:一种是订阅模式,登录后被动等回报推送,适合做自动交易;一种是轮询模式,定时去查资金、持仓和委托状态,适合做监控工具。实盘自动交易必须用订阅模式,轮询只能作为辅助。

3.2 报文编码与字节序:最常见的隐性 Bug

恒生接口的报文格式,行内人都知道,本质上是定长字段拼接的二进制协议。每个字段占多少字节、什么顺序、什么类型,都有严格定义。开源项目里最常翻车的不是业务逻辑,而是编码转换。

# 常见的报错场景:字符串编码不对,导致下单字段被截断 order_data = { "instrument_id": "rb2510", # 合约代码 "exchange_id": "SHFE", # 交易所代码 "price": "3560.0", # 委托价格,注意是字符串 "volume": "1", # 委托数量 } # 某些恒生接口要求 GBK 编码 encoded_bytes = order_data["instrument_id"].encode("gbk") # 而 Python 字符串默认是 Unicode,直接传给接口就会出问题

逻辑说明:在上面这个例子里,instrument_id的值如果出现 UTF-8 和 GBK 混用,送到柜台那边解析出来的就是乱码,订单要么被拒,要么下到了错误的合约上。最典型的翻车现场是处理合约名称里的中文,比如某些品种的期权合约带“购”“沽”字样,一编码错整个订单就飞了。

参数说明:恒生接口对字符串字段的编码要求一般是 GBK,但不同版本可能有差异。拿到 SDK 头文件后,先看里面有没有编码或Charset相关的枚举定义。如果拿不准,最笨也最有效的办法是:先用仿真环境发一笔小单,然后去柜台客户端里查这笔委托的合约名称是否显示正常。

3.3 一条委托单的生命周期

把一条委托单从策略里送出,到柜台回报“已成”,全链路是这样一个状态机:

已报(Submitted) -> 待撤(PendingCancel) -> 已撤(Canceled) -> 部成(PartiallyFilled) -> 已成(Filled) -> 拒绝(Rejected)

很多开源策略只处理了“已报”和“已成”两个状态,忽略了“部成”和“拒绝”,这在实盘里非常危险。比如你的策略发了一笔 10 手的限价单,结果市场波动太快只成交了 3 手,剩下的 7 手挂在那边没动。如果策略不处理“部成”状态,它可能以为这单已经全部成交了,然后继续发下一笔单,导致持仓超过预期。

# 正确的做法是:在订单回报处理函数里,显式区分状态 def on_order_report(report): status = report["order_status"] if status == "FILLED": # 全部成交,可以更新持仓记录 self.position.update(report["instrument_id"], report["filled_volume"]) elif status == "PARTIALLY_FILLED": # 部分成交,剩余部分要决定是继续等,还是撤单重发 remaining = report["total_volume"] - report["filled_volume"] if self.should_cancel(remaining): self.cancel_order(report["order_id"]) elif status == "REJECTED": # 被拒单,必须记录原因,否则策略会在下次循环里继续报单 self.logger.error(f"订单被拒: {report['reject_reason']}")

逻辑说明:这段代码的核心是把“部成”和“拒绝”当成一等公民来处理,而不是只关注最终成交。PARTIALLY_FILLED分支里调用了should_cancel(),这个函数可以根据剩余数量、剩余时间、滑点预期来决定是否撤单,逻辑可以写得很简单,比如“超过 3 秒没完全成交就撤掉”。

参数说明:report字典的字段名在恒生不同版本接口里略有差异,但order_status、filled_volume、total_volume、reject_reason这些核心字段基本都有。使用前一定先打印一次完整的回报内容,亲眼确认字段命名,而不是想当然。

4. 策略开发实战:把轮询行情变成自动盯盘

4.1 行情数据的获取方式

恒生接口的行情接入和交易接口往往是分开的两套体系。交易接口管下单、撤单、查资金;行情接口管实时 tick、K 线、盘口快照。AutoTrader 这类开源项目一般会同时对接两个东西:一个行情源,一个交易通道。

行情源的选择直接影响策略质量。如果你做的是 t+0 的品种,需要看盘口五档甚至十档,那就得用速度快的行情源,比如直接从交易所的行情网关拿数据。如果只是做分钟级别的趋势跟踪,用普通的行情接口足够了。

# 行情回调的典型写法 class MomentumStrategy(BaseStrategy): def __init__(self, params): self.threshold = params.get("threshold", 0.5) # 价格变动阈值 self.tick_buffer = [] # 缓存最近N个tick def on_tick(self, tick): # 每来一个tick,更新价格缓存,并判断是否触发开仓条件 self.tick_buffer.append(tick["last_price"]) if len(self.tick_buffer) > 20: self.tick_buffer.pop(0) if len(self.tick_buffer) == 20: avg_price = sum(self.tick_buffer) / len(self.tick_buffer) if tick["last_price"] > avg_price * (1 + self.threshold): self.buy(tick["instrument_id"], 1)

逻辑说明:on_tick是策略基类暴露给子类的回调函数,每当行情网关推过来一个新 tick,这个函数就会被调用。策略要做的就是把状态维护好——缓存最近的 20 个价格、判断价格突破幅度、决定是否开仓。这种方式的好处是,策略代码不需要关心行情怎么来的,只管在回调里做决策。

参数说明:threshold是触发开仓的阈值,0.5% 是一个偏保守的起步值;数值设得太小容易被行情噪音频繁触发,设得太大可能一整天都开不了仓。建议先用历史数据做个简单回测,看这个参数在什么区间内交易次数和胜率比较平衡。

4.2 挂单、追单、撤单:一个最小可用的自动盯盘逻辑

很多新手写策略时,把重点放在“什么价格开仓”上,真正该花心思的其实是“开仓之后怎么办”。这里分享一个很实用的做法——五秒追单法。逻辑很简单:发出限价单后,如果 5 秒内没有完全成交,就撤掉重新按最新市场价格挂单。

import time class FastOrderExecutor: def __init__(self, gateway): self.gateway = gateway self.pending_orders = {} # key: 订单ID, value: 下单时间 def send_order_with_timeout(self, instrument_id, price, volume): order_id = self.gateway.send_order(instrument_id, price, volume) self.pending_orders[order_id] = time.time() return order_id def check_timeout(self): now = time.time() for order_id, sent_time in list(self.pending_orders.items()): if now - sent_time > 5.0: # 超过5秒还没完全成交,说明价格偏离市场,撤掉重发 self.gateway.cancel_order(order_id) self.pending_orders.pop(order_id) # 上面的循环只负责撤单,重发逻辑由策略在下一轮tick中触发

逻辑说明:send_order_with_timeout在下单的同时记录时间戳,check_timeout定期扫描所有未完成订单,把超时订单撤掉。撤单后不再立即重下,而是等下一轮 tick 来的时候由策略判断是否重新进场——这样可以防止行情剧烈抖动时连续追单造成不必要的手续费损耗。

参数说明:5 秒这个值不是拍脑袋定的,它取决于品种的流动性和滑点容忍度。我做过的实盘里,螺纹钢这种流动性好的品种 3 秒就够,而某些冷门合约要放宽到 10 秒。建议先用仿真环境测出“从下单到成交回报回来”的平均耗时,再乘以 2 到 3 作为超时阈值。

4.3 风控前置:把检查放在下单之前,而不是等爆仓之后

开源 AutoTrader 里一般会内置一个risk_check.py,但默认参数往往很宽松,直接拿去实盘会出事。最值得改的参数有三个:单笔最大手数、单日最大亏损、持仓上限。

class RiskManager: def __init__(self, max_volume_per_order, max_daily_loss, max_position): self.max_volume_per_order = max_volume_per_order self.max_daily_loss = max_daily_loss self.max_position = max_position self.daily_loss = 0.0 self.current_position = {} def pre_check(self, instrument_id, volume, estimated_price): # 1. 单笔手数超限 if volume > self.max_volume_per_order: return False, "单笔手数超限" # 2. 当前持仓加上新订单后超限 new_position = self.current_position.get(instrument_id, 0) + volume if new_position > self.max_position: return False, "持仓会超上限" # 3. 预估亏损超限 if self.daily_loss > self.max_daily_loss: return False, "当日亏损已达上限" return True, "OK" def on_order_filled(self, instrument_id, volume, price): self.current_position[instrument_id] = self.current_position.get(instrument_id, 0) + volume

逻辑说明:pre_check在下单前做三道检查,只要有一道不通过,订单就会被拦下并记录原因。很多开源项目的风控模块只是摆样子,真正的自动交易系统必须把风控做成强制校验,和下单接口串成同一个调用链。

参数说明:max_daily_loss建议按“当日账户权益回撤”来算,而不是按“累计亏损”。因为在单边行情里,即使每一笔单都有浮盈,一旦行情反转,之前的浮盈会瞬间变成亏损。按权益回撤来算,可以避免“今天赚了 2 万,回吐 1 万还在触发区间内”这种尴尬。

5. 避坑记录:恒生链路里最隐蔽的几个坑

5.1 乱码:行情和交易走同一套编码,偏字节序也会出问题

现象:在仿真环境里用 Python 发了一笔买单,柜台客户端里查询时发现合约名称变成了乱码,委托价格看起来也不对。
原因:代码里用的编码跟柜台预期的不一致。更隐蔽的是,某些恒生接口把价格字段定义成整型乘以 1000 的形式,比如 3560.5 实际传输的是 3560500,如果直接用浮点数传输,对端解析出来就会差三个数量级。
解决:在封装接口时统一做一次转换,字符串字段统一 GBK 编码,数字字段看接口定义是否需要乘系数。仿真环境里先下一笔 1 手的最小单,查回报确认价格、合约都正确后再跑策略。

5.2 行情时钟不一致:策略说到了,柜台说没到

现象:策略按自己的系统时间判断“集合竞价已经结束”,发了一笔市价单,结果被柜台以“非交易时段”拒绝。
原因:柜台的交易时段判断用的是交易所时钟,跟你的本地电脑时间可能有几秒到几十秒的偏差。尤其是在做开盘前条件单的时候,哪怕差 1 秒都会出问题。
解决:登录成功后,先向柜台同步一次时间戳,后续所有“是否可以下单”的判断都基于柜台时间,而不是本地时间。开源项目里一般会有sync_time()之类的函数,登录成功后记得调用。

5.3 撤单把别人的单也撤了

现象:策略持仓出现错误,一查发现某笔订单被撤了,但自己的策略代码里根本没发过撤单指令。
原因:恒生柜台支持按“委托编号”或“合约”两种方式撤单,如果你的代码里传的是instrument_id而不是order_id,柜台会把这个合约下所有未成交的挂单全部撤掉。在联机环境里,同一账号可能有多个客户端同时在操作,影响范围会更大。
解决:撤单接口强制要求传order_id,并且在下单后立即保存order_id到本地字典。如果实在只能按合约撤,那就先查一下这个合约下面有哪些委托,选择性地撤。

5.4 登录成功当天晚上就掉线了

现象:有一天早上启动策略时,发现登录失败,日志里显示“连接已断开”或“认证超时”。
原因:恒生网关在收盘后或夜间会做系统初始化,长连接在某个时间点被服务端强制断开。开源项目默认的心跳机制如果只发心跳但不检测回复,就会被这种静默断开坑到。
解决:客户端要主动检测连接状态,比如每隔 30 秒发一次心跳,如果连续 3 次没有收到心跳回复,就主动断开重连。重连时要带上“重连次数”参数,避免无限重连把网关打爆。实盘环境里,开盘前 5 分钟建议手动检查一次连接状态。

5.5 仿真环境成交回报比实盘慢很多

现象:仿真环境下,下了单之后五六秒才有回报,而且成交价格经常跟最新价差很远。
原因:仿真环境的撮合引擎是模拟撮合,很多品种用的是“按最新价成交”而不是“按盘口深度撮合”的规则,流动性也是模拟的,跟实盘完全不是一个量级。
解决:仿真环境只用来验证“你的代码能不能正常走完整条链路”,不要用来评估策略的滑点和收益率。策略参数调试建议用手工记录的行情数据回测,或直接上实盘用小仓位测试。

6. 验证与复盘:没有回测系统,就用仿真跑出一份复盘报告

AutoTrader 这类开源项目多数不带完整的回测引擎,只有实盘或仿真交易。但只要你把日志记录完整,完全可以自己做一份比回测更有价值的复盘报告。

我的做法是:每次策略跑完,除了看盈亏,更重点看三张表。第一张表是“订单状态分布”,统计有多少单被拒、被撤、部成、已成,这能直接暴露策略代码里的 bug。第二张表是“下单到成交的时间差”,如果平均时间差超过 2 秒,说明你的行情源或者网关链路有延迟,策略效果会大打折扣。第三张表是“滑点分布”,用你的下单价格跟当时盘口最优价的差来算,滑点稳定在一个区间内说明执行没问题,滑点忽大忽小就要检查是不是追单逻辑太激进。

import sqlite3 import pandas as pd # 把日志里的订单数据导入 SQLite,方便做统计 conn = sqlite3.connect("backtest_results.db") orders_df = pd.read_sql(""" SELECT order_status, COUNT(*) AS cnt FROM orders GROUP BY order_status """, conn) print(orders_df) # 直接看出有多少单被拒,多少单部成 # 计算每笔单从发单到全部成交的耗时 time_df = pd.read_sql(""" SELECT instrument_id, AVG(JULIANDAY(filled_time) - JULIANDAY(sent_time)) * 86400 AS avg_fill_sec FROM orders WHERE order_status = 'FILLED' GROUP BY instrument_id """, conn) print(time_df)

逻辑说明:这段代码假设你的策略已经把每笔订单的时间戳、状态、成交量都记录到了 SQLite 数据库里,然后直接用 SQL 做分组统计。AVG(JULIANDAY(filled_time) - JULIANDAY(sent_time)) * 86400算的是填充时间差的平均值,单位是秒。

参数说明:sent_time和filled_time建议都用柜台回报的时间,而不是本地时间,因为本地时钟可能已经偏移了。如果日志里没有存柜台时间,那就只能退而求其次用本地时间,但要意识到这个统计结果会有几百毫秒的误差。

从那次被乱码坑了一整天之后,我养成了一个习惯:每次拿到新的柜台接口版本,第一件事不是写策略,而是把“发一笔最小单、撤掉它、查回报”这条链路反复跑十遍,确认编码、价格精度、时间戳、订单状态全部符合预期。这套确认动作大概只需要半小时,但它能挡住后面大部分莫名其妙的盘中问题。希望这篇笔记能帮你在恒生自动交易这个方向少走一些弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 7:07:56

C语言sizeof与strlen区别详解:底层原理、数组指针陷阱与笔试真题

先问你一道送分题:char s[] "hello";那么sizeof(s)和strlen(s)分别是多少?答案是6和5。可别小看这一题,据这些年我面试新人、帮网友改代码的经验,能在五分钟内把这两个东西讲清楚的人,连三分之一都不到。很…

作者头像 李华
网站建设 2026/10/13 7:07:52

bert-base-chinese中文NLP模型加载与微调实战指南

简介:这份资源是针对中文场景优化的BERT预训练模型权重包,面向深度学习与自然语言处理领域的开发者、研究者和学生,可用来完成中文文本分类、情感分析、问答、翻译等任务,尤其适合需要离线加载大模型进行微调或验证算法效果的学习…

作者头像 李华
网站建设 2026/10/13 7:05:47

毛坯房装修的正确流程是什么?

现在温度越来越低,虽然不适合动工,可以趁着淡季活动多、优惠大、工程少,尽快定下装修公司、设计方案和需要定制的装修材料,不仅省钱,还不耽误工期。1)装修的准备工作,2)毛坯房装修的…

作者头像 李华
网站建设 2026/10/13 7:05:46

AI做PPT终于不丑了!开源gpt-image2-ppt-skills让你自动生成PPT

AI做PPT终于不丑了!开源gpt-image2-ppt-skills让你自动生成PPT 大家好,我是星哥。 最近后台有不少朋友跟我吐槽:“星哥,现在市面上的 AI 做 PPT 工具,怎么做出来的东西都那么‘AI味’啊?” 说实话&#…

作者头像 李华