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 转换流程:三步走

订单转换不是简单的字段映射。它包含三个步骤:

  1. 字段映射:把策略信号的字段名,映射到标准订单的字段名。比如策略里的"action": "buy" 要变成 "side": "BUY"。
  2. 数据校验:检查价格、数量是否合法。比如价格不能为负,数量不能超过持仓上限。
  3. 补充信息:生成order_id、填充时间戳、添加风控标记等。

我画了一张流程图,帮你理解这个过程:

订单转换流程 策略信号 买入100股茅台 字段映射 action → side 数据校验 价格 > 0?数量合法? 补充信息 生成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 小结

订单转换看起来简单,但细节很多。它就像翻译官,把策略的「方言」翻译成交易所的「普通话」。做得好,订单顺利执行;做不好,轻则拒单,重则造成交易事故。

我个人习惯在转换器里加一个「调试模式」。开发阶段打开它,可以看到每一步的转换细节。上线后关掉,保证性能。这个小功能帮我省了不少排查时间。

专注资料整理