简介面向程序化交易开发者的一份 C 券商接口对接示例工程聚焦算法交易中的自动下单环节帮助读者理解如何通过证券公司提供的 API 完成连接建立、身份认证与委托发送适合具备 C 与网络编程基础、希望把策略思路推进到实盘落地的开发者参考。压缩包共 15 个文件约 16KB以 .h 头文件与 .cpp 源文件为主体另含 axstock.lib 券商接口静态库、vcproj 工程配置、rc 与 ico 界面资源以及 ReadMe 说明文档工程可在 Visual Studio 中直接打开编译登录与下单逻辑分层清晰。目前已有 5773 人学习下载。代码涵盖登录对话框封装、登录请求发送与响应解析、预编译头组织等关键环节配合静态库调用示例可帮助读者梳理网络通信、订单类型构造、事件监听与异常处理的实现路径是理解券商接口调用流程、搭建自研交易平台雏形的实用参考。1. 从一次手工挂单的延迟说起这份接口示例源码能替你做什么行情在 14:56 突然拉了两个价位你盯着盘口按下了买入成交回报回来时已经滑掉三档——这种场景做过手动交易的人都不陌生。这份《程序化交易自动下单券商接口示例源代码》解决的就是这一段把“策略给出信号”到“委托真正落到柜台”之间的手工环节换成一条可以监控、可以重放、可以回滚的代码链路。它不是一个能直接赚钱的策略包里面没有选股逻辑也没有神奇的参数它是执行层的一组骨架——会话建立与保活、委托参数组装、下单/撤单调用、回报解析与本地状态机、下单前的风控拦截都给了可读的示例实现。适合两类人手里已经有一版能出信号的策略、但卡在“怎么让它自动发出去”的开发者以及想把每天几十笔手工单改成脚本执行、又不想一上来就依赖黑盒终端的从业者。它不适合对交易规则完全没概念、指望跑起来就稳定盈利的人。2. 拆开代码包三类接口形态与一次委托的完整链路拿到一份接口示例最先要判断的不是代码写得好不好而是它对接的是哪一类通道。同样叫“券商接口”HTTP 开放平台和柜台二进制协议在字段语义、回报节奏、失败重试策略上完全是两个世界盲目照搬参数一定会翻车。2.1 示例源码的分层接入层、业务层、风控层我一般把这类代码按三层去看而不是从头文件顺序读。接入层负责和柜台打交道建链、登录、心跳、断线重连、报文编解码这一层写错了上面写得再漂亮也白搭。业务层负责把“买 100 股某标的、限价 10.20”翻译成柜台认识的字段组合并把回来的成交回报、委托回报、撤单回报归并到同一个订单对象上。风控层在业务层之前做数量、金额、频率、价格偏离度的拦截。示例源码里这三层的边界大致是session目录是接入层order目录是业务层risk目录是前置校验model里放着订单、成交、回报的公共结构。阅读顺序建议从model开始——先搞清一个订单对象有哪些字段、状态怎么流转再回头看它是怎么被填进去的。反过来读很容易陷在 socket 收发细节里出不来。分层带来的实际好处是换柜台时往往只动接入层。我见过把业务逻辑直接写死在报文拼装函数里的代码换一家柜台等于重写整个项目这种血泪经验没必要重复一遍。2.2 三种常见接口形态HTTP/JSON、FIX、二进制长连接市面上能拿到手的程序化交易接口大致落在三类里选型和排错思路完全不同形态典型特征回报时效常见坑HTTP/JSON 开放接口请求-响应有签名鉴权文档齐全秒级需轮询或另接推送频率限制严格、签名串拼错、幂等靠客户端自己保证FIX 协议标签号字段35D 新单、35F 撤单毫秒到秒级会话序号MsgSeqNum断档、重发请求处理不当二进制长连接柜台私有结构体、需要 .so/.dll登录后有私有心跳毫秒级假死检测、字段偏移、版本不匹配示例源码默认实现的是第二、三类共用的“长连接 请求/回报分离”模型发出去的请求只回一个受理确认真正的成交和撤单结果是异步推回来的。这一点必须先在脑子里立住——很多人按 HTTP 的思维写代码place_order()拿到返回值就当作“已成交”然后去更新持仓账面立刻和柜台对不上。HTTP 接口相对好上手但要注意签名和频率FIX 的会话层比业务层更容易出事序号断了要么重发要么建新会话示例里给了一个简化的序号管理和重发缓存二进制柜台则要看清楚厂商给的版本号结构体字段对不上时收到的价格可能是乱码而不是报错。提示示例代码里的字段名是通用命名实际对接时以柜台文档的字段顺序和类型为准尤其是数量、金额这类字段是字符串还是整型、单位是股还是手一定要逐项核对。2.3 一次委托从函数调用到柜台回报的六跳把链路摊开看一次自动下单大概经过六跳策略调用下单函数 → 风控前置校验 → 组装请求并生成客户端委托号 → 发送到接入层排队 → 柜台受理并回委托回报 → 成交回报异步推送回来更新本地订单。任何一跳出问题最终表现都是“单子没出去”或者“出去了但本地不知道”。关键在于第四跳到第五跳之间很多柜台是“先回受理、再回成交”受理成功不代表成交。示例里把这两个事件分开处理on_insert_ack只更新委托状态on_trade才累加成交量。如果只写一个回调日志里就会出现“状态是已报但成交量是 100”的诡异记录。另一个容易忽略的是第四跳的排队。示例用一个单线程发送队列保证同一账户的请求按生成顺序发出避免同一标的的先卖后买被柜台判成废单。多线程直接并发调发送函数短时间内在同一标的上交叉下单废单率会明显上升。3. 把示例跑起来环境、配置与第一笔模拟委托这一章按能照着敲的粒度写。示例默认连的是模拟环境所有账号、经纪商编号都是占位值跑通之后再换成柜台给的真实参数。建议第一次跑不要接任何策略手工调一次下单和撤单确认回报都能对上再谈自动化。3.1 依赖、目录与配置文件逐字段说明环境用 Python 3.10 以上依赖只有三四个装完就能跑。目录结构大致如下路径作用是否需要改config/gateway.yaml连接参数、风控阈值必须改src/session/建链、登录、心跳、重连一般不改src/order/下单、撤单、回报分发改字段映射src/risk/前置风控规则按账户改阈值examples/手工调用脚本参考用logs/报文与业务日志不用改安装依赖用一条命令即可注意虚拟环境隔离这类项目经常要同时维护两三个柜台的版本全局安装迟早互相打架python -m venv .venv source .venv/bin/activate pip install -r requirements.txt # 通常包含 pyyaml、websocket-client 或 socket 封装、pandas(可选)配置文件是整份代码里最该逐行看懂的地方示例字段如下gateway: mode: sim # sim模拟环境, live实盘; 示例默认 sim, 切换前先跑通全部回报 endpoint: tcp://127.0.0.1:9101 # 柜台前置地址, 模拟环境给的是本地网关 broker_id: 9999 # 经纪商编号, 柜台分配, 占位值 account: SIM000001 # 资金账号 password_env: GW_PASS # 密码从环境变量读, 不落盘 heartbeat_sec: 15 # 心跳间隔 reconnect_backoff: [1, 2, 5, 10, 30] # 重连退避秒数 risk: max_order_qty: 1000 # 单笔最大数量(股) max_order_notional: 200000 # 单笔最大金额(元) max_orders_per_sec: 5 # 每秒最大委托笔数 price_deviation_pct: 0.02 # 价格偏离最新价上限 2%mode这一项建议在代码里做成硬开关非sim时启动打印醒目日志并且要求额外传一个命令行参数才允许继续防止调试脚本半夜把实盘账户刷了。password_env的方式比把密码写进 yaml 稳妥得多启动前export GW_PASSxxx即可。heartbeat_sec不是越小越好柜台一般有最小间隔限制设成 3 秒以内有些柜台会直接断你。3.2 建立会话与心跳保活建链和登录是整条链路里最“玄学”的一段网络通、账号对也可能因为前置机负载或者会话数超限登不上去。示例给的是带指数退避的重试核心代码import os, time, logging from dataclasses import dataclass dataclass class GatewayConfig: endpoint: str broker_id: str account: str heartbeat_sec: int 15 backoff: tuple (1, 2, 5, 10, 30) class Session: def __init__(self, cfg: GatewayConfig): self.cfg cfg self.logged_in False self.last_rx 0.0 # 最后一次收到任意报文的时间, 用于假死检测 def connect(self) - bool: for i, wait in enumerate(self.cfg.backoff): try: self._open_socket() # 建立底层连接 self._login(self.cfg.account, os.environ[GW_PASS], # 密码只从环境变量取 self.cfg.broker_id) self.logged_in True self.last_rx time.time() logging.info(login ok, attempt%s, i 1) return True except Exception as e: logging.warning(connect failed: %s, retry in %ss, e, wait) time.sleep(wait) return False def keepalive(self): while self.logged_in: # 超过 3 个心跳周期没收到任何报文, 判定连接假死, 主动重连 if time.time() - self.last_rx self.cfg.heartbeat_sec * 3: logging.error(heartbeat timeout, reconnecting) self._reconnect() continue self._send_heartbeat() time.sleep(self.cfg.heartbeat_sec)last_rx这个字段是整个保活逻辑的核心。只发心跳、不检查收包是国内长连接柜台最典型的假死场景TCP 层没断但柜台侧会话已经被清掉你发出去的单全部石沉大海而发送函数不报错。判定阈值取心跳间隔的 3 倍是个经验值太灵敏会在网络抖动时频繁重连太迟钝则要等好几分钟才发现。_reconnect()里必须做一件事重连成功后重新登录、重新订阅、并且把本地所有“已报未成”的订单标记为“状态待确认”主动发一次查询去对账。直接重连当无事发生是后面重复下单的根源。3.3 下单、撤单、回报的完整闭环建好会话之后先别急着接策略用示例里的手工脚本走一遍完整闭环。下单函数长这样from enum import Enum class Side(str, Enum): BUY B SELL S class PriceType(str, Enum): LIMIT 2 # 限价 MARKET 1 # 市价, 部分柜台要求额外字段 BEST_5 5 # 最优五档即时成交剩余撤销 def place_order(session, symbol, side, price, qty, price_typePriceType.LIMIT, cidNone): req { MsgType: ORDER_INSERT, ClientOrderId: cid, # 幂等键, 全局唯一, 由调用方生成 Symbol: symbol, # 带市场后缀, 如 600000.SH Side: side.value, Price: f{price:.2f}, # 按标的价位精度格式化, 不要传 float Volume: int(qty), # 单位: 股, 按整手校验 PriceType: price_type.value, TimeInForce: 0, # 0当日有效 } return session.request(req, timeout3.0)几个参数值得单独说。ClientOrderId不能省它是你在本地唯一能可靠定位一笔委托的钥匙柜台返回的OrderId有时候要延迟一拍才给。Price用字符串而不是浮点是因为10.2存成 float 再格式化可能变成10.199999直接被柜台判成价格非法。Volume按股传、按整手校验A 股一手 100 股科创板等板块另有规则这里不做展开示例里留了校验钩子。撤单只要原样带上OrderId没有就带ClientOrderId回报路径和新单一致。收到CANCELED之后本地状态才算终结别在发出撤单请求的那一刻就把订单从活动列表里删掉——撤单也会失败尤其是临近收盘撮合高峰。回报分发建议按ClientOrderId和OrderId双索引存一份订单簿两个键都能查到同一笔单。只存一个的后果是委托回报先到、成交回报后到还好成交回报先到时你查不到单只能丢掉或者靠猜日志里就会缺失一段。4. 参数怎么设价格类型、委托属性与回报状态机接口跑通只是及格线真正决定这套自动下单能不能用在生产的是字段语义和状态机。这一章把最容易填错、也最容易在事后复盘时说不清的部分拆开。4.1 价格类型与委托数量最容易填错的六个字段同样是“限价买入”不同柜台的字段组合差别不小。下面这张表是这类示例代码里最常见的六个字段值得贴在显示器边上字段常见取值填错的后果价格类型限价 / 市价 / 五档即成剩撤 / 本方最优用市价单打小盘股, 滑点失控委托数量股数, 整手非整手直接废单买卖方向B/S, 不是 buy/sell 字符串报文字段不识别, 整单被拒开平标志股票一般不用, 期货必填缺失时柜台拒单或默认值错误有效期当日有效 / 即时成交剩余撤销收盘前挂单跨日, 次日变成废单价格精度按标的最小变动价位小数位多一位就是价格非法市价单要特别克制。示例里虽然实现了MARKET但默认风控会拦截它原因是市价单在流动性差的标的上成交价可能远离预期程序化下单放大了这个风险。我一般只对流动性最好的几个标的开放市价其余一律限价加追价逻辑。数量字段还有一个隐蔽的坑部分柜台的Volume单位是“手”而不是“股”文档里写得含糊示例配置里用volume_unit标出来。传错了就是 100 倍杠杆的错误这种单子出去要么被资金校验拦下要么直接捅出大窟窿。4.2 回报状态机与幂等别让一次重连重复下单本地订单状态要收敛到一个明确的状态机否则日志永远对不上账。示例里的状态流转是PENDING已发出未确认→SUBMITTED柜台已受理→PARTIAL部分成交→FILLED/CANCELED/REJECTED。另外留一个UNKNOWN用于重连后还没对账的中间态。class OrderBook: def __init__(self): self.by_cid {} # ClientOrderId - order self.by_oid {} # 柜台 OrderId - order self.orphan [] # 成交回报先于委托回报到达时暂存 def on_insert_ack(self, ack): o self.by_cid.setdefault(ack[ClientOrderId], {cid: ack[ClientOrderId], filled: 0, status: PENDING}) if ack.get(OrderId): o[oid] ack[OrderId] self.by_oid[ack[OrderId]] o o[status] SUBMITTED if ack.get(Accepted) else REJECTED self._drain_orphan(o) # 把之前暂存的成交回报补记上 def on_trade(self, t): o self.by_oid.get(t[OrderId]) or self.by_cid.get(t.get(ClientOrderId)) if o is None: self.orphan.append(t) # 查不到就先挂起, 不要丢 return o[filled] int(t[Volume]) o[status] FILLED if o[filled] o[qty] else PARTIALorphan这个暂存列表看着多余实际能救回不少丢单。快速行情下成交回报跑在委托回报前面是常有的事尤其是通过网关转发的时候。幂等则靠ClientOrderId保证。生成规则我一般用“日期 策略编号 自增序号”比如20240612S01000123重启后从当天的最大序号继续不要用随机数——随机数在重连补发时无法判断是不是同一笔。发送超时后要不要重发判断依据就是这个 ID先查本地订单簿状态还是PENDING且超过超时阈值才用同一个 CID 重发一次柜台侧会按重复委托处理而不是新建一笔。4.3 前置风控限频、单笔上限与资金校验风控放在策略和接口之间作用是拦住“代码 bug 变成真金白银”的那一类事故。示例里给了四项单笔数量上限、单笔金额上限、每秒委托笔数、价格偏离度。前两项是硬阈值后两项防的是失控循环。import time class PreTradeRisk: def __init__(self, cfg, latest_price_fn): self.cfg cfg self.latest latest_price_fn # 回调: 取最新价 self.window [] # 最近 1 秒的委托时间戳 def check(self, req) - str | None: qty, price int(req[Volume]), float(req[Price]) if qty 0 or qty % 100 ! 0: return REJECT_LOT if qty self.cfg[max_order_qty]: return REJECT_QTY_LIMIT if qty * price self.cfg[max_order_notional]: return REJECT_NOTIONAL ref self.latest(req[Symbol]) if ref and abs(price / ref - 1) self.cfg[price_deviation_pct]: return REJECT_PRICE_DEVIATION # 防手滑打错价格 now time.time() self.window [t for t in self.window if now - t 1.0] if len(self.window) self.cfg[max_orders_per_sec]: return REJECT_RATE_LIMIT self.window.append(now) return None限频这一段用的是滑动窗口比固定计数器准确代价是列表要定期裁剪。频率阈值别贴着柜台的上限设留三成余量因为你的计数和柜台的计数窗口不完全对齐。价格偏离度这一项是我认为性价比最高的规则。策略信号出错、价格变量取错、单位换算错最终都表现为价格离谱这一条能拦住绝大多数“手滑型”事故。5. 避坑实录接口对接最容易翻车的五个地方下面五条都是我在对接阶段真实遇到过的按“现象 → 原因 → 解决”写能省掉不少半夜爬起来看日志的时间。5.1 连接层心跳正常单子却发不出去现象日志里心跳一直在发send()也不报错但柜台没有任何回报持仓和委托列表都不动。原因TCP 连接还在柜台侧会话已经因为超时或前置切换被清掉了。发送缓冲区还能写所以应用层感知不到。解决不要以“发送成功”作为连接健康的判据改成以“收到报文”为准。示例里用last_rx做假死检测超过三个心跳周期没有任何入站报文就主动重连。重连之后第一件事是查委托和查持仓把本地状态和柜台对齐而不是接着发新单。还有一个变种重连太频繁被柜台限流登录返回“会话数超限”。退避策略里要加最大等待和上限次数连续失败超过阈值就报警停手别无限重试。5.2 业务层重连之后同一笔单下了两次现象网络抖了一下恢复后发现同一个标的出现了两笔一模一样的委托数量翻倍。原因发送超时后代码走了重发逻辑但重发时用了新生成的ClientOrderId柜台认为是两笔独立委托。解决幂等键必须在“生成请求”那一刻确定并跟随这笔委托走完全部重试流程。超时重发前先查本地订单簿如果状态已经不是PENDING说明柜台其实收到了不要再发。重连对账时也要按 CID 去重。5.3 字段层价格明明是合法价却一直废单现象报单价格和盘口一致但委托回报里状态是废单错误码指向价格。原因价格精度或最小变动价位不对。有的标的价位是 0.01有的 ETF 是 0.001代码里统一用两位小数格式化后者就被截断了反过来给 0.01 价位的标的传三位小数也可能被判非法。解决把标的的价位精度做成配置或者从合约信息里取格式化时按精度来不要写死。数量单位同理股和手必须在上层就统一别指望柜台帮你换算。5.4 回报层成交回报先到本地查不到订单现象日志里出现一条成交回报但订单簿里没有对应的单成交被丢弃收盘对账差一笔。原因委托回报和成交回报走不同通道到达顺序无保证。快速行情下成交回报跑到前面很正常。解决查不到就先暂存示例里的orphan列表等委托回报到了再合并或者用ClientOrderId反查很多柜台的成交回报里带这个字段。无论如何不要直接丢弃丢了就是账实不符。5.5 时间层收盘前撤单回报说“已撤”持仓却没变现象14:59 发撤单收到撤单成功回报但结算时发现单子已经成交了。原因撤单请求和撮合在柜台侧是竞态的回报顺序不代表业务顺序。撤单成功回报可能对应的是“撤单请求已受理”真正的撤单结果要看最终的委托状态。解决撤单后不要立刻认为订单终结等委托状态变成CANCELED且成交量不再变化再收尾。临近收盘的撤单请求最好在本地做一次短间隔轮询确认并记录最终成交量收盘对账时以柜台查询结果为准。6. 进阶把示例改成能跑一天的骨架示例代码解决的是“一笔单怎么发出去”真正上生产还要回答“跑一整天不出事”。我一般的做法是先搭一个只做记录、不做决策的骨架让它跟着真实行情跑满一个交易日看日志里有没有异常断连、有没有孤儿回报、有没有状态卡在PENDING不动的订单确认干净了再接策略。骨架的主循环大致是这样重点不在交易逻辑而在异常兜底def run_trading_day(gw, book, risk, strategy, halt_fileHALT): if not gw.connect(): logging.critical(gateway login failed, abort) return threading.Thread(targetgw.keepalive, daemonTrue).start() while is_trading_time(): # 交易时段判断, 含午休 if os.path.exists(halt_file): # 熔断开关: 落地一个文件即停手 logging.critical(halt file found, stop sending) break sig strategy.next_signal() # 策略只产出信号, 不下单 if sig is None: time.sleep(0.2) continue reason risk.check(sig) if reason: logging.warning(risk reject %s: %s, sig[Symbol], reason) continue place_order(gw, cidnext_cid(), **sig) time.sleep(0.05) # 主动节流, 别把柜台打满 reconcile(gw, book) # 收盘对账: 持仓、委托、成交三份比对halt_file这个土办法非常好用出问题时不需要去杀进程touch HALT就停手比任何优雅退出都可靠。reconcile()是每天的必做项把本地订单簿、柜台委托列表、柜台成交明细三方比对差异写进当日报告。差异超过阈值第二天就不许自动启动。几个进阶参数值得单独调发送节流间隔从 50ms 起按柜台限频往下压别一上来就设 1ms重连退避的最大次数设 5 次超过就报警而不是继续重试日志按天切分并且保留原始报文出问题时能回放。下面这张表是我自己用的上线前检查项检查项通过标准假死检测断网 3 分钟后能自动重连并对账幂等同一 CID 重发两次, 柜台只有一笔委托风控故意传错价格, 被偏离度规则拦下回报完整性跑满一天, 孤儿回报数为 0对账本地与柜台持仓、成交完全一致从那以后我每次接新柜台都强制先跑一遍这张表尤其是假死检测和幂等这两项——它们平时不显形一旦出问题就是重复下单或者静默丢单事后补都补不回来。希望这套拆法帮到你少走几段弯路。本文还有配套的精品资源点击获取