4. 订单转换:将信号转换为标准订单格式
信号来了,然后呢?
很多新手会以为,策略发出「买入」信号,系统就直接去下单了。其实没那么简单。中间还有一道关键的工序——订单转换。说白了,就是把策略的「想法」翻译成交易所能理解的「语言」。
我刚开始做量化的时候,就吃过这个亏。策略跑得好好的,信号也准,结果订单格式不对,被交易所直接拒了。嗯,从那以后,我再也不敢小看这一步了。
核心观点:订单转换是策略逻辑与执行系统之间的桥梁。它负责将策略产生的原始信号(如“买入100股茅台”)转化为符合交易所或券商API规范的标准化订单结构。
4.1 信号长什么样?
先看看策略输出的信号。通常是一个结构体或字典,包含几个关键字段:
// 策略输出的原始信号示例(伪代码)
{
"symbol": "600519.SH", // 股票代码
"action": "buy", // 买卖方向
"price_type": "limit", // 价格类型:限价/市价
"price": 2050.00, // 目标价格
"quantity": 100, // 数量(股)
"strategy_id": "ma_cross_v1", // 策略标识
"timestamp": 1699000000 // 信号生成时间
}
这个结构很直观对吧?但交易所不认这个。交易所要的是固定长度的字节流,或者特定格式的JSON。所以我们需要做一层转换。
4.2 标准订单格式长什么样?
不同的交易所、不同的券商,订单格式千差万别。但万变不离其宗,核心字段就那么几个。我整理了一个通用的标准订单模型:
| 字段名 | 类型 | 说明 | 示例值 |
|---|---|---|---|
| order_id | string | 全局唯一订单号 | ORD_20231103_001 |
| client_id | string | 客户/账户标识 | ACCT_001 |
| symbol | string | 交易标的代码 | 600519.SH |
| side | enum | 买卖方向 | BUY / SELL |
| order_type | enum | 订单类型 | LIMIT / MARKET |
| price | double | 价格(限价单必填) | 2050.00 |
| quantity | int | 数量(股/手) | 100 |
| time_in_force | enum | 有效期 | DAY / IOC / GTC |
| strategy | string | 来源策略 | ma_cross_v1 |
| created_at | int64 | 创建时间戳 | 1699000000 |
你可能会问:「为什么要有order_id?策略信号里没有啊。」
没错,order_id是转换过程中生成的。它的作用可大了——用来追踪订单生命周期、去重、对账。我习惯用「时间戳+随机数+策略ID」的组合来生成,保证全局唯一。
4.3 转换流程:三步走
订单转换不是简单的字段映射。它包含三个步骤:
- 字段映射:把策略信号的字段名,映射到标准订单的字段名。比如策略里的"action": "buy" 要变成 "side": "BUY"。
- 数据校验:检查价格、数量是否合法。比如价格不能为负,数量不能超过持仓上限。
- 补充信息:生成order_id、填充时间戳、添加风控标记等。
我画了一张流程图,帮你理解这个过程:
4.4 代码实现:一个简单的转换器
下面是一个Python版本的订单转换器。它把策略信号转成标准订单格式,同时做了基本的校验。
class OrderConverter:
"""订单转换器:策略信号 → 标准订单"""
def __init__(self, client_id: str):
self.client_id = client_id
self.order_counter = 0
def convert(self, signal: dict) -> dict:
"""
将策略信号转换为标准订单
:param signal: 策略输出的原始信号
:return: 标准订单字典
"""
# 1. 字段映射
side_map = {"buy": "BUY", "sell": "SELL", "short": "SELL"}
type_map = {"limit": "LIMIT", "market": "MARKET", "stop": "STOP"}
order = {
"client_id": self.client_id,
"symbol": signal["symbol"],
"side": side_map.get(signal["action"], "BUY"),
"order_type": type_map.get(signal["price_type"], "LIMIT"),
"price": signal.get("price", 0.0),
"quantity": signal["quantity"],
"time_in_force": "DAY", # 默认当日有效
"strategy": signal.get("strategy_id", "unknown")
}
# 2. 数据校验
errors = self._validate(order)
if errors:
raise ValueError(f"订单校验失败: {errors}")
# 3. 补充信息
self.order_counter += 1
order["order_id"] = f"ORD_{int(time.time())}_{self.order_counter:04d}"
order["created_at"] = int(time.time())
return order
def _validate(self, order: dict) -> list:
"""校验订单字段合法性"""
errors = []
if order["quantity"] <= 0:
errors.append("数量必须大于0")
if order["order_type"] == "LIMIT" and order["price"] <= 0:
errors.append("限价单价格必须大于0")
if order["symbol"] not in self._valid_symbols():
errors.append(f"无效的交易标的: {order['symbol']}")
return errors
def _valid_symbols(self):
"""模拟可交易标的列表"""
return {"600519.SH", "000001.SZ", "AAPL.US"}
小技巧:我在实际项目中,会把校验逻辑单独抽成一个类。这样不同的策略可以复用同一套校验规则。另外,建议在转换时记录日志,方便排查问题。
4.5 避坑指南
做订单转换,有几个坑我踩过,分享给你:
- 精度问题:价格和数量的小数位数,不同交易所要求不同。A股价格精确到分,但美股可以到小数点后4位。我曾经因为精度问题,被拒单了十几次才反应过来。
- 订单ID重复:如果多个策略同时运行,订单ID可能冲突。我建议在ID里加上策略标识和进程ID。
- 时间戳时区:交易所服务器通常用UTC时间。如果你的策略用的是本地时间,转换时一定要做时区转换。否则订单可能被标记为「过期」。
- 数量单位:A股是「手」(1手=100股),但港股和美股是「股」。转换时要注意单位换算。
警告:千万不要在转换过程中修改原始信号!我见过有人直接在信号字典上改字段,结果导致其他模块读取到错误数据。正确的做法是:深拷贝一份,在新对象上做转换。
4.6 小结
订单转换看起来简单,但细节很多。它就像翻译官,把策略的「方言」翻译成交易所的「普通话」。做得好,订单顺利执行;做不好,轻则拒单,重则造成交易事故。
我个人习惯在转换器里加一个「调试模式」。开发阶段打开它,可以看到每一步的转换细节。上线后关掉,保证性能。这个小功能帮我省了不少排查时间。