跳转到内容

创建自定义经纪商适配器

使用自定义经纪商适配器,将 AQE 接入交易执行场所。适配器负责网络传输、账户状态获取和订单执行。交易信号仍归策略管理;经纪商适配器接收的是不可变订单请求,而不是策略的完整交易信号树。

本指南以 AQE 0.2.0 接口为准。实现时,请遵循策略所用 AQE 版本中的 trait 定义。先使用模拟传输层或交易场所的沙盒,不要直接接入已入金的实盘账户。

创建实现 aq_engine::core::broker::traits::{Broker, OrderManagementProvider} 的 Rust 类型。该类型必须满足 Send + Sync,并持有自己的连接、缓存和事件队列。

Trait必需方法职责
Brokerconnect, disconnect, is_connected管理传输生命周期,即使连接尝试被取消,也要完成清理。
Brokerget_name, get_account_type准确标识适配器和账户环境。
OrderManagementProvidersubmit_order, cancel_order提交 BrokerOrderRequest,并按 ID 取消经纪商订单。
OrderManagementProviderupdate_take_profit, update_stop_loss使用传入的价格和数量修改保护设置。
OrderManagementProviderclose_position, close_all_positions请求平仓并管理剩余保护。
OrderManagementProviderget_orders, get_order返回带有真实经纪商标识和状态的 AQE Order 记录。
OrderManagementProviderget_positions, get_position返回 Position 记录;单个持仓方法接收品种代码。
OrderManagementProviderget_account返回包含余额、货币、购买力和限制的 Account。
OrderManagementProviderdrain_trade_events_bounded, has_pending_trade_events提供有序、非阻塞的事件队列。
OrderManagementProvidersubscribe_to_trade_stream, unsubscribe_from_trade_stream管理交易回调及其生命周期。

多个方法明确要求返回满足 Send 的 future,包括 connect、单笔订单变更方法、get_orders、get_positions 和 get_account。请严格遵循 trait 签名;只要返回的 future 满足 Send,就可以使用 async fn 实现。不要跨 .await 持有非 Send 的 guard,也不要在运行时工作线程上执行阻塞式网络传输。

对于不提供推送事件的交易场所,应规划如何通过轮询生成生命周期事件,并满足订阅接口的约定。除非经纪商确实能在 AQE 不轮询的情况下提供事件,否则不要声明 TradeEventStream。

重写 Broker::get_features(),返回由 BrokerFeature 值组成的静态切片。只声明适配器实际支持的功能,例如 MarketOrder、LimitOrder、PartialClose 或 BrokerManagedOco。如果场所支持对未成交订单进行对应的入场价格修改,请分别声明 AmendLimitPrice 和 AmendStopPrice。

购买力的报告口径应保持一致:BuyingPowerLeveraged 表示返回值已计入杠杆。将经纪商支持的上限映射到 Account.max_total_orders 和 Asset.max_total_size,将单笔订单数量上限放在 Asset.max_order_size 中。不要编造经纪商限制,也不要把仅凭通用名义金额公式计算的估算值当作权威结果。

对于不支持的操作,应在任何传输变更之前返回 BrokerError::UnsupportedOperation。不要静默替换为其他订单类型,也不要返回成功。

submit_order 接收 BrokerOrderRequest。将其品种代码、方向、数量、订单类型/类别、限价和止损触发价、保护价位及移动止损映射为场所支持的表示形式。

保留 client_order_id 用于关联,并在场所支持时将其用作客户端订单标识。同一入场意图在重试或恢复过程中,该值保持稳定;它不能证明场所已创建订单。在返回的 Order 中提供实际经纪商订单 ID。

区分以下结果:

结果适配器行为
发送前发现不支持返回 UnsupportedOperation;未发生变更。
在调用经纪商变更前已确认拒绝使用 SubmissionRejected 并附上原因。
发送后传输超时保留不确定性。不要声称已拒绝,也不要盲目再次提交。
请求已接受返回观测到的经纪商订单状态;接受本身不等于成交。
后续成交、拒绝、取消或平仓通过对应订单和事件发出生命周期证据。

replace_order 是可选的,默认返回不支持错误。需要替换待处理入场订单的价格/数量时,重写该方法。它不能替代 update_take_profit 或 update_stop_loss。

amend_entry_prices(order_id, limit_price, stop_price) 是另一个可选方法,用于在不改变数量的情况下,修改待处理入场订单的限价部分或止损触发价。默认返回不支持错误。在声明任一修改能力前,先实现该方法。不要将止损触发价修改转发给 update_stop_loss,后者控制的是保护性止损。

Rust 结构体字面量必须包含当前类型的每个字段,可选字段也不例外。#[serde(default)] 有助于读取旧版序列化记录,但并不意味着在 Rust 构造时可以省略该字段。

记录与字段适配器要求
Account.account_id, currency, accrued_commission, max_total_orders观测结果必须对应实际账户和币种。根据经纪商证据填写应计佣金,并在有报告时填写挂单数量上限。旧版默认值不能证明不存在费用或限制。
Position.order_id新增的可选字段,用于记录与该次入场关联的确切执行或持仓票据号。只有适配器能够证明关联关系时才使用 Some(...);对于旧版记录,或包含多个归属方的品种级汇总记录,应使用 None。绝不要根据品种代码编造标识。
Position.entry_commission单独保留持仓的入场佣金,不要混入未平仓交易盈亏。报告最终核算结果时,不要重复计入同一笔费用。
Order.filled_qty, realized_pnl, commission, swap入场快照使用累计入场成交量,退出事件则只使用该次退出实际成交的数量。保留经纪商报告的成交和现金数值;接受订单并不等于成交。
Order.position_accounting, OrderLifecycleCloseDetails.position_accounting可选的 PositionCloseAccounting 证据,用于能够证明归属的持仓。不支持或存在歧义时保留 None;仅实现订单提交并不要求提供此证据。
BrokerLifecycleSnapshot.snapshot_complete, captured_at只有完整枚举相关生命周期视图后,才能标记为完整,并保留观测时间。不完整的空快照不能证明订单或持仓已经消失。

构造 Order 时,除真实经纪商订单 ID 外,还应保留其交易信号和策略标识。随订单和持仓返回当前的 Asset 元数据,包括已知的交易规则、交易日历和费用。品种字段详见自定义数据源元数据。

get_position(symbol) 可能返回汇总视图。较新的 get_position_by_order_id(order_id, symbol) 执行异步精确查询,返回 Result<Option<Position>, BrokerError>。默认实现会检查 get_positions(),遇到订单标识缺失或存在歧义时拒绝查询,而不是任意选择同一品种的某个持仓。如果交易场所需要专门的票据查询,请重写此方法。

对于精确的内存模拟或自定义缓存,position_snapshot_reader 可返回 PositionSnapshotReader:一个拥有所有权、线程安全且接收订单 ID 和品种代码的回调。该回调不得进行网络传输。只有观测到持仓确实不存在时才返回 Ok(None);数据不可用或无法确定所请求的标识时,应返回错误。实时策略决策使用 AQE 带时间戳的快照缓存,而不是从交易信号回调中额外发起网络请求。

按来源顺序维护 (Order, TradeUpdateEvent) 队列。drain_trade_events_bounded(max_events) 最多移除指定数量的事件,其余事件保留在队列中。限制为零时,不移除任何事件。绝不要先清空全部事件,再截断返回的向量。

以下辅助函数演示队列操作,不限定使用哪种锁库。调用时,应仅短暂持有适配器队列的锁 guard:

use aq_engine::core::broker::types::{Order, TradeUpdateEvent};
use std::collections::VecDeque;
pub fn take_trade_events(
queue: &mut VecDeque<(Order, TradeUpdateEvent)>,
max_events: usize,
) -> Vec<(Order, TradeUpdateEvent)> {
let count = max_events.min(queue.len());
queue.drain(..count).collect()
}

has_pending_trade_events 不执行网络或数据库 I/O,只检查队列中是否有待处理事件。请在锁外调用回调,并缩短回调执行时间。准确发布部分成交和保护订单分支的更新,妥善处理供应商的重复或乱序消息,不要臆造终态。

默认的通用经纪商事件包装器会生成事件 ID,但无法确定事件在源端发生的时间。如果适配器能够提供可靠的 BrokerOrderEvent 标识、序列号和时间戳,请重写 subscribe_to_broker_event_stream 及相关事件封装处理。

close_position 负责清理保护订单:全部平仓时,取消经纪商端剩余的原生订单分支;部分平仓时,保留保护订单或根据剩余敞口调整其数量。Ok(true) 只表示命令已获确认,不表示已确认平仓。最终交易状态的证据应单独发送。

可选钩子可改善监督和恢复:

钩子何时实现
get_cached_account在不进行传输 I/O 的情况下返回最新账户快照。
get_cached_positions为确定性的内存执行提供无需网络传输的持仓数据;不能替代带时间戳的实时刷新。
get_cached_lifecycle_snapshot无需 I/O 即可提供完整权威生命周期视图;尚不存在时返回 None。
estimate_order_margin不实际下单,获取针对该具体请求的权威保证金估算。
reconcile_submission查询确切账户和客户端命令,而不重新提交。
reconcile_order_lifecycle重启后使用订单历史和持仓标识解析订单 ID。
lifecycle_reconciler提供拥有所有权的 Arc<dyn OrderLifecycleReconciler>,用于重启恢复。
get_position_by_order_id, position_snapshot_reader解析确切的入场票据号,将异步刷新与不进行网络传输的查询分开。
pending_cancellation_recovery, retry_cancellation通过持久化命令工作器,基于证据恢复结果不确定的取消操作。
execution_fill_history_reader为确切账户、入场订单和品种提供有界、只读的成交历史。
position_valuation_reader为确切的实际持仓提供以账户币种计价的只读估值,而不是品种级估算。
account_cash_flow_reader提供只读的账户资金划转和调整记录,包含来源标识、序列和覆盖范围。
configure_live_session, configure_asset_metadata在适配器需要时接收会话上下文和资产规则。

默认的提交结果核对方法返回 Unsupported。默认的生命周期核对方法使用 get_orders(),将缺失的订单 ID 报告为 Unknown;它无法确认已成交订单对应的持仓是否已平仓。不要将订单缺失、快照不完整或超时视为已取消或已平仓的证据。

pending_cancellation_recovery(account, order) 提供只读证据,表明结果不确定的取消操作仍指向待处理订单。它默认返回 None,这并不能证明订单已进入终态。只有持久化命令工作器在记录恢复尝试后,才可以调用 retry_cancellation。该钩子不得提交订单、平仓或替换订单;它必须获取权威的取消证据,并通过正常的经纪商事件归约器传递。默认实现不支持此操作。不要添加绕过此流程的独立重试定时器。

历史、估值和现金流读取钩子默认返回 None。执行适配器无法提供证据时,可以保留这些默认实现。不支持的分析数据必须保持不可用,不能伪造为空历史、无持仓的投资组合或零现金流。

  • ExecutionFillHistoryReader::read(account_id, entry_order_id, symbol) 为能够证明仅对应一个入场订单的持仓返回 ExecutionFillHistory。保留成交标识和来源时间;position_snapshot 可补充独立观测到的数量校验。历史缺失或经净额合并,并不等于有证据证明历史为空。
  • PositionValuationReader::read_positions(account_id, symbol, position_ids) 为有界的精确持仓批次返回 PositionValuations。保留账户币种、as_of_us、报价时间、交易盈亏、持仓期间的利息与费用,以及明确的名义金额计算口径或不可用原因。NotOpen 表示观测到持仓不存在,并非平仓成交,也不代表获准关闭交易信号。
  • AccountCashFlowReader::read(account_id, source_id, after_sequence) 返回 AccountCashFlowPage,包含来源标识、币种、时间覆盖范围、序列游标和记录。记录实际资金划转或调整;不要根据权益变动、成交或交易信号状态推断入金或出金。

这三种读取都在 Result 内返回可选结果。读取不可用和读取出错,都不同于权威且完整的观测确认没有匹配记录。读取分析数据时,绝不能附带下单或执行生命周期核对等副作用。

UnifiedBroker 可直接接收你的执行类型。以下辅助方法适用于任何符合要求的经纪商和数据源;两个适配器都必须已实现各自的 trait:

use aq_engine::core::broker::{
traits::{Broker, DataFeed, DataProvider, OrderManagementProvider},
UnifiedBroker,
};
pub fn live_with<E, D>(execution: E, datafeed: D) -> UnifiedBroker<E, D>
where
E: Broker + OrderManagementProvider,
D: DataFeed + DataProvider,
{
UnifiedBroker::new(execution, datafeed)
}

将 PaperBroker 与 UnifiedBroker::new_backtest 配合使用,即可使用内置历史模拟器。仅实现实时执行的经纪商 trait,并不会让远程交易场所具备该模拟器的能力。要让适配器出现在 AQS 连接选择器中,还需单独完成桌面端集成;实现 trait 不会自动获得此支持。

  1. 测试连接失败、取消、重连和完整关闭。
  2. 测试每种声明支持的订单类型、精度规则和容量限制。
  3. 验证部分成交、取消、保护更新、部分平仓和全部平仓清理。
  4. 分别使用零个、一个和多个事件测试有界队列提取;断言事件顺序和剩余事件均被保留。
  5. 模拟场所接受订单后的提交超时。确认恢复不会造成重复订单。
  6. 在有挂单和未平仓持仓时重启;验证未知结果与已确认结果始终明确区分。
  7. 考虑接入实时交易前,将获取的账户、订单和持仓数据与交易场所的沙盒记录逐一比对。
  8. 同一品种的两次入场应保持独立。测试精确票据查询、标识未知和快照不完整的情况,不要直接选择第一个品种匹配的持仓。
  9. 模拟跨重启恢复结果不确定的取消操作,并验证只有持久化恢复流程才可重试。
  10. 使用不支持、不完整、过期和属于其他账户的数据测试可选读取器。确认分析读取不能提交订单,也不能编造核算证据。

在 AQE 检出目录中,从以下文件开始:

  • src/core/broker/mod.rs:执行 trait、默认实现和组合。
  • src/core/broker/commands.rs:BrokerOrderRequest 和 SubmissionResolution。
  • src/core/broker/types/mod.rs:账户、订单、持仓、生命周期和错误类型。
  • src/core/broker/features.rs:原生能力标志。
  • src/core/broker/fill_history.rs、position_valuation.rs 和 account_cash_flow.rs:可选的只读分析接口约定。
  • live-sync-protocol/src/position_accounting.rs 和 position_valuation.rs:核算证据和估值验证。
  • src/core/broker/mt5_broker.rs 和 src/core/broker/paper_broker.rs:经纪商实时执行与模拟执行的实现。

下一步:创建自定义数据源或实时运行时。