
简介TradeX-TPS交易插件系统v1.0是一套面向股票量化交易与自动化下单场景的插件工具适用于熟悉大智慧、通达信公式体系希望将预警信号直接转化为实盘买卖操作的进阶用户。插件通过TradeX.dll或Trade.dll连接券商服务器可实现股票买入、卖出、撤单、按预设比例清仓并支持查询账户信息、持仓与可撤单信息同时兼容通达信、大智慧的股票池和预警公式系统。资源包共20个文件约14.44MB以9个dll动态库为核心配合ini配置、uid授权、xml参数、lic许可及exe主程序另附用户指南PDF与两平台安装说明结构完整、开箱可用。目前已有814人学习下载。标准版免费读者可据此快速搭建交易执行环境理解插件与券商接口的对接方式掌握预警到下单的完整链路并借助说明文档完成通达信、大智慧两端的部署与调试。1. TradeX-TPS交易插件系统 v1.0从零搭建一套可插拔的交易策略引擎很多做量化交易的朋友都遇到过这样的场景策略逻辑本身不复杂但每次想换一种下单方式、加一层风控、或者对接一个新的行情源就得把整个交易主程序翻个底朝天。改一处、崩三处回测和实盘的行为还不一致最后连自己都不敢确定线上跑的是哪个版本。TradeX-TPS交易插件系统 v1.0 要解决的就是这个问题——它把交易链路拆成可独立替换的插件模块让策略、风控、执行、行情各司其职通过统一接口拼装成完整系统。这套方案适合有一定 Python 基础、正在从单文件脚本向工程化交易系统过渡的开发者也适合需要快速验证多种策略组合的团队。下面我从架构设计讲到代码落地把能踩的坑一次说清楚。2. 插件化交易系统的架构选型与核心接口设计2.1 为什么选择插件化而不是继承式扩展传统交易系统常用继承来扩展功能基类定义on_tick、on_bar子类覆写。这种方式在策略数量少时没问题但一旦需要组合多个策略、动态切换风控规则继承树就会变得又深又脆。插件化的核心思路是组合优于继承每个功能单元实现同一套接口由引擎在运行时按配置加载和调用。TradeX-TPS 把系统拆成四类插件插件类型职责典型实现Strategy产生交易信号均线交叉、布林带突破Risk对信号做二次过滤最大回撤限制、单笔仓位上限Executor把信号转为订单限价单、市价单、TWAPDataFeed提供行情数据历史回放、实时推送这四类插件通过一个PluginBase抽象基类统一约束引擎只认接口不认具体实现。好处是你可以写一个只做日志记录的 Executor 来调试信号也可以写一个模拟撮合的 DataFeed 来做回测主引擎代码一行不用改。2.2 定义插件基类与事件总线先看基类定义。所有插件必须实现on_init、on_event、on_stop三个方法其中on_event是核心入口。# plugin_base.py from abc import ABC, abstractmethod from enum import Enum class EventType(Enum): TICK tick BAR bar SIGNAL signal ORDER order FILL fill class PluginBase(ABC): 所有插件的抽象基类引擎通过该接口调用插件 def __init__(self, name: str, config: dict): self.name name self.config config self._enabled True abstractmethod def on_init(self, context: dict): 插件加载时调用用于初始化内部状态 pass abstractmethod def on_event(self, event_type: EventType, payload: dict): 引擎分发事件时调用插件在此处理逻辑 pass def on_stop(self): 插件卸载时调用用于释放资源 pass def enable(self): self._enabled True def disable(self): self._enabled Falseon_init接收一个context字典里面包含引擎注入的共享资源比如订单管理器、持仓查询接口。on_event的payload是事件携带的数据不同event_type对应不同结构。_enabled标志让引擎可以临时禁用某个插件而不卸载它这在实盘中做紧急熔断很有用。事件总线负责把事件按顺序分发给订阅了该类型的插件。实现上用一个字典维护EventType - [插件列表]的映射分发时遍历列表依次调用。注意这里要处理插件抛异常的情况一个插件出错不能影响其他插件所以每个调用都包在 try/except 里记录日志后继续。# event_bus.py import logging from collections import defaultdict from plugin_base import EventType class EventBus: def __init__(self): self._subscribers defaultdict(list) self._logger logging.getLogger(EventBus) def subscribe(self, event_type: EventType, plugin): self._subscribers[event_type].append(plugin) def publish(self, event_type: EventType, payload: dict): for plugin in self._subscribers[event_type]: if not plugin._enabled: continue try: plugin.on_event(event_type, payload) except Exception as e: # 单个插件异常不影响其他插件 self._logger.error(fPlugin {plugin.name} error: {e}, exc_infoTrue)参数说明subscribe在插件加载阶段调用publish在引擎主循环中调用。defaultdict(list)保证未注册的事件类型返回空列表避免 KeyError。日志用exc_infoTrue保留堆栈方便排查是哪个插件在什么数据上出的错。2.3 插件加载器与配置驱动的装配流程插件加载器负责从配置文件读取插件列表动态导入模块并实例化。配置文件用 YAML 或 JSON 都行我一般用 YAML因为注释方便。# config.yaml plugins: - name: ma_cross type: strategy module: strategies.ma_cross class: MACrossStrategy params: fast_period: 5 slow_period: 20 - name: max_drawdown type: risk module: risk.max_drawdown class: MaxDrawdownRisk params: limit: 0.15 - name: limit_executor type: executor module: executors.limit class: LimitExecutor params: timeout_seconds: 3加载器代码# loader.py import importlib import yaml from plugin_base import PluginBase, EventType class PluginLoader: def __init__(self, bus): self.bus bus self.plugins [] def load_from_config(self, path: str): with open(path, r, encodingutf-8) as f: cfg yaml.safe_load(f) for item in cfg[plugins]: module importlib.import_module(item[module]) cls getattr(module, item[class]) instance cls(nameitem[name], configitem.get(params, {})) instance.on_init(context{}) self.plugins.append(instance) # 按类型订阅事件 ptype item[type] if ptype strategy: self.bus.subscribe(EventType.BAR, instance) elif ptype risk: self.bus.subscribe(EventType.SIGNAL, instance) elif ptype executor: self.bus.subscribe(EventType.ORDER, instance) elif ptype datafeed: self.bus.subscribe(EventType.TICK, instance) return self.pluginsimportlib.import_module按字符串路径导入模块getattr取出类对象。实例化后立即调用on_init让插件有机会建立数据库连接、加载模型文件等。订阅关系由type字段决定这样新增插件类型时只需在加载器里加一个分支不用改事件总线。注意on_init里不要做耗时超过 1 秒的操作否则会拖慢启动。如果确实需要加载大文件放到独立线程里异步初始化用状态标志控制on_event是否处理事件。3. 策略插件与风控插件的实现细节3.1 均线交叉策略插件的完整代码策略插件订阅BAR事件在on_event里维护价格序列计算均线产生SIGNAL事件。# strategies/ma_cross.py from collections import deque from plugin_base import PluginBase, EventType class MACrossStrategy(PluginBase): def on_init(self, context): self.fast self.config.get(fast_period, 5) self.slow self.config.get(slow_period, 20) self.prices deque(maxlenself.slow) self.last_signal None # 避免重复发信号 def on_event(self, event_type, payload): if event_type ! EventType.BAR: return close payload[close] self.prices.append(close) if len(self.prices) self.slow: return fast_ma sum(list(self.prices)[-self.fast:]) / self.fast slow_ma sum(self.prices) / self.slow signal None if fast_ma slow_ma and self.last_signal ! buy: signal buy elif fast_ma slow_ma and self.last_signal ! sell: signal sell if signal: self.last_signal signal # 通过引擎暴露的 publish 接口发出信号 self.context[bus].publish(EventType.SIGNAL, { strategy: self.name, action: signal, price: close, timestamp: payload[timestamp] })deque(maxlenself.slow)自动丢弃旧数据省去手动切片。last_signal用来去重均线交叉后可能连续多根 bar 保持同一方向不加这个判断会重复下单。信号通过context[bus]发布这个 bus 引用在on_init时由引擎注入。参数说明fast_period和slow_period是最核心的两个参数。经验上 fast 取 5 到 10slow 取 20 到 60。周期太短信号频繁手续费吃掉利润周期太长信号滞后错过入场点。建议先用回测跑参数网格找到夏普比率最高的区间再上实盘。3.2 风控插件如何拦截异常信号风控插件订阅SIGNAL事件对信号做过滤。如果信号不满足风控规则直接丢弃不向下游传递。# risk/max_drawdown.py from plugin_base import PluginBase, EventType class MaxDrawdownRisk(PluginBase): def on_init(self, context): self.limit self.config.get(limit, 0.15) self.peak_equity 0.0 self.current_equity 0.0 self.context context def on_event(self, event_type, payload): if event_type EventType.FILL: # 成交后更新权益 self.current_equity payload[equity] if self.current_equity self.peak_equity: self.peak_equity self.current_equity elif event_type EventType.SIGNAL: if self.peak_equity 0: return # 还没有权益数据放行 drawdown (self.peak_equity - self.current_equity) / self.peak_equity if drawdown self.limit: # 回撤超限拦截信号 self.context[logger].warning( fSignal blocked by drawdown: {drawdown:.2%} ) return # 风控通过转发给执行器 self.context[bus].publish(EventType.ORDER, { action: payload[action], price: payload[price], quantity: self._calc_position(payload[price]) }) def _calc_position(self, price): # 简单固定仓位每次用 10% 权益 return (self.current_equity * 0.1) / price这里有个关键设计风控插件既订阅SIGNAL又订阅FILL。FILL用来更新权益曲线SIGNAL用来做拦截。拦截逻辑是「回撤超过阈值就丢弃信号」而不是「平掉现有仓位」——平仓应该由独立的止损插件处理职责分离。_calc_position用固定比例仓位实际中可以根据波动率动态调整。注意current_equity初始为 0第一笔信号来的时候peak_equity也是 0所以加了if self.peak_equity 0: return放行避免启动阶段误拦截。3.3 执行器插件的订单生命周期管理执行器订阅ORDER事件负责把订单发到交易所或模拟撮合并在成交后发布FILL事件。# executors/limit.py import time from plugin_base import PluginBase, EventType class LimitExecutor(PluginBase): def on_init(self, context): self.timeout self.config.get(timeout_seconds, 3) self.pending {} # order_id - order_info self.context context def on_event(self, event_type, payload): if event_type EventType.ORDER: order_id self._gen_id() order { id: order_id, action: payload[action], price: payload[price], quantity: payload[quantity], created_at: time.time(), status: pending } self.pending[order_id] order self._send_to_exchange(order) elif event_type EventType.TICK: # 检查挂单是否超时 now time.time() for oid, order in list(self.pending.items()): if now - order[created_at] self.timeout: self._cancel(oid) def _send_to_exchange(self, order): # 实际对接交易所 API这里用日志模拟 self.context[logger].info(fOrder sent: {order}) def _cancel(self, order_id): order self.pending.pop(order_id, None) if order: self.context[logger].info(fOrder cancelled: {order_id}) def _gen_id(self): return ford_{int(time.time() * 1000)}pending字典跟踪所有未完成订单。TICK事件里检查超时超过timeout_seconds就撤单。这个超时机制在流动性差的市场里很重要挂单太久可能成交在不利价格。参数说明timeout_seconds默认 3 秒适合流动性好的品种。如果是小盘股或冷门合约可以放宽到 10 秒。但也不能太长否则信号失效后订单还挂着变成被动持仓。4. 回测与实盘的一致性保障避坑与排查4.1 时间戳对齐回测和实盘最大的差异来源现象同一套策略回测年化 30%实盘跑一周亏 5%。原因回测时 bar 的时间戳是 bar 结束时间实盘 tick 是实时到达。策略在on_event里用payload[timestamp]做判断时回测和实盘的时间语义不一致。比如回测里 10:00 的 bar 包含 9:59:00 到 10:00:00 的数据但实盘在 10:00:00 收到的 tick 只代表那一刻的行情。解决统一用「事件到达时间」而不是「数据时间」做逻辑判断。在引擎层给每个事件打上recv_time策略只读这个字段。回测时recv_time等于 bar 结束时间实盘时等于系统时间。这样策略代码不用改行为一致。# engine.py 片段 def _dispatch(self, event_type, payload): payload[recv_time] time.time() self.bus.publish(event_type, payload)4.2 插件加载顺序导致的空指针现象启动时报KeyError: bus某个插件在on_init里访问context[bus]失败。原因加载器按配置文件顺序实例化插件如果策略插件排在事件总线之前初始化context里还没有bus引用。解决加载器分两阶段先创建所有插件实例并注入context再调用on_init。这样无论配置顺序如何on_init执行时context已经完整。# loader.py 修正 def load_from_config(self, path): # 第一阶段实例化 instances [] for item in cfg[plugins]: module importlib.import_module(item[module]) cls getattr(module, item[class]) inst cls(nameitem[name], configitem.get(params, {})) instances.append((item, inst)) # 第二阶段注入 context 并初始化 context {bus: self.bus, logger: logging.getLogger(Engine)} for item, inst in instances: inst.context context inst.on_init(context) # 订阅事件...4.3 风控插件拦截后信号丢失现象开了风控之后某些本该成交的信号没有下单但日志里也没有风控拦截记录。原因风控插件在on_event里return之前没有打日志或者日志级别是 DEBUG 而生产环境只开 INFO。信号被静默丢弃排查时以为策略没产生信号。解决所有拦截分支必须打 WARNING 级别日志并带上足够上下文信号来源、当前回撤值、阈值。另外在引擎层加一个信号计数器记录「产生信号数 / 通过风控数 / 实际下单数」三个数字对不上就能快速定位是哪一层丢的。4.4 执行器重复下单现象同一根 bar 上发了两次同方向订单。原因策略插件的last_signal去重逻辑在插件重载时被重置。如果实盘中动态重载策略比如改参数on_init重新执行last_signal变回None下一根 bar 又发一次信号。解决把last_signal持久化到引擎的共享状态里而不是插件实例变量。或者重载时从引擎查询当前持仓方向用持仓方向做去重依据。更稳妥的做法是执行器层做幂等同一策略、同一方向、同一 bar 时间内只接受一次订单。4.5 回测数据前视偏差现象回测曲线完美实盘完全不是那么回事。原因策略在on_event里用了payload中未来才会知道的数据。比如 bar 的high、low、close在 bar 结束时才确定但回测框架可能在 bar 开始时就推送了包含完整 OHLC 的数据。解决回测数据源必须严格按时间顺序推送且每个 bar 只推送一次推送时机是 bar 结束。在 DataFeed 插件里加一个校验如果payload[timestamp]小于上一条数据的timestamp直接抛异常。这个校验在实盘时不会触发但能挡住回测数据源的错误。5. 插件热加载与参数动态调整的进阶技巧插件化系统最大的好处是可以在不重启引擎的情况下替换或调整插件。实现热加载的关键是状态迁移新插件实例需要继承旧实例的运行时状态否则持仓、挂单这些信息会丢失。我一般用「快照 恢复」的方式。每个插件实现snapshot()返回一个可序列化的字典实现restore(snapshot)从字典恢复状态。热加载时引擎先调用旧插件的snapshot()卸载旧插件加载新插件再调用新插件的restore()。# 在 PluginBase 中增加 def snapshot(self) - dict: 返回插件运行时状态用于热加载迁移 return {} def restore(self, state: dict): 从快照恢复状态 pass以均线策略为例# strategies/ma_cross.py 增加 def snapshot(self): return { prices: list(self.prices), last_signal: self.last_signal } def restore(self, state): self.prices deque(state[prices], maxlenself.slow) self.last_signal state[last_signal]热加载的触发方式有两种文件监听和信号触发。文件监听用watchdog库监控插件目录文件修改时自动重载。信号触发是注册一个系统信号比如SIGUSR1收到信号后重载指定插件。实盘中我更倾向信号触发因为文件监听在容器环境里有时不可靠而且自动重载可能在行情剧烈波动时引入意外行为。参数动态调整是热加载的简化版不替换插件实例只修改self.config里的值。但要注意有些参数在on_init时已经用来初始化内部结构比如deque的maxlen运行时改这些参数不会生效。所以参数分两类热参数如风控阈值、超时时间和冷参数如均线周期、队列长度。冷参数修改必须走热加载流程。验证热加载是否成功我习惯看三个指标重载前后持仓数量是否一致、挂单列表是否完整迁移、下一根 bar 的策略信号是否与不重载时相同。前两个靠日志对比第三个靠回放同一段行情做 A/B 测试。最后说一个血泪教训热加载一定要在模拟环境先跑通再上实盘。我曾经在实盘中直接热加载一个改了仓位计算逻辑的风控插件结果新插件restore时没恢复peak_equity导致回撤计算从零开始风控形同虚设半小时内多开了三倍仓位。从那以后我的习惯是任何热加载操作前先snapshot到文件出问题可以回滚。希望帮到你。本文还有配套的精品资源点击获取