@vibek/strategy 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/package.json +3 -1
- package/vibek-strategy-sdk.md +490 -0
- package/vibek-strategy-testing.md +463 -0
package/README.md
CHANGED
|
@@ -4,13 +4,11 @@ Vibek 的公开 Strategy SDK 与 Bun Testing helper。
|
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
|
-
Strategy SDK 与 `@vibek/cli` 使用同一版本线。策略工程必须精确依赖与所用 CLI 相同的版本,避免编译期契约和运行时不一致:
|
|
8
|
-
|
|
9
7
|
```bash
|
|
10
|
-
bun add
|
|
8
|
+
bun add @vibek/strategy@0.5.5
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
当前包版本为 `0.5.
|
|
11
|
+
当前包版本为 `0.5.5`;对应 CLI 应为 `@vibek/cli@0.5.5`。
|
|
14
12
|
|
|
15
13
|
## 使用
|
|
16
14
|
|
|
@@ -34,7 +32,6 @@ Bun 测试从独立入口导入:
|
|
|
34
32
|
import { setupStrategyTests } from '@vibek/strategy/testing';
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
生产策略只应从 `@vibek/strategy` 导入,不应依赖其他内部 `@vibek/*` 包。Testing helper 仅用于 Bun 测试,不应从生产策略入口导入。
|
|
38
35
|
|
|
39
36
|
## License
|
|
40
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibek/strategy",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Public Vibek strategy SDK and Bun testing helper",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"index.d.ts",
|
|
29
29
|
"testing.js",
|
|
30
30
|
"testing.d.ts",
|
|
31
|
+
"vibek-strategy-sdk.md",
|
|
32
|
+
"vibek-strategy-testing.md",
|
|
31
33
|
"README.md",
|
|
32
34
|
"LICENSE"
|
|
33
35
|
]
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# Vibek Strategy SDK 使用契约
|
|
2
|
+
|
|
3
|
+
本文面向使用 `@vibek/strategy` 的外部 Agent 和外部用户,可独立于 Vibek 内置 AI 工作流使用。Agent 编写或审查 `strategy.ts` 前应完整读取;用户可按章节查阅接口、运行规则和使用边界。
|
|
4
|
+
|
|
5
|
+
使用 `@vibek/strategy/testing` 编写测试时,参阅 [Testing 指南](vibek-strategy-testing.md)。
|
|
6
|
+
|
|
7
|
+
## 使用契约
|
|
8
|
+
|
|
9
|
+
| 来源 | 决定什么 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 用户明确的策略需求(下称策略规格) | 预期交易行为:信号、价格、数量、追价、重试、反转、风控和恢复决策 |
|
|
12
|
+
| 本文 | 公开 API、运行时保证和使用边界 |
|
|
13
|
+
| 示例 | 局部写法;其中的参数和行为不作为策略默认规则 |
|
|
14
|
+
|
|
15
|
+
先确认需求能由公开 API 表达。影响交易的规则缺失、材料冲突或能力不支持时,指出具体问题,由需求提出者明确规则;不要从当前策略代码、内部实现或记忆推导业务要求,也不要假定未公开 API 存在。
|
|
16
|
+
|
|
17
|
+
## 模块与文件边界
|
|
18
|
+
|
|
19
|
+
- 生产策略只从 `@vibek/strategy` 导入;禁止导入 Testing helper、其他 `@vibek/*` 包或内部相对路径。
|
|
20
|
+
- 文件只导出一个 `defineStrategy(...)` 返回值,使用默认导出。实例 ID、`vtSymbol`、`setting` 和 Paper/Live 模式由加载或回测时传入。
|
|
21
|
+
- 模块顶层只做声明和确定性初始化。策略虽是受信任的本机模块,也不应访问文件、网络、数据库、环境变量、凭证、CLI 或 CTP。
|
|
22
|
+
- 所有回调必须同步并快速返回。`onInit` 抛错或返回 Promise 会使初始化失败;运行期回调异常会使策略进入 fault、停止交易并触发活动委托清理。
|
|
23
|
+
- 使用严格 TypeScript;覆盖方法和静态字段加 `override`,不通过类型断言访问未公开字段。
|
|
24
|
+
|
|
25
|
+
公开运行时值只有 `CtaTemplate`、`defineStrategy`、`isActiveOrder`、`BarGenerator`、`parseTimeframe`。
|
|
26
|
+
|
|
27
|
+
公开类型:`Bar`、`TickData`、`EngineType`、`OrderStatus`、`OrderData`、`OrderErrorData`、`OrderEvent`、`OrderFailureCode`、`TradeData`、`ContractData`、`CommissionData`、`CommissionLeg`、`TimerEvent`、`TradesEvent`、`ClaimOrderResult`、`StrategyHook`、`StrategyHookData`、`StrategyHookValue`、`StrategyEvent`、`StrategyEventHandler`、`StrategyVariables`、`StrategyDefinition`、`StrategyParameterDescriptor`、`StrategyParameterKind`、`StrategyTimeframe`、`LoadBarsRequest`。
|
|
28
|
+
|
|
29
|
+
`CtaEngine`、运行上下文、实例绑定、Tick 聚合器、状态存储和 daemon 类型均不是策略 API。
|
|
30
|
+
|
|
31
|
+
### 合约和手续费元数据
|
|
32
|
+
|
|
33
|
+
`getContract(): ContractData` 返回当前实例绑定合约的只读标准化快照。数据由运行引擎在 `onInit` 前准备,两种 getter 都是同步读取,不发起柜台查询,也不能在构造函数或模块顶层使用。Live、Paper、回测和 Testing 使用同一字段语义。
|
|
34
|
+
|
|
35
|
+
| `ContractData` 字段 | 类型与含义 |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `vtSymbol` | `string`,大写完整合约代码,如 `RB2610.SHFE` |
|
|
38
|
+
| `instrumentID` / `exchangeID` / `productID` | `string`,大写合约、交易所、产品代码 |
|
|
39
|
+
| `name` | `string`,合约名称 |
|
|
40
|
+
| `priceTick` | `number`,最小价格跳动,有限正数 |
|
|
41
|
+
| `size` | `number`,每手合约乘数,正整数;成交金额为价格乘以 `size` 再乘以手数 |
|
|
42
|
+
| `minOrderVolume` / `maxOrderVolume` | `number`,单笔限价委托的最小/最大手数,正整数 |
|
|
43
|
+
| `longMarginRatio` / `shortMarginRatio` | `number`,多/空保证金比例,有限非负数,`0.1` 表示 10% |
|
|
44
|
+
| `isTrading` | `boolean`,合约是否可交易;不可交易合约拒绝初始化和启动 |
|
|
45
|
+
|
|
46
|
+
daemon 的保证金比例来自 CTP 合约规格,可能与期货公司对当前投资者实际收取的保证金不同,不能把它当作账户专属保证金率。
|
|
47
|
+
|
|
48
|
+
`getCommission(): CommissionData` 返回只读费率快照:`open`、`close` 和 `closeToday` 三个费用段,类型均为 `CommissionLeg`。每段的 `ratioByMoney: number` 是成交金额比例,`perLot: number` 是每手金额(普通人民币期货为元/手);两项均为有限非负数,可以同时非零。`close` 对应普通平仓费率,平昨使用此段;`closeToday` 对应平今费率,不自动合并为 `close`。
|
|
49
|
+
|
|
50
|
+
给定适用费用段 `leg`,估算手续费为 `price * getContract().size * volume * leg.ratioByMoney + volume * leg.perLot`。例如价格 3500、乘数 10、2 手,费率 `0.0001` 加每手 1 元,估算手续费为 9 元。接口仅提供费率,实际逐笔或累计手续费仍以账户和结算对账为准。
|
|
51
|
+
|
|
52
|
+
| 运行环境 | 数据来源与准备规则 |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| daemon Live / Paper | CTP 合约规格及当前账号手续费。优先采用合约费率,缺少时接受匹配产品费率;初始化和每次启动均检查准备结果,缺失、冲突或非法费率使操作失败,即使策略未调用 getter |
|
|
55
|
+
| 离线回测 | 本次回测的合约与六项手续费参数,与账户成本模型一致;名称使用合约代码,限价单数量边界为 `1` / `Number.MAX_SAFE_INTEGER`,`isTrading=true` |
|
|
56
|
+
| Testing | 来自 fixture;合约可选字段有明确默认值,手续费无默认值。不调用 `getCommission()` 的旧测试可省略 `commission`,调用时必须显式提供三个费用段 |
|
|
57
|
+
|
|
58
|
+
daemon 在同一次 TD 服务运行期间缓存元数据,临时断线恢复保留已准备的费率;TD 停止后清空手续费缓存,下次启动重新查询。仅停止再启动策略会复用 TD 缓存;需要刷新柜台费率时须重启 TD 服务或 daemon。停止或恢复期间未完成的旧查询不能写回缓存。
|
|
59
|
+
|
|
60
|
+
返回对象及手续费的三个费用段均为只读冻结对象;已取得的快照不会自动更新,不保证两次调用返回同一对象引用。合约或手续费未准备、合约不匹配或字段非法时,getter 抛错;未捕获的异常在 `onInit` 中导致初始化失败,在运行期回调中导致 fault。`getPriceTick()` 和 `getSize()` 已标记弃用并保留兼容,分别等价于 `getContract().priceTick` 和 `getContract().size`。
|
|
61
|
+
|
|
62
|
+
## 最小策略结构
|
|
63
|
+
|
|
64
|
+
此骨架可编译,只校验参数并登记预热,不主动下单。
|
|
65
|
+
|
|
66
|
+
~~~ts
|
|
67
|
+
import { CtaTemplate, defineStrategy, type Bar } from '@vibek/strategy';
|
|
68
|
+
|
|
69
|
+
class MyStrategy extends CtaTemplate {
|
|
70
|
+
static override readonly author = 'local';
|
|
71
|
+
static override readonly parameters = ['warmupCount'] as const;
|
|
72
|
+
static override readonly variables = [] as const;
|
|
73
|
+
|
|
74
|
+
warmupCount = 20;
|
|
75
|
+
|
|
76
|
+
override onInit(): void {
|
|
77
|
+
if (!Number.isInteger(this.warmupCount) || this.warmupCount <= 0) {
|
|
78
|
+
throw new Error('warmupCount 必须是正整数');
|
|
79
|
+
}
|
|
80
|
+
this.loadBars({ timeframe: '1min', count: this.warmupCount });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override onBar(_bar: Bar): void {
|
|
84
|
+
// 指标和缓存更新应放在 trading 检查之前。
|
|
85
|
+
if (!this.trading) return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default defineStrategy({ strategy: MyStrategy });
|
|
90
|
+
~~~
|
|
91
|
+
|
|
92
|
+
### 策略定义与元数据
|
|
93
|
+
|
|
94
|
+
`defineStrategy({ strategy: MyStrategy })` 返回策略定义;定义对象只接受 `strategy`,其值必须是继承 `CtaTemplate`、可无参构造的类。
|
|
95
|
+
|
|
96
|
+
| 静态字段 | 用途 |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `author` | 可选展示文字 |
|
|
99
|
+
| `parameters` | 允许 `setting` 覆盖的实例字段名;每个参数在 `onInit()` 中校验 |
|
|
100
|
+
| `variables` | 需要跨运行时重启恢复的自定义实例字段名 |
|
|
101
|
+
|
|
102
|
+
参数和变量均须有实例默认值;名称唯一,两张列表不能重叠,也不能占用框架字段或方法名。默认值及持久化变量必须是 JSON 值:`null`、字符串、布尔值、有限数字、数组或无循环引用的普通对象。禁止保存 `undefined`、非有限数字、bigint、函数、`Date`、`Map`、`Set` 或类实例。
|
|
103
|
+
|
|
104
|
+
框架只读维护以下字段:
|
|
105
|
+
|
|
106
|
+
| 字段 | 类型与含义 |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `strategyName` | `string`,当前实例 ID |
|
|
109
|
+
| `vtSymbol` | `string`,规范化完整合约 |
|
|
110
|
+
| `mode` | `EngineType = 'paper' \| 'live'` |
|
|
111
|
+
| `inited` | `boolean`,预热与状态恢复是否完成 |
|
|
112
|
+
| `trading` | `boolean`,当前是否允许发单 |
|
|
113
|
+
| `pos` | `number`,已成交净仓位,多为正、空为负;不是目标仓位 |
|
|
114
|
+
|
|
115
|
+
辅助类型:`StrategyDefinition` 只含 `strategy`;`StrategyVariables` 为 `Record<string, unknown>`。`StrategyParameterDescriptor` 的只读字段为 `name: string`、`defaultValue: unknown`、`kind: StrategyParameterKind`;`StrategyParameterKind = 'number' | 'boolean' | 'string' | 'json'`。
|
|
116
|
+
|
|
117
|
+
## 生命周期
|
|
118
|
+
|
|
119
|
+
构建与启动顺序固定:
|
|
120
|
+
|
|
121
|
+
| 顺序 | 阶段 | 状态与行为 |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| 1 | 无参构造、应用有效 `setting` | `inited=false`,`trading=false` |
|
|
124
|
+
| 2 | `onInit(): void` | 必须实现;校验参数、重建临时缓存、登记预热和事件订阅 |
|
|
125
|
+
| 3 | 回放预热 | 逐根闭合 `1min` Bar;仍不可交易 |
|
|
126
|
+
| 4 | 恢复 `variables` | 普通初始化恢复持久状态,随后设置 `inited=true` |
|
|
127
|
+
| 5 | `onStart(): void` | 调用时 `trading=false`,返回后才允许交易 |
|
|
128
|
+
|
|
129
|
+
初始化、预热、启动及停止回调均不得创建新委托。正常停止调用 `onStop(): void`,完整语义见“正常停止决策”。
|
|
130
|
+
|
|
131
|
+
`strategy reset` 用同一部署 bundle 和 setting 重新执行第 1—3 步,但跳过第 4 步的旧状态恢复,并把新实例状态原子写回。reset 要求 TD 正常、策略已停止且委托/成交回报已收齐,完成后仍不进入 trading;Live 还要求券商无活动委托且目标合约多空持仓均为零。导入、初始化、预热或落盘失败会保留旧实例和旧 state,历史成交不会被删除。
|
|
132
|
+
|
|
133
|
+
| 实时回调 | 输入与保证 |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `onTick(tick: TickData): void` | 绑定合约的 Tick;已初始化但停止时也可能收到 |
|
|
136
|
+
| `onBar(bar: Bar): void` | 绑定合约的闭合 `1min` Bar;默认也接收预热数据 |
|
|
137
|
+
| `onOrder(order: OrderEvent): void` | 委托状态变化或提交失败 |
|
|
138
|
+
| `onTrade(trade: TradeData): void` | 每笔成交;调用前 `pos` 和今昨库存已更新 |
|
|
139
|
+
|
|
140
|
+
一条市场 Tick 的处理顺序为:先确定该 Tick 闭合的源 Bar,并撮合 Tick 到达前已有的 Paper 委托,再依次调用 `onBar()`、`onTick()`。`onBar()` 使用触发它的 Runtime 当前时间,`bar.barTime` 仍是 Bar 起始时间。Live、Paper、回测和测试共用此时间线;Live 成交以券商回报为准。
|
|
141
|
+
|
|
142
|
+
其他合约的 Tick 可以推进时钟并关闭 Bar,但 Bar 按各自合约分发;其他合约的 Tick 不参与本策略的 Paper 撮合。
|
|
143
|
+
|
|
144
|
+
### Runtime 时间与事件注册
|
|
145
|
+
|
|
146
|
+
`getCurrentTime(): number` 返回 Runtime 当前 epoch 毫秒:daemon 为运行时间,回测为回放时间,测试为可控时间。超时、冷却和间隔判断只用 `getCurrentTime()`;不要用 `Date.now()`、`new Date()`、模块加载时间、sleep 或真实定时器。
|
|
147
|
+
|
|
148
|
+
时间单位:`Bar.barTime` 是起始 epoch 秒;Tick、订单、成交的 `datetime` 及事件的 `now` 是 epoch 毫秒。
|
|
149
|
+
|
|
150
|
+
`CtaTemplate` 提供以下注册方法;事件类型决定 handler 的参数类型:
|
|
151
|
+
|
|
152
|
+
~~~ts
|
|
153
|
+
registerEvent<T extends StrategyEvent['type']>(
|
|
154
|
+
eventType: T,
|
|
155
|
+
handler: StrategyEventHandler<T>,
|
|
156
|
+
): () => void;
|
|
157
|
+
~~~
|
|
158
|
+
|
|
159
|
+
`StrategyEvent = TimerEvent | TradesEvent`;`StrategyEventHandler<T>` 为 `(event: Extract<StrategyEvent, { type: T }>) => void`,`T` 默认是 `StrategyEvent['type']`。
|
|
160
|
+
|
|
161
|
+
- 只能在 `onInit()` 注册,返回的 disposer 可取消订阅。重启后重新注册;停止时保留,fault、卸载或 dispose 时清理。
|
|
162
|
+
- `registerEvent('timer', handler)` 订阅全局每秒一次的 Timer。`TimerEvent` 的只读字段为 `{ type: 'timer'; now: number }`,`now` 与 handler 内的 `getCurrentTime()` 一致。
|
|
163
|
+
- Live 使用 daemon 单一秒级时钟;Paper、回测和测试随 Runtime 时间前进,逐秒补发所有到期 Timer。同一时间点 Timer 先于市场事件入队。
|
|
164
|
+
- Timer 中提交的订单、排队执行的本地 `onOrder()` 及其派生同步回调继承该秒点的逻辑时间;一次补发多秒与逐秒推进的超时判断相同。
|
|
165
|
+
|
|
166
|
+
## 预热与行情
|
|
167
|
+
|
|
168
|
+
`loadBars(request: LoadBarsRequest, callback?: (bar: Bar) => void): void` 登记预热。`LoadBarsRequest` 的只读字段为 `{ timeframe: StrategyTimeframe; count: number }`。
|
|
169
|
+
|
|
170
|
+
- 只能在 `onInit()` 调用,每实例最多登记一次;`count` 必须是正整数,周期必须受支持。
|
|
171
|
+
- 请求的是目标周期数量,回调收到的始终是按时间排序的闭合 `1min` 源 Bar;默认回调为 `bar => this.onBar(bar)`。数据不足以形成所需目标 Bar 时初始化失败。
|
|
172
|
+
- `count` 是最低数量。回测保留触发上一窗口闭合的下一根源 Bar;它若也闭合自身窗口,会完整回放两个目标 Bar,因此结果可能多一根。预热源 Bar 不重复进入交易回放。
|
|
173
|
+
- 多周期策略按最长窗口请求足量源 Bar,再自行聚合;不要按固定每日交易分钟数换算日线。
|
|
174
|
+
- 当前仅支持 Bar 预热,没有 `loadTicks()`。历史 Tick 不落库,Tick 指标从启动后的第一条实时或回放 Tick 开始积累。
|
|
175
|
+
|
|
176
|
+
### 行情类型
|
|
177
|
+
|
|
178
|
+
~~~ts
|
|
179
|
+
interface Bar {
|
|
180
|
+
code: string;
|
|
181
|
+
timeframe: string;
|
|
182
|
+
barTime: number;
|
|
183
|
+
open: number;
|
|
184
|
+
high: number;
|
|
185
|
+
low: number;
|
|
186
|
+
close: number;
|
|
187
|
+
volume: number;
|
|
188
|
+
tradingDay?: string;
|
|
189
|
+
boundaryReason?: 'break' | 'sessionEnd' | 'disconnect';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface TickData {
|
|
193
|
+
code: string;
|
|
194
|
+
exchangeID?: string;
|
|
195
|
+
productID?: string;
|
|
196
|
+
priceTick?: number;
|
|
197
|
+
lastPrice: number;
|
|
198
|
+
updateTime: string;
|
|
199
|
+
updateMillisec: number;
|
|
200
|
+
actionDay?: string;
|
|
201
|
+
rawActionDay?: string;
|
|
202
|
+
tradingDay?: string;
|
|
203
|
+
volume: number;
|
|
204
|
+
datetime?: number;
|
|
205
|
+
receivedAt?: number;
|
|
206
|
+
dateSource?: 'actionDay' | 'receivedAt';
|
|
207
|
+
tradingDaySource?: 'quote' | 'mdLogin';
|
|
208
|
+
bidPrice1?: number;
|
|
209
|
+
bidVolume1?: number;
|
|
210
|
+
askPrice1?: number;
|
|
211
|
+
askVolume1?: number;
|
|
212
|
+
upperLimitPrice?: number;
|
|
213
|
+
lowerLimitPrice?: number;
|
|
214
|
+
}
|
|
215
|
+
~~~
|
|
216
|
+
|
|
217
|
+
一档报价、盘口量、涨跌停、交易日和时间等可选字段,使用前检查是否存在及数值是否有效。
|
|
218
|
+
|
|
219
|
+
### BarGenerator
|
|
220
|
+
|
|
221
|
+
`StrategyTimeframe` 支持 `1min`、`3min`、`5min`、`10min`、`15min`、`30min`、`1h`、`2h`、`4h`、`1d`。
|
|
222
|
+
|
|
223
|
+
| 接口 | 契约 |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| `new BarGenerator(timeframe: StrategyTimeframe)` | 构造目标周期聚合器;不支持的周期抛错 |
|
|
226
|
+
| `pushBars(bar: Bar): Bar[]` | 只输入闭合 `1min` Bar,遍历返回的全部目标 Bar |
|
|
227
|
+
| `pushBar(bar: Bar): Bar \| null` | 单次输入闭合多个窗口时抛错;通常用 `pushBars` |
|
|
228
|
+
| `flush(reason?: Bar['boundaryReason']): Bar \| null` | 显式关闭当前非空窗口 |
|
|
229
|
+
| `parseTimeframe(value: string): number` | 将正整数 `Nmin`、`Nh`、`Nd` 转为秒,格式或数量无效时抛错;解析成功不代表聚合器支持 |
|
|
230
|
+
|
|
231
|
+
分钟周期按墙钟边界闭合,任意 `boundaryReason` 也会结束窗口。小时和日线忽略 `break`、`disconnect`,在自然边界、`sessionEnd` 或交易日改变时闭合。日线优先使用 `tradingDay`,缺失时用 Runtime 日历规则。
|
|
232
|
+
|
|
233
|
+
局部范式:以下是策略类成员,需导入 `BarGenerator` 和 `type Bar`;只聚合并保留最近 20 根收盘价。
|
|
234
|
+
|
|
235
|
+
~~~ts
|
|
236
|
+
private generator!: BarGenerator;
|
|
237
|
+
private closes: number[] = [];
|
|
238
|
+
|
|
239
|
+
override onInit(): void {
|
|
240
|
+
this.generator = new BarGenerator('15min');
|
|
241
|
+
this.closes = [];
|
|
242
|
+
this.loadBars({ timeframe: '15min', count: 20 });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
override onBar(bar: Bar): void {
|
|
246
|
+
for (const windowBar of this.generator.pushBars(bar)) {
|
|
247
|
+
this.closes.push(windowBar.close);
|
|
248
|
+
if (this.closes.length > 20) this.closes.shift();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
~~~
|
|
252
|
+
|
|
253
|
+
### 行情断线
|
|
254
|
+
|
|
255
|
+
- 网络断线保留未闭合 `1min` Bar,不立即触发 `onBar()`。同分钟重连继续累积 OHLCV,按原分钟时钟或交易时段边界只闭合一次;实时策略与历史库使用同一根 Bar。
|
|
256
|
+
- 重连首条有效 Tick 只重建累计成交量基线;不补入无法归属分钟的成交量,也不推测缺失价格路径。长时间断线仍按时钟闭合已有非空分钟,不生成空 Bar;旧 Tick 不重开已闭合分钟。
|
|
257
|
+
- 受影响的源 Bar 标记 `boundaryReason='disconnect'`,实际 `break`、`sessionEnd` 优先。网关进程退出、服务停止按各自流程强制收尾。
|
|
258
|
+
|
|
259
|
+
## 下单、撤单与仓位
|
|
260
|
+
|
|
261
|
+
### 提交与成交
|
|
262
|
+
|
|
263
|
+
四个下单方法的签名均为 `(price: number, volume: number): string[]`:
|
|
264
|
+
|
|
265
|
+
| 方法 | direction | offset | 用途 |
|
|
266
|
+
| --- | --- | --- | --- |
|
|
267
|
+
| `buy` | `long` | `open` | 开多 |
|
|
268
|
+
| `sell` | `short` | `close` | 平多 |
|
|
269
|
+
| `short` | `short` | `open` | 开空 |
|
|
270
|
+
| `cover` | `long` | `close` | 平空 |
|
|
271
|
+
|
|
272
|
+
- `price` 必须是有限正数,按最小价位舍入;舍入后非有限或不大于零时以 `invalidRequest` 拒绝,不创建订单。`volume` 必须是正整数。
|
|
273
|
+
- 仅支持普通限价单,不提供市价、FAK、FOK 或有效期选项。可缓存最新 Tick,按规格选择有效卖一 `askPrice1` 买入或有效买一 `bidPrice1` 卖出;仍不保证立即或全部成交。缺失对手价时的处理由规格决定,`lastPrice` 不能视为必然可成交价格。
|
|
274
|
+
- 通用平仓可能按今昨仓拆为多个委托;提交后立即跟踪返回的每个 ID。SHFE/INE 可平数量涉及今昨库存,不能仅凭净 `pos` 推断。
|
|
275
|
+
- 只有成交回报改变 `pos` 和库存,一张订单可多次成交。实例只支持空仓、净多或净空;反转必须先平当前实际持仓,等成交后再开反向仓,不支持同时持有多空仓。
|
|
276
|
+
|
|
277
|
+
| 提交结果 | 返回值与事件 | 当日委托笔数 |
|
|
278
|
+
| --- | --- | --- |
|
|
279
|
+
| `trading=false` | `[]`,不进入提交路径 | 不增加 |
|
|
280
|
+
| 校验、执行限制、库存、风控、Paper 接受检查或 Live 通道在提交路径拒绝 | `[]`,`onOrder()` 收到 `type='error'` | 不增加 |
|
|
281
|
+
| 已取得订单 ID | 返回全部 ID,后续接收订单及成交回报;不代表成交 | 提交时立即增加,每笔只计一次;后续拒单也计数 |
|
|
282
|
+
|
|
283
|
+
同一回调内的后续开仓也受剩余委托额度限制。Live 已返回 ID 后发生报单 RPC 超时或连接异常,保留活动状态、归属和冻结数量,`statusMessage` 提示待确认;Runtime 先阻止实盘提交并查询券商,只有明确拒绝才产生 `rejected`。查询失败或无法确认活动订单时 fault 并保留未完成订单,需人工核查,不能因超时自动重发。
|
|
284
|
+
|
|
285
|
+
### 订单与成交类型
|
|
286
|
+
|
|
287
|
+
`onOrder()` 先用 `order.type === 'error'` 区分提交失败;错误事件没有 `vtOrderId`。已有 ID 后的异步柜台拒单是普通订单的 `status='rejected'`。
|
|
288
|
+
|
|
289
|
+
~~~ts
|
|
290
|
+
type OrderStatus =
|
|
291
|
+
| 'submitting' | 'notTraded' | 'partTraded'
|
|
292
|
+
| 'allTraded' | 'cancelled' | 'rejected';
|
|
293
|
+
|
|
294
|
+
type OrderFailureCode =
|
|
295
|
+
| 'invalidRequest' | 'maxOrderVolume' | 'maxPosition' | 'maxActiveOpenOrders'
|
|
296
|
+
| 'insufficientPosition' | 'riskRejected' | 'submitFailed' | 'paperRejected';
|
|
297
|
+
|
|
298
|
+
interface OrderData {
|
|
299
|
+
type?: 'order';
|
|
300
|
+
vtOrderId: string;
|
|
301
|
+
vtSymbol: string;
|
|
302
|
+
direction: 'long' | 'short';
|
|
303
|
+
offset: 'open' | 'close' | 'closeToday' | 'closeYesterday';
|
|
304
|
+
price: number;
|
|
305
|
+
volume: number;
|
|
306
|
+
traded: number;
|
|
307
|
+
status: OrderStatus;
|
|
308
|
+
statusMessage: string;
|
|
309
|
+
datetime: number;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface OrderErrorData {
|
|
313
|
+
type: 'error';
|
|
314
|
+
stage: 'submit';
|
|
315
|
+
code: OrderFailureCode;
|
|
316
|
+
vtSymbol: string;
|
|
317
|
+
direction: 'long' | 'short';
|
|
318
|
+
offset: 'open' | 'close' | 'closeToday' | 'closeYesterday';
|
|
319
|
+
price: number;
|
|
320
|
+
volume: number;
|
|
321
|
+
message: string;
|
|
322
|
+
datetime: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
type OrderEvent = OrderData | OrderErrorData;
|
|
326
|
+
|
|
327
|
+
interface TradeData {
|
|
328
|
+
vtTradeId: string;
|
|
329
|
+
vtOrderId: string;
|
|
330
|
+
vtSymbol: string;
|
|
331
|
+
direction: 'long' | 'short';
|
|
332
|
+
offset: 'open' | 'close' | 'closeToday' | 'closeYesterday';
|
|
333
|
+
price: number;
|
|
334
|
+
volume: number;
|
|
335
|
+
datetime: number;
|
|
336
|
+
tradingDay: string;
|
|
337
|
+
}
|
|
338
|
+
~~~
|
|
339
|
+
|
|
340
|
+
`OrderData.volume` 是委托总量,`traded` 是订单回报的累计成交量;`TradeData.volume` 是本笔成交量。
|
|
341
|
+
|
|
342
|
+
### 活动委托与撤单
|
|
343
|
+
|
|
344
|
+
`isActiveOrder(order: Pick<OrderData, 'status'>): boolean` 对 `submitting`、`notTraded`、`partTraded` 返回 `true`;`allTraded`、`cancelled`、`rejected` 为终态。
|
|
345
|
+
|
|
346
|
+
`cancelOrder(vtOrderId: string): void`、`cancelAll(): void` 在 `trading=false` 时无操作。撤单请求可能失败;保留活动 ID,等 `onOrder()` 终态确认后再移除或提交替代委托,不能把请求成功当作撤单成功。
|
|
347
|
+
|
|
348
|
+
局部范式:以下是类成员,需导入 `isActiveOrder` 和 `type OrderEvent`。集合在 `onInit()` 清空,下单返回的全部 ID 立即加入;回报处理也支持认领订单。
|
|
349
|
+
|
|
350
|
+
~~~ts
|
|
351
|
+
private activeOrderIds = new Set<string>();
|
|
352
|
+
|
|
353
|
+
override onOrder(order: OrderEvent): void {
|
|
354
|
+
if (order.type === 'error') {
|
|
355
|
+
this.writeLog(order.message);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (isActiveOrder(order)) this.activeOrderIds.add(order.vtOrderId);
|
|
359
|
+
else this.activeOrderIds.delete(order.vtOrderId);
|
|
360
|
+
}
|
|
361
|
+
~~~
|
|
362
|
+
|
|
363
|
+
此片段只维护活动集合;部分成交后的决策、失败重试与撤单请求状态仍须按规格实现。
|
|
364
|
+
|
|
365
|
+
### 正常停止决策
|
|
366
|
+
|
|
367
|
+
Paper 与 Live 的正常 `stop` 契约相同:
|
|
368
|
+
|
|
369
|
+
- 从交易态停止时,先设 `trading=false`,调用一次 `onStop()` 并保存状态;重复停止不重复调用。
|
|
370
|
+
- 保留挂单,不自动撤单、平仓或后台重试;框架展示剩余订单并提示用户手动撤销,重复停止仍反馈未处理订单。
|
|
371
|
+
- 保留实例和订单归属,继续接收订单及成交回报,更新 `pos`、今昨库存和持久化状态;停止后仍可能成交。
|
|
372
|
+
- 活动委托未终结,或终态报告的成交尚未收齐时,拒绝卸载或替换;处理完订单且回报收齐后可重试。卸载被拒绝时仍保留部署与行情订阅,但实例已停止并禁用自动启动。
|
|
373
|
+
- 成功卸载会从 Runtime 移除实例,并删除目标模式的 state 文件;持久化变量、持仓库存和交易日不会在下次加载时恢复。
|
|
374
|
+
|
|
375
|
+
手动撤单不依赖策略回调,入口见附录。fault、daemon 退出、交易会话关闭和测试 Runtime 完整重启执行各自的委托清理,不套用正常停止语义;Live 清理发起撤单也不保证撤单成功。
|
|
376
|
+
|
|
377
|
+
## Paper 模拟执行决策
|
|
378
|
+
|
|
379
|
+
Paper 用真实或回放行情执行确定的限价撮合,保留订单、持仓、风控、冻结库存、生命周期、事件和持久化契约。
|
|
380
|
+
|
|
381
|
+
| 条件 | 结果 |
|
|
382
|
+
| --- | --- |
|
|
383
|
+
| 新委托 | 只由提交后到达的 Tick 撮合;行情、订单或成交回调中的新单最早在下一条 Tick 成交 |
|
|
384
|
+
| 买单限价不低于有效卖一,或卖单限价不高于有效买一 | 默认按该对手价成交,允许优于限价 |
|
|
385
|
+
| 对手价缺失、非有限或非正数 | 保留挂单,不用 `lastPrice`、累计成交量或另一侧盘口推测成交 |
|
|
386
|
+
| 对应盘口量显式为零、负数或非有限数 | 等待;盘口量缺失时,有效对手价仍可撮合 |
|
|
387
|
+
| 满足成交条件 | 每个物理委托一次全成、一笔成交,不生成 `partTraded`;正数盘口量和 Tick 成交量不限制数量,同一 Tick 的多个委托不共享容量 |
|
|
388
|
+
| SHFE/INE 今昨仓拆单 | 各 ID 独立成交 |
|
|
389
|
+
| 撤单 | 本地确定完成,不模拟延迟或失败;订单事件按独立快照排队,提交后立即撤单仍分别记录,终态不回填此前事件 |
|
|
390
|
+
| 跨交易日未完成 | 到期撤销;事件保留原交易日,不占新交易日委托和撤单额度 |
|
|
391
|
+
|
|
392
|
+
正常停止后的挂单仍可撮合。手续费、滑点、保证金和强平由独立账户模型承担,离线回测使用 `BacktestAccount`;显式账户或测试成交价模型可调整价格,不改变普通委托一次全成的数量规则。
|
|
393
|
+
|
|
394
|
+
部分成交、异步拒单、撤单失败、迟到回报及断线恢复用 Fake-Live Broker 定向测试,Paper 不随机制造这些事件。需要处理部分成交的策略必须有对应 Fake-Live 测试;Paper 账户拒单及成交价测试钩子用于账户模型验证。测试用法见 [Testing 指南](vibek-strategy-testing.md)。
|
|
395
|
+
|
|
396
|
+
## 全局成交事件
|
|
397
|
+
|
|
398
|
+
在 `onInit()` 调用 `registerEvent('trades', handler)`;注册与清理规则见“Runtime 时间与事件注册”。
|
|
399
|
+
|
|
400
|
+
~~~ts
|
|
401
|
+
interface TradesEvent {
|
|
402
|
+
readonly type: 'trades';
|
|
403
|
+
readonly now: number;
|
|
404
|
+
readonly trade: Readonly<TradeData>;
|
|
405
|
+
readonly order: (Readonly<OrderData> & {
|
|
406
|
+
readonly origin: 'strategy' | 'external';
|
|
407
|
+
}) | null;
|
|
408
|
+
}
|
|
409
|
+
~~~
|
|
410
|
+
|
|
411
|
+
- Live 向当前账号所有 Live 订阅策略广播成交,覆盖其他合约、策略和外部订单;按 `trade.vtSymbol` 自行筛选。Paper 仅向同一 Runtime 的 Paper 订阅策略广播,Paper、回测、Live 成交流隔离。
|
|
412
|
+
- 每次一笔,按 `vtTradeId` 去重。初始化账户快照中的历史成交不回放;同进程重连补齐的未见成交可通知,但不提供首仓认领资格。
|
|
413
|
+
- 事件和快照只读;`now` 与 handler 内 `getCurrentTime()` 一致,`trade.datetime` 是成交时间。`order` 为最新已知回报,信息未到时为 `null`。
|
|
414
|
+
- `origin='strategy'` 表示本次 Runtime 策略提交,`external` 表示其他来源,包括手动终端;认领不改变原始来源。
|
|
415
|
+
- 订阅本身不改变库存或归属。已有归属的成交先更新库存并调用普通 `onTrade()`,再广播 `trades`;停止态订阅仍会收到通知。
|
|
416
|
+
|
|
417
|
+
### 成交后认领订单
|
|
418
|
+
|
|
419
|
+
`claimOrder(vtOrderId: string): ClaimOrderResult` 用于手动首笔开仓成交后托管整张订单。认领方向、数量及等待条件由策略规格决定。
|
|
420
|
+
|
|
421
|
+
| 前置条件 | 要求 |
|
|
422
|
+
| --- | --- |
|
|
423
|
+
| 调用位置 | 对应外部开仓订单的首笔新 `trades` handler 内;事件结束后不能补认领 |
|
|
424
|
+
| 策略 | 正在交易的 Live 实例,完整合约相同,无任何今昨多空库存,也无未结束订单或待收成交回报 |
|
|
425
|
+
| 订单 | 未被其他策略拥有;历史订单和重连补发成交无认领资格 |
|
|
426
|
+
|
|
427
|
+
`ClaimOrderResult` 返回 `{ readonly ok: true }` 或 `{ readonly ok: false; readonly reason: string }`。
|
|
428
|
+
|
|
429
|
+
- 成功返回前,本笔成交已且只计入库存一次,`pos` 同步更新;本次全局事件分发结束后依次投递普通 `onOrder()`、`onTrade()`,避免重入。
|
|
430
|
+
- 剩余订单及成交回报自动归属该策略。成交处理放在普通 `onTrade()`;活动 ID 在 `onOrder()` 建立和清理,因为外部首单没有下单返回值。
|
|
431
|
+
- 失败不改变归属、不下新单。在 Timer、行情、普通 `onTrade()` 或后续成交中调用均失败。Live 支持柜台外部订单;交互回测还允许在暂停或自动播放期间注入立即成交的外部订单,其首笔开仓成交可按同一规则认领。普通 daemon Paper 没有外部订单注入入口,不能借此伪造认领上下文。可用 Fake-Live 或交互回测测试此流程。
|
|
432
|
+
- 未成交订单不归策略管理。外部委托已绕过 Vibek 下单前风控,认领不会重新提交或撤回它。
|
|
433
|
+
|
|
434
|
+
## 状态与恢复
|
|
435
|
+
|
|
436
|
+
| 状态 | 所属与恢复方式 |
|
|
437
|
+
| --- | --- |
|
|
438
|
+
| 必须延续的控制状态,如阶段、冷却截止时间、有限重试预算 | 声明在 `variables`,预热后恢复 |
|
|
439
|
+
| 指标窗口、`BarGenerator` | 临时缓存,在 `onInit()` 重建并用预热填充 |
|
|
440
|
+
| 活动委托 ID、撤单请求集合 | 通常为临时状态,不放入 `variables`;活动委托不跨进程恢复 |
|
|
441
|
+
| 净持仓、当前方向今昨库存、交易日 | 框架独立保存;Paper 与 Live 状态隔离 |
|
|
442
|
+
|
|
443
|
+
- 普通回调后的变量异步防抖保存,成交和正常停止强制保存。测试防抖由可控时钟驱动,普通回调后推进到 500ms 完成保存,不依赖 sleep。
|
|
444
|
+
- 恢复后结合控制状态与实际库存,在下一次规格定义的决策事件重新计算,不盲目重发旧意图。
|
|
445
|
+
- 进程重启不会撤销交易所订单。Live 启动查询券商;有遗留活动委托或实际库存与恢复状态不一致时拒绝启动,须先处理券商状态。
|
|
446
|
+
- 订单和成交 ID 按不透明字符串处理。Paper ID 含每次构建的独立会话标识,替换或重启不复用旧 ID。
|
|
447
|
+
|
|
448
|
+
## 日志、Hook 与辅助方法
|
|
449
|
+
|
|
450
|
+
| 方法 | 契约 |
|
|
451
|
+
| --- | --- |
|
|
452
|
+
| `getContract(): ContractData` | 当前合约的只读标准化快照;元数据未准备或非法时抛错 |
|
|
453
|
+
| `getCommission(): CommissionData` | 当前账号的开仓、普通平仓和平今费率;费率未准备或非法时抛错 |
|
|
454
|
+
| `getPriceTick(): number` | `getContract().priceTick` 的兼容别名 |
|
|
455
|
+
| `getSize(): number` | `getContract().size` 的兼容别名 |
|
|
456
|
+
| `writeLog(message: string): void` | 写策略诊断日志;回测保存至结果 JSON 的 `diagnostics.logs`,并在 WebUI 回测报告展示 |
|
|
457
|
+
| `emitHook(name: string, data?: StrategyHookData): void` | 主动产生 `strategy.event` Hook;`data` 默认 `{}` |
|
|
458
|
+
|
|
459
|
+
旧的 `sendNotification()` 已移除。需要外部动作或通知时,策略应产生语义化 Hook,例如
|
|
460
|
+
`this.emitHook('signal.entry', { price })`,再由本机或远端 Hook Runner 的 Handler 决定是否发送消息。
|
|
461
|
+
|
|
462
|
+
策略规格中的日志规则应使用 `writeLog()` 约定策略特有生命周期、信号变化与关键值、当前和目标仓位、交易动作、异常及恢复。连续未变化的无动作决策不应逐 Bar 或 Tick 重复记录。日志和 Hook 不能代替订单、成交或状态机,也不得含敏感账户信息;直接调用 `console.log()` 不属于策略诊断日志。
|
|
463
|
+
|
|
464
|
+
`emitHook()` 的 `name` 必须匹配 `[A-Za-z0-9][A-Za-z0-9._-]{0,63}`。`data` 是只读语义的 JSON 对象:值可递归包含 `null`、字符串、布尔值、有限数字、数组和普通对象,不能包含 `undefined`、非有限数字、bigint、函数、稀疏数组、循环引用或类实例。Runtime 会复制数据并补充当前 `strategyId`、`mode`、`vtSymbol` 和 `occurredAt`;该时间来自策略运行时钟,后续修改原对象不会改变已产生的 Hook。
|
|
465
|
+
|
|
466
|
+
构造阶段、`onInit()` 和预热期间的调用会被忽略;初始化完成后不要求 `trading=true`,每次调用都产生独立事件,不去重或限流。名称、数据或完整协议帧不合法(单帧 JSON 上限 64 KiB)时同步抛错,未捕获时按普通回调异常使策略 fault。daemon 的 Paper/Live 将事件尽力广播给当时在线的 Hook Runner;无在线 Runner、背压或投递异常不会使策略 fault。Testing 将事件记录到 `runtime.hooks` 和 trace,离线回测记录到 `diagnostics.hooks`,不会启动 Runner 或执行 Handler。Handler 配置、stdin 和环境变量契约见 [Vibek Hooks](../hooks.md)。
|
|
467
|
+
|
|
468
|
+
## 编写检查表
|
|
469
|
+
|
|
470
|
+
- 参数均经校验;指标只使用当前及过去的闭合数据,可选行情字段已检查。
|
|
471
|
+
- 重复信号、空返回、部分成交、拒单、撤单失败和反转均有明确规格依据及对应验证。
|
|
472
|
+
- 跟踪全部订单 ID,按实际成交仓位决策,未把提交、撤单请求或停止当成订单终结。
|
|
473
|
+
- 使用 Runtime 时间;持久化字段满足 JSON 约束,恢复不重放过期意图。
|
|
474
|
+
- 生产文件只使用公开 SDK;测试依据用户明确的策略需求与预期结果。
|
|
475
|
+
|
|
476
|
+
## 附录:人工操作
|
|
477
|
+
|
|
478
|
+
### 停止后手动撤单
|
|
479
|
+
|
|
480
|
+
`strategy stop`、`strategy list` 展示剩余订单及撤单命令,并提示:“策略已停止,仍有 N 笔委托未撤销,可能继续成交,请手动撤单。”
|
|
481
|
+
|
|
482
|
+
~~~bash
|
|
483
|
+
vibek cancel <委托ID>
|
|
484
|
+
~~~
|
|
485
|
+
|
|
486
|
+
完整委托 ID 精确匹配,大小写保持原样,无需提供合约。Paper 从 `vibek strategy list` 获取 ID,停止态也可撤单,撤单在本地完成。Live 从 `vibek orders` 获取 ID,可撤销当前账号当前交易日的策略、手动和其他来源委托,不受手动交易合约配置限制;成功提交后提示“已请求撤单,以委托终态回报为准”,失败可再次手动请求。
|
|
487
|
+
|
|
488
|
+
订单不存在时报错;已终结的订单提示无需撤单,不再发送请求。账号不匹配或 Live 通道不可用时拒绝操作。Paper 订单只在当前 daemon 运行时可查找,Live 只处理当前交易日柜台订单,不支持历史订单撤销。
|
|
489
|
+
|
|
490
|
+
控制 API 快照保留 `activeOrderDetails` 和停止态 `warning`。新增 `POST /api/orders/<委托ID>/cancel`,委托 ID 需 URL 编码,并要求匹配当前账号的 `X-Vibek-Account`;成功响应为 `{ok:true,vtOrderId,mode,result}`,其中 `mode` 为 `paper` 或 `live`,`result` 为 `requested`(本次已执行 Paper 撤单或发送 Live 请求)或 `terminal`(已终结,无需撤单)。`vibek cancel <委托ID> --json` 在标准 `{ok:true,data,meta}` envelope 的 `data` 中返回这三个字段。原有 `POST /api/strategies/<id>/orders/<委托ID>/cancel` 仍保留,只处理指定策略拥有的订单;同合约 Paper 与 Live 并存时通过 `?mode=paper|live` 指定部署。策略的 start、stop 和 DELETE 控制接口也接受相同查询参数;仅有一个匹配部署时可省略,并存时省略会返回歧义错误。按合约批量撤单功能也保留。
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Vibek Strategy Testing 使用契约
|
|
2
|
+
|
|
3
|
+
本文面向使用 `bun:test` 和 `@vibek/strategy/testing` 的外部 Agent 与外部用户,可独立于 Vibek 内置 AI 工作流使用。Agent 编写或审查测试前应完整读取;用户可按章节查阅接口和运行方法。
|
|
4
|
+
|
|
5
|
+
生产行为以 [SDK 使用契约](vibek-strategy-sdk.md) 为准。Testing helper 在本地隔离 Runtime 中验证策略,不连接真实柜台。
|
|
6
|
+
|
|
7
|
+
## 使用契约
|
|
8
|
+
|
|
9
|
+
| 来源 | 决定什么 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 用户明确的策略需求(下称策略规格) | 场景输入、预期交易行为和断言 |
|
|
12
|
+
| SDK 与本文 | 生产语义、测试接口、时钟及回报规则 |
|
|
13
|
+
| `strategy.ts` | 被测对象;不能用其当前实现反推预期 |
|
|
14
|
+
| 示例 | 局部写法;前置条件和数值不作为策略默认规则 |
|
|
15
|
+
|
|
16
|
+
预期缺失或矛盾时,指出具体问题,由需求提出者明确规则;不为通过测试猜测预期或降低断言。测试验证可观察行为,不评价收益,也不替代 Paper 或 Live 环境验证。
|
|
17
|
+
|
|
18
|
+
测试文件约定:
|
|
19
|
+
|
|
20
|
+
- 使用 `tests/AC-001.test.ts` 等稳定编号,一个文件对应一个场景,测试名称以编号开头;同场景可包含多个输入组合,已有编号不复用。
|
|
21
|
+
- 只从 `bun:test`、`../strategy.ts`、`@vibek/strategy/testing` 导入;生产策略不得导入 Testing helper。
|
|
22
|
+
- 每个文件在模块顶层调用一次 `setupStrategyTests({ strategy, fixture })`;每个用例创建独立 Runtime,不使用 `test.concurrent`。
|
|
23
|
+
- 黑盒断言提交、成交、仓位和公开状态;不访问策略实例、私有字段、daemon 内部对象或文件。
|
|
24
|
+
- 每个业务用例须实际执行 `expect` 断言。空用例、跳过用例或其他文件的同编号用例不能算作覆盖。
|
|
25
|
+
|
|
26
|
+
## 最小测试结构
|
|
27
|
+
|
|
28
|
+
前提是已有 Bun/TypeScript 策略工程,能解析 SDK 和 Testing 入口。以下骨架搭建 fixture 与 Runtime;最后一行故意失败,须替换为该场景的输入事件与业务断言。
|
|
29
|
+
|
|
30
|
+
~~~ts
|
|
31
|
+
import { test } from 'bun:test';
|
|
32
|
+
import strategy from '../strategy.ts';
|
|
33
|
+
import { setupStrategyTests } from '@vibek/strategy/testing';
|
|
34
|
+
|
|
35
|
+
const testing = setupStrategyTests({
|
|
36
|
+
strategy,
|
|
37
|
+
fixture: {
|
|
38
|
+
vtSymbol: 'RB2610.SHFE',
|
|
39
|
+
instrument: { priceTick: 1, size: 10 },
|
|
40
|
+
seedPrice: 3500,
|
|
41
|
+
tradingDay: '20260105',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('AC-001 业务场景', async () => {
|
|
46
|
+
const runtime = testing.createRuntime();
|
|
47
|
+
await runtime.initialize();
|
|
48
|
+
await runtime.start();
|
|
49
|
+
|
|
50
|
+
throw new Error('请按策略需求补充输入事件与业务断言');
|
|
51
|
+
});
|
|
52
|
+
~~~
|
|
53
|
+
|
|
54
|
+
### 自动 SDK 契约
|
|
55
|
+
|
|
56
|
+
`setupStrategyTests` 返回 `StrategyTestHelper`,并自动注册以下五项测试及每个用例结束后的 Runtime 清理:
|
|
57
|
+
|
|
58
|
+
| 契约 | 检查内容 |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| 定义 | 默认导出、策略类、参数和默认值 |
|
|
61
|
+
| Paper 生命周期 | 初始化、启动、行情回调和停止 |
|
|
62
|
+
| Fake-Live 生命周期 | 同上,使用模拟券商 |
|
|
63
|
+
| 持久化与重启 | 正常重启后的仓位和变量 |
|
|
64
|
+
| 确定性回放 | Paper 与 Fake-Live 中行情、Timer、成交、Hook、停止、重启的完整结果 |
|
|
65
|
+
|
|
66
|
+
生命周期检查只计启动后注入行情引起的 `onTick`、`onBar` 增量,不计预热回调。回放比较提交、撤单、订单、成交、Hook、日志、库存和变量;业务测试补充规格特有分支,不重复注册这些契约。
|
|
67
|
+
|
|
68
|
+
## 配置与创建 Runtime
|
|
69
|
+
|
|
70
|
+
`setupStrategyTests({ strategy, fixture })` 接受策略默认导出与共享 fixture。
|
|
71
|
+
|
|
72
|
+
| fixture 字段 | 类型与要求 |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `vtSymbol` | 完整合约字符串,如 `RB2610.SHFE` |
|
|
75
|
+
| `instrument.priceTick` | 有限正数 |
|
|
76
|
+
| `instrument.size` | 正整数合约乘数 |
|
|
77
|
+
| `instrument.name` | 可选合约名称,默认使用合约代码 |
|
|
78
|
+
| `instrument.minOrderVolume` / `maxOrderVolume` | 可选限价单数量边界(手),正整数且最大值不小于最小值,默认 `1` / `100000` |
|
|
79
|
+
| `instrument.longMarginRatio` / `shortMarginRatio` | 可选多空保证金比例,有限非负数,默认 `0.1`(10%) |
|
|
80
|
+
| `instrument.isTrading` | 可选可交易状态,默认 `true`;为 `false` 时初始化和启动均被拒绝 |
|
|
81
|
+
| `commission` | 可选当前账号手续费快照,包含 `open`、`close`、`closeToday`,每段为 `{ ratioByMoney, perLot }`;策略调用 `getCommission()` 时必须提供 |
|
|
82
|
+
| `setting` | 可选 JSON 参数对象,键和值由策略 `parameters` 推导 |
|
|
83
|
+
| `seedPrice` | 可选有限正数,默认 `3500` |
|
|
84
|
+
| `tradingDay` | 可选 `YYYYMMDD`,默认 `20260105` |
|
|
85
|
+
|
|
86
|
+
`commission` 没有默认费率。显式配置时必须提供三个完整费用段,每段两项都必须是有限非负数;零手续费也需显式填写 `0`。按金额费率使用比例值,例如 `0.0001` 表示万分之一,`perLot` 为元/手。这些元数据在 `onInit()` 中即可读取,Paper、脚本化 Live 和 `execution: 'ctp'` 均适用,场景导出与回放会保留它们。配置费率只为 `getCommission()` 提供数据,不会给 Testing 自动添加手续费扣款或账户核算。
|
|
87
|
+
|
|
88
|
+
helper 方法:
|
|
89
|
+
|
|
90
|
+
| 方法 | 返回与用途 |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `createRuntime(options?)` | `StrategyTestRuntime`,每用例独立创建 |
|
|
93
|
+
| `bar(value)` | 补齐 `Bar`;`value` 为 `Partial<Bar> & Pick<Bar, 'close'>` |
|
|
94
|
+
| `tick(value)` | 补齐 `TickData`;`value` 为 `Partial<TickData> & Pick<TickData, 'lastPrice'>` |
|
|
95
|
+
|
|
96
|
+
`bar()`、`tick()` 只构造数据,不交付行情。`StrategyTestFixture`、`StrategySettingFor`、`StrategyTestHelper` 均可从 Testing 入口导入,通常依靠函数推导即可。
|
|
97
|
+
|
|
98
|
+
### Runtime 选项
|
|
99
|
+
|
|
100
|
+
`createRuntime(options?: StrategyTestRuntimeOptions)` 的选项均可省略:
|
|
101
|
+
|
|
102
|
+
| 选项 | 默认或行为 |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `mode` | `'live'`;只有验证 Paper 撮合时使用 `'paper'` |
|
|
105
|
+
| `execution` | `'scripted'`,使用可控 Fake-Live Broker;`'ctp'` 仅与 Live 组合,使用离线内存网关 |
|
|
106
|
+
| `setting` | 替换 fixture 的整份 `setting`;未写入的参数回到策略类默认值,不与 fixture 合并 |
|
|
107
|
+
| `initialState` | `StrategyTestInitialState`,注入已持久化变量及库存 |
|
|
108
|
+
| `brokerState` | `StrategyTestReconciliation`,独立设置 Scripted Broker 状态;省略时沿用持久化库存 |
|
|
109
|
+
| `gatewayState`、`gatewaySession` | CTP 模式的查询快照与会话,见“原始 CTP 网关” |
|
|
110
|
+
| `executionLimits`、`riskLimits` | 覆盖执行及风控上限 |
|
|
111
|
+
| `paperExecution` | Paper 账户拒单、成交价及回调钩子 |
|
|
112
|
+
|
|
113
|
+
`initialState.variables` 是声明变量的 JSON 对象。`initialState.inventory` 可指定 `longToday`、`longYesterday`、`shortToday`、`shortYesterday`,均为非负整数,缺失项为零;`pos` 按多仓减空仓计算。库存完整类型为 `StrategyTestPositionInventory`。
|
|
114
|
+
|
|
115
|
+
`createStrategyTest({ strategy, fixture }, options?)` 创建同样的独立 Runtime,但不注册 Bun 契约或清理钩子;调用方须在 `finally` 中 `await runtime.dispose()`。
|
|
116
|
+
|
|
117
|
+
### 执行限制与 Paper 控制
|
|
118
|
+
|
|
119
|
+
| 配置 | 字段 |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `executionLimits` | `maxOrderVolume`、`maxPosition`、`maxActiveOpenOrders`,按需覆盖 |
|
|
122
|
+
| `riskLimits.dailyLimits` | `totalOrderLimit`、`totalCancelLimit`、`totalTradeLimit`、`instrumentOrderLimit`、`instrumentCancelLimit`、`instrumentTradeLimit`,按需覆盖 |
|
|
123
|
+
| `riskLimits.duplicateOrderLimit` | 重复报单预算 |
|
|
124
|
+
|
|
125
|
+
对应类型为 `StrategyTestExecutionLimits`、`StrategyTestDailyRiskLimits`、`StrategyTestRiskLimits`。设置较低上限可覆盖风控边界。
|
|
126
|
+
|
|
127
|
+
`StrategyTestPaperExecution` 的可选同步钩子如下,输入订单、Tick、成交均只读:
|
|
128
|
+
|
|
129
|
+
| 钩子 | 签名与用途 |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `acceptOrder` | `(order) => { ok: true } \| { ok: false; reason: string }`,控制接受或拒单 |
|
|
132
|
+
| `fillOrder` | `(order, tick, volume) => { referencePrice: number; price: number }`,调整成交价 |
|
|
133
|
+
| `onTrade` | `(trade) => void`,观察成交 |
|
|
134
|
+
| `onOrderClosed` | `(order) => void`,观察委托终结 |
|
|
135
|
+
|
|
136
|
+
Paper 遵循 [SDK 撮合规则](vibek-strategy-sdk.md#paper-模拟执行决策):提交后的 Tick 有有效对手价且达到限价时一次全成;盘口量显式为零或非法时等待,缺失时只依据有效价格。正数盘口量和累计成交量不产生部分成交或总容量限制,价格钩子也不改变数量规则。
|
|
137
|
+
|
|
138
|
+
## 生命周期与 fault
|
|
139
|
+
|
|
140
|
+
在行情输入、快照或 Broker 控制前等待 `initialize()`。下表中异步方法必须 `await`:
|
|
141
|
+
|
|
142
|
+
| 方法 | 返回 | 行为 |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `initialize({ warmupBars? }?)` | `Promise<void>` | 每 Runtime 一次:构造、`onInit`、预热、恢复状态 |
|
|
145
|
+
| `start()` | `Promise<void>` | `onStart` 返回后启用交易 |
|
|
146
|
+
| `stop()` | `Promise<void>` | 先关闭交易,再调用一次 `onStop` 并保存;保留挂单,继续接收回报 |
|
|
147
|
+
| `cancelOrder(id: string)` | `void` | 手动请求撤单,停止态也可调用;Live 仍需显式注入回报 |
|
|
148
|
+
| `restart()` | `Promise<void>` | 正常停止、请求撤单、保存并重建 |
|
|
149
|
+
| `crashAndRestart()` | `Promise<void>` | 跳过 `onStop`、撤单和最后保存,再重建 |
|
|
150
|
+
| `expectFault(message: string)` | `void` | 精确验证一次已发生的 fault;不存在、文本不符或重复验证均失败 |
|
|
151
|
+
| `dispose()` | `Promise<void>` | 释放资源,拒绝任何未验证 fault;helper 自动调用 |
|
|
152
|
+
|
|
153
|
+
- 初始化与预热时 `trading=false`,不应产生提交。重启完成后也为 `false`,须再次 `start()` 才能交易。
|
|
154
|
+
- 重复停止不重复调用 `onStop()`,但仍警告未撤销订单。`onStop()` 中 SDK 下单、撤单方法均无操作;测试侧 `runtime.cancelOrder()` 是独立的手动入口。
|
|
155
|
+
- 停止不代表撤单成功:Paper 挂单仍可被后续 Tick 撮合;Live 需注入 `broker.cancel(id)` 或相应原始回报。
|
|
156
|
+
- 负向场景用 `expectFault('完整 fault 文本')` 验证故障。只断言“没有下单”不证明运行正常;旧 fault 跨重启保留,即使快照已清空,未验证故障仍会使清理失败。
|
|
157
|
+
|
|
158
|
+
## 行情与确定性时间
|
|
159
|
+
|
|
160
|
+
### 输入接口
|
|
161
|
+
|
|
162
|
+
以下方法返回 `void`:
|
|
163
|
+
|
|
164
|
+
| 方法 | 行为 |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `setTime(epochMilliseconds: number)` | 初始化前设置时钟基准;初始化后沿统一时钟路径推进,不能回退 |
|
|
167
|
+
| `advanceTime(milliseconds: number)` | 推进非负整数毫秒,执行到期 Timer、Bar 闭合和持久化任务 |
|
|
168
|
+
| `pushTick(value)` | 输入 Tick 并自动聚合 `1min` Bar;`value` 至少包含 `lastPrice` |
|
|
169
|
+
| `pushMarketEvent(value, closedBars: readonly Bar[])` | 输入 Tick 及其闭合源 Bar;相同时间的自动 Bar 与显式 Bar 合并,显式值优先,只回调一次 |
|
|
170
|
+
| `pushBar(value, { datetime? }?)` | 单独输入闭合 Bar,不创建 Tick;`value` 至少包含 `close` |
|
|
171
|
+
|
|
172
|
+
`pushTick`、`pushMarketEvent` 的 `value` 类型与 `testing.tick()` 相同;`pushBar` 与 `testing.bar()` 相同。构造器拒绝非法价格、数量、日期、周期和 OHLC 关系。盘口价省略时默认使用 `lastPrice`,盘口量默认 `1000`;显式传入 `askPrice1: undefined` 或 `bidPrice1: undefined` 才保留缺失盘口。
|
|
173
|
+
|
|
174
|
+
### 时间与交付顺序
|
|
175
|
+
|
|
176
|
+
- 默认时钟是 fixture 交易日北京时间 09:00。连续行情未给显式时间时,每次默认前进一分钟。
|
|
177
|
+
- `tick.datetime` 为行情 epoch 毫秒;`receivedAt` 为实际交付毫秒,省略时与 `datetime` 相同,须在行情时间之后 30 秒以内。交付时间严格禁止毫秒级回退;聚合使用行情时间,回调使用交付时间。
|
|
178
|
+
- `bar.barTime` 为起始 epoch 秒;`pushBar` 默认交付时间是 `(barTime + 60) * 1000`,可用 `datetime` 指定更晚时间。省略 `barTime` 时生成隐式事件时刻之前刚闭合的分钟,并确保晚于预热历史。
|
|
179
|
+
- 同一时刻先执行 Timer 与到期的时钟 Bar,再按登记顺序执行到期任务,最后交付直接输入的行情或回报。Tick 推进也处理途中所有到期事件。
|
|
180
|
+
- `pushTick` 先撮合旧 Paper 委托,再调用闭合 Bar 的 `onBar()`,最后调用 `onTick()`;这些回调中的新单最早在下一条 Tick 撮合。
|
|
181
|
+
- 没有下一条 Tick 时,时钟按生产的 30 秒迟到窗口闭合已有非空分钟;例如 09:00 Bar 在 09:01:30 闭合。不伪造 Tick 或空 Bar。
|
|
182
|
+
- 显式 Bar 须属于 fixture 合约、严格递增且已闭合;不能替换已由时钟交付的 Bar,也不能超过其自动闭合期限再替换,非法输入在推进时钟前拒绝。
|
|
183
|
+
- 时钟同时控制每秒 Timer、普通回调后 500ms 的状态保存和风控保存。策略的 Timer 停止时仍会触发,行情决策则需输入规格指定的事件。
|
|
184
|
+
|
|
185
|
+
策略内部用 `getCurrentTime()`,测试侧用 `runtime.snapshot().now` 观察交付时刻;不能拿 `barTime * 1000` 代替它。不 sleep、不等待真实时间,也不缩短生产超时参数来加速测试;发现策略依赖墙钟应记录为生产缺陷。
|
|
186
|
+
|
|
187
|
+
局部范式:假设当前时刻已提交 `orderId`,规格要求在 Tick 中判断 `timeoutMs` 到期并撤单,且没有其他撤单。时间和订单 ID 均来自本用例:
|
|
188
|
+
|
|
189
|
+
~~~ts
|
|
190
|
+
const submittedAt = runtime.snapshot().now;
|
|
191
|
+
runtime.pushTick({ lastPrice: 3500, datetime: submittedAt + timeoutMs - 1 });
|
|
192
|
+
expect(runtime.cancelRequests).toEqual([]);
|
|
193
|
+
|
|
194
|
+
runtime.pushTick({ lastPrice: 3500, datetime: submittedAt + timeoutMs });
|
|
195
|
+
expect(runtime.cancelRequests).toEqual([orderId]);
|
|
196
|
+
~~~
|
|
197
|
+
|
|
198
|
+
验证 Timer 自行触发的超时时,用 `advanceTime()` 推进到对应秒点并断言,不额外输入行情。
|
|
199
|
+
|
|
200
|
+
## 预热与恢复
|
|
201
|
+
|
|
202
|
+
### 预热数据
|
|
203
|
+
|
|
204
|
+
- `initialize()` 根据策略的 `loadBars({ timeframe, count })` 自动生成当前时刻之前的足量闭合源 Bar。需要精确指标、缺口或周期边界时传入 `initialize({ warmupBars })`。
|
|
205
|
+
- 显式数组用 `testing.bar()` 构造,须有足量闭合 `1min` Bar、时间严格递增、合约匹配;未来或重复时间数据会被拒绝。历史较晚时先 `setTime()` 再初始化。
|
|
206
|
+
- 多余历史按生产规则选取最新目标周期后缀,保留末尾未组成完整高周期的源 Bar。显式 `[]` 表示没有历史,不自动补数据;目标 Bar 不足时初始化失败。
|
|
207
|
+
- 高周期输入明确设置 `barTime`、`tradingDay` 和必要的 `boundaryReason`,不假设固定日内分钟数。重复登记 `loadBars()` 会使初始化失败。
|
|
208
|
+
- 预热可重建指标并更新变量,但不提交订单;持久化变量在预热后恢复。重启使用本 Runtime 已收到的闭合行情历史重建最新缓存。
|
|
209
|
+
|
|
210
|
+
### 重启边界
|
|
211
|
+
|
|
212
|
+
| 状态 | `restart()` | `crashAndRestart()` |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| 停止通知、撤单、最后保存 | 执行;Live 撤单成功仍需显式回报 | 均跳过 |
|
|
215
|
+
| 策略恢复来源 | 本次保存的仓位与变量 | 之前已落盘的仓位与变量,未落盘修改丢失 |
|
|
216
|
+
| 券商订单、成交和实际库存 | 保留 | 保留 |
|
|
217
|
+
| 已持久化风控计数及重复报单预算 | 保留;Live 初始化还从券商快照恢复 | 同左 |
|
|
218
|
+
|
|
219
|
+
Live 启动时若仍有活动委托,或券商库存与恢复状态不一致,`start()` 拒绝。先处理券商状态,再测试下一步;进程崩溃不代表交易所撤单。
|
|
220
|
+
|
|
221
|
+
同一 Runtime 重启前后的订单及成交 ID 不复用;独立 Runtime 使用确定性 ID,便于比较相同输入。规格依赖控制状态时,同时覆盖正常与崩溃恢复,并验证下一决策事件到来前不重发过期意图。
|
|
222
|
+
|
|
223
|
+
## 可控 Fake-Live Broker
|
|
224
|
+
|
|
225
|
+
`runtime.broker` 的类型为 `StrategyTestBroker`,仅用于 `mode='live'`、`execution='scripted'`。下表中的 `id`、`message` 为字符串,`volume`、`price` 为数字;方法均返回 `void`。
|
|
226
|
+
|
|
227
|
+
| 方法 | 行为 |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `failNextSubmit(message?)` | 下一次提交同步失败,不创建委托 |
|
|
230
|
+
| `acknowledge(id)` | 发出 `notTraded` 订单回报 |
|
|
231
|
+
| `partialFill(id, volume, price?)` | 成交指定正整数数量;余量未完成时为 `partTraded` |
|
|
232
|
+
| `fill(id, price?)` | 成交全部余量,终态 `allTraded` |
|
|
233
|
+
| `reject(id, message?)` | 注入拒单终态,不改变仓位 |
|
|
234
|
+
| `rejectSubmit(id, message?)` | 注入已提交订单的异步报单错误 |
|
|
235
|
+
| `cancel(id, message?)` | 注入撤单终态,保留已成交量 |
|
|
236
|
+
| `failNextCancel(message?)` | 下一次撤单请求同步失败,订单保持活动 |
|
|
237
|
+
| `rejectCancel(id, message?)` | 注入异步撤单错误,受回报交付控制影响 |
|
|
238
|
+
| `emitExternalOrder(order)` | 创建外部订单,已有 ID 会被拒绝 |
|
|
239
|
+
| `emitOrder(order)` | 独立注入已有或外部订单回报,可报告尚未交付的成交量 |
|
|
240
|
+
| `emitTrade(trade)` | 独立注入成交,可先于订单或晚于终态到达 |
|
|
241
|
+
| `disconnect()` | 券商状态继续变化,策略暂不接收回报,也不能提交或撤单 |
|
|
242
|
+
| `reconcile(snapshot?)` | 补发变化订单与遗漏成交,再核对库存 |
|
|
243
|
+
| `fault(message?)` | 模拟 Broker 故障并锁定,需重启 Runtime 恢复 |
|
|
244
|
+
|
|
245
|
+
策略订单 ID 取自 `runtime.submissions`;外部订单自行提供稳定 ID。模拟通常的交易顺序时先确认再成交。部分或完整成交生成订单及成交回报;提交、撤单和 Broker 回报本身不会生成下一条行情。
|
|
246
|
+
|
|
247
|
+
全部成交、拒单或撤单终态会清除活动订单;部分成交保留余量,撤单失败不产生终态。已有独立成交也计入校验:`cancel()` 保留真实已成交量,`acknowledge()`、`reject()`、`rejectSubmit()` 拒绝将已成交订单退回未成交状态。
|
|
248
|
+
|
|
249
|
+
### 注入数据
|
|
250
|
+
|
|
251
|
+
以下类型均从 Testing 入口导入,字段只读:
|
|
252
|
+
|
|
253
|
+
| 类型 | 必填字段 | 可选字段 |
|
|
254
|
+
| --- | --- | --- |
|
|
255
|
+
| `StrategyTestExternalOrder` | `vtOrderId: string`、`direction: 'long' \| 'short'`、`price: number`、`volume: number` | `vtSymbol: string`、`offset`、`traded: number`、`status`、`statusMessage: string`、`datetime: number` |
|
|
256
|
+
| `StrategyTestTrade` | `vtTradeId: string`、`vtOrderId: string`、`volume: number` | `price: number`、`datetime: number`、`tradingDay: string` |
|
|
257
|
+
| `StrategyTestReconciliation` | 无 | `reason: 'initial' \| 'reconnect'`、`tradingDay: string`、`inventory`、`orders`、`trades` |
|
|
258
|
+
|
|
259
|
+
`offset`、`status` 使用 SDK 订单枚举;`orders`、`trades` 分别是上述订单、成交类型的只读数组;`inventory` 为部分今昨库存。新订单省略 `vtSymbol` 使用 fixture 合约,更新已有订单时省略则保留原合约,不允许改换合约。
|
|
260
|
+
|
|
261
|
+
成交的方向、开平及合约从订单取得,相同成交 ID 只处理一次。累计成交不能超过委托数量,终态后的迟到成交也不能超过已报告 `traded`;`fill()`、`partialFill()` 拒绝在终态后创造新成交。
|
|
262
|
+
|
|
263
|
+
局部范式:假设已有一张开多 2 手订单,初始空仓,先收到报告成交 1 手的撤单终态,再收到该笔成交:
|
|
264
|
+
|
|
265
|
+
~~~ts
|
|
266
|
+
const order = runtime.activeOrders()[0]!;
|
|
267
|
+
runtime.broker.emitOrder({ ...order, status: 'cancelled', traded: 1 });
|
|
268
|
+
runtime.advanceTime(100);
|
|
269
|
+
runtime.broker.emitTrade({
|
|
270
|
+
vtTradeId: 'delayed-trade-1',
|
|
271
|
+
vtOrderId: order.vtOrderId,
|
|
272
|
+
volume: 1,
|
|
273
|
+
price: order.price,
|
|
274
|
+
});
|
|
275
|
+
expect(runtime.snapshot()).toMatchObject({ pos: 1, activeOrders: 0 });
|
|
276
|
+
~~~
|
|
277
|
+
|
|
278
|
+
验证停止期间成交时,先 `await runtime.stop()` 并确认没有自动撤单,再按需要调用 `runtime.cancelOrder(id)`、注入回报;用持仓和成交断言结果,再重启验证保存。
|
|
279
|
+
|
|
280
|
+
### 交付顺序与重复回报
|
|
281
|
+
|
|
282
|
+
| 方法 | 返回或行为 |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `holdReports()` | 暂停交付,不暂停券商状态变化 |
|
|
285
|
+
| `pendingReports()` | 待交付回报副本 `readonly StrategyTestBrokerReport[]` |
|
|
286
|
+
| `deliverReport(id: string)` | 交付指定回报,可自行安排顺序 |
|
|
287
|
+
| `repeatReport(id: string)` | 将已交付的同一回报再次传入生产 Runtime,测试侧不去重 |
|
|
288
|
+
| `resumeReports()` | 按登记顺序交付剩余回报,恢复自动交付 |
|
|
289
|
+
| `snapshot()` | 独立券商快照 `StrategyTestBrokerSnapshot` |
|
|
290
|
+
|
|
291
|
+
回报含 `id: string`、`createdAt: number`、`type`、`value`;`type` 为 `order`、`trade` 或 `error`,错误值含 `vtOrderId`、`operation: 'submit' | 'cancel'`、`message`。券商快照含 `phase: 'running' | 'recovering' | 'faulted'`、`tradingDay`、fixture 合约 `inventory`、`orders`、`trades`。
|
|
292
|
+
|
|
293
|
+
局部范式:初始空仓,`orderId` 为开多 2 手订单;验证券商已成交但回报未交付,以及重复成交不会重复计仓:
|
|
294
|
+
|
|
295
|
+
~~~ts
|
|
296
|
+
runtime.broker.holdReports();
|
|
297
|
+
runtime.broker.partialFill(orderId, 1);
|
|
298
|
+
expect(runtime.broker.snapshot().inventory.longToday).toBe(1);
|
|
299
|
+
expect(runtime.snapshot().pos).toBe(0);
|
|
300
|
+
|
|
301
|
+
const report = runtime.broker.pendingReports().find(item => item.type === 'trade')!;
|
|
302
|
+
runtime.advanceTime(100);
|
|
303
|
+
runtime.broker.deliverReport(report.id);
|
|
304
|
+
expect(runtime.snapshot().pos).toBe(1);
|
|
305
|
+
runtime.broker.repeatReport(report.id);
|
|
306
|
+
expect(runtime.snapshot().pos).toBe(1);
|
|
307
|
+
runtime.broker.resumeReports();
|
|
308
|
+
~~~
|
|
309
|
+
|
|
310
|
+
重连丢弃未交付旧消息,改用券商快照补发;未实际交付过的旧消息不能用 `repeatReport()` 重复。
|
|
311
|
+
|
|
312
|
+
### 断线与外部成交
|
|
313
|
+
|
|
314
|
+
- 断线期间可继续用 `partialFill`、`fill`、`cancel`、`emitOrder`、`emitTrade` 修改模拟券商,再 `reconcile()` 补发遗漏回报,也可直接提供 `trades`。对账失败会抛错、使运行策略 fault 并锁定 Broker,须重启恢复。
|
|
315
|
+
- 测试手动首仓托管时,先让策略订阅 `trades`,再注入符合规格的外部订单与首笔成交,由策略调用 `claimOrder()`。覆盖忽略首笔后的延后认领失败、部分成交、迟到回报和重启;初始化历史不重复广播。
|
|
316
|
+
- 全局通知含未归属策略的成交;`runtime.tradeEvents` 只记录进入该策略普通 `onTrade()` 的成交。全局通知的业务效果通过公开变量、提交和普通回报断言。Paper 可观察模拟全局成交,但认领失败。
|
|
317
|
+
- 外部订单可指定其他 `vtSymbol`,成交继承订单合约;其他合约的库存、活动订单和重连补发不应污染本合约或阻止启动。`reconcile({ inventory })` 只覆盖 fixture 合约库存;按规格验证跨合约通知是否被忽略。
|
|
318
|
+
|
|
319
|
+
## 原始 CTP 网关
|
|
320
|
+
|
|
321
|
+
`createRuntime({ mode: 'live', execution: 'ctp' })` 在内存网关上运行真实 CTP 适配器,完全离线;可同时传入 `gatewayState`、`gatewaySession`。通过 `runtime.gateway`(`StrategyTestGateway`)控制,此模式不提供 Scripted Broker 方法。
|
|
322
|
+
|
|
323
|
+
| 方法 | 返回与行为 |
|
|
324
|
+
| --- | --- |
|
|
325
|
+
| `setSnapshot(snapshot)` | 设置原始查询结果,保留未提供集合 |
|
|
326
|
+
| `push(method: string, params)` | 交付原始推送,如 `onOrder`、`onTrade`、`onOrderError`;不修改查询快照 |
|
|
327
|
+
| `hold(method: string)`、`resume(method: string)` | 暂停或恢复指定 RPC 方法的自动响应 |
|
|
328
|
+
| `pendingRequests()` | 当前请求 `readonly StrategyTestGatewayRequest[]` |
|
|
329
|
+
| `waitForRequest(method: string)` | `Promise<StrategyTestGatewayRequest>`,等待该类请求开始 |
|
|
330
|
+
| `reply(id: number, response?)` | 只交付 RPC 响应,可返回失败 |
|
|
331
|
+
| `completeQuery(id: number, rows?)` | 交付查询响应及带 requestID 的结果推送 |
|
|
332
|
+
| `disconnect()` | 断开内存网关 |
|
|
333
|
+
| `reconnect(session?)` | `Promise<void>`,真实适配器对账完成或失败后结束 |
|
|
334
|
+
| `queryOrders()` | `Promise<readonly Readonly<OrderData>[]>`,通过适配器查询订单 |
|
|
335
|
+
|
|
336
|
+
未标明返回值的方法为 `void`。`params` 为只读键值对象,`rows` 为只读对象数组。`reply` 的可选响应为 `{ ok: boolean; data?: Record<string, unknown>; error?: { code: number; msg: string } }`。
|
|
337
|
+
|
|
338
|
+
| 类型 | 字段 |
|
|
339
|
+
| --- | --- |
|
|
340
|
+
| `StrategyTestGatewaySnapshot` | 可选 `orders`、`trades`、`positions`、`accounts`,均为原始对象的只读数组 |
|
|
341
|
+
| `StrategyTestGatewaySession` | `tradingDay: string`、`frontID: number`、`sessionID: number`,可选 `maxOrderRef: string` |
|
|
342
|
+
| `StrategyTestGatewayRequest` | `id: number`、`method: string`、只读 `params`、毫秒 `now` |
|
|
343
|
+
|
|
344
|
+
`gatewaySession`、`reconnect(session?)` 接受部分会话字段。默认使用 fixture 交易日,`frontID=1`、`sessionID=1`;测试账户的 `brokerID`、`investorID`、`accountID` 均为 `test`。原始回报须使用 CTP 字段和枚举,不会自动修正。
|
|
345
|
+
|
|
346
|
+
`gateway.requests` 保留全部请求及时间。`hold()` 持续到 `resume()` 或客户端重建,`completeQuery()` 只完成指定一次查询。RPC 与查询超时由 Runtime 时钟推进;重启保留查询快照及已分配委托序号,重建客户端缓存和订阅。
|
|
347
|
+
|
|
348
|
+
## 可观察状态
|
|
349
|
+
|
|
350
|
+
优先断言具体字段,避免巨大整对象快照。公开观察结果按只读使用:
|
|
351
|
+
|
|
352
|
+
| 观察面 | 内容 |
|
|
353
|
+
| --- | --- |
|
|
354
|
+
| `submissions` | 成功提交的 `StrategySubmission`:`vtOrderId`、`direction`、`offset`、`price`、`volume` |
|
|
355
|
+
| `snapshot()` | 策略状态、变量及库存,见下表 |
|
|
356
|
+
| `activeOrders()` | 当前活动订单的只读副本 |
|
|
357
|
+
| `orderEvents`、`tradeEvents` | 订单、提交错误及普通成交回报 |
|
|
358
|
+
| `cancelRequests` | 撤单尝试的订单 ID 数组,不代表成功 |
|
|
359
|
+
| `events` | 订单、成交和 Hook 的相对顺序;每项含毫秒 `now` 和事件内容 |
|
|
360
|
+
| `hooks` | 按调用顺序记录的 `StrategyHook`;包含名称、实例身份、模式、合约、`occurredAt` 和复制后的数据 |
|
|
361
|
+
| `logs` | 最近 20 条脱敏 Runtime 日志 |
|
|
362
|
+
| `trace` | 完整事件记录,不受日志窗口限制 |
|
|
363
|
+
|
|
364
|
+
`StrategyTestRuntimeSnapshot` 的字段均只读:
|
|
365
|
+
|
|
366
|
+
| 字段 | 类型 |
|
|
367
|
+
| --- | --- |
|
|
368
|
+
| `inited`、`trading` | `boolean` |
|
|
369
|
+
| `pos`、`activeOrders`、`now` | `number`;分别是净仓位、活动订单数、当前毫秒时钟 |
|
|
370
|
+
| `fault` | `string \| null` |
|
|
371
|
+
| `variables` | `Record<string, unknown>`,策略声明的变量 |
|
|
372
|
+
| `inventory` | `StrategyTestPositionInventory`,今昨多空库存 |
|
|
373
|
+
|
|
374
|
+
`StrategyTestEvent` 的 `type='order' | 'trade' | 'hook'` 均对应 `value`。正常订单事件带 `type='order'`;提交失败为 `type='error'`,含失败 code/message 和原请求,不含订单 ID。
|
|
375
|
+
|
|
376
|
+
`StrategyTestTraceEvent` 含递增 `sequence`、交付毫秒 `now`、输入关联 `cause: number | null`、`type`、`value`。覆盖创建上下文、输入、异步完成、Timer、Bar、回调起止、提交、撤单、回报、日志、Hook、状态读写和 fault;业务断言优先使用上表稳定字段。
|
|
377
|
+
|
|
378
|
+
`emitHook()` 遵循生产生命周期和输入校验:`onInit()` 与预热调用不记录,初始化完成后的每次调用都追加到 `hooks`、`events` 和 trace。Testing 只记录事件,不连接 Runner;可直接断言 `runtime.hooks`,场景回放结果则通过 `trace` 观察 Hook。
|
|
379
|
+
|
|
380
|
+
Paper 的提交和撤单记录来自实际操作入口,之后 fault 抑制回调也不丢记录;交易日切换的订单到期只产生 `cancelled` 回报,不算撤单请求。不要用日志代替成交或状态断言;需要检查日志时,在触发动作后立即断言。
|
|
381
|
+
|
|
382
|
+
## 场景录制与确定性回放
|
|
383
|
+
|
|
384
|
+
以下函数均从 Testing 入口导入;`definition` 为 `{ strategy, fixture }`,`options` 为 Runtime 选项。
|
|
385
|
+
|
|
386
|
+
| API | 返回与用途 |
|
|
387
|
+
| --- | --- |
|
|
388
|
+
| `runStrategyScenario(definition, scenario, options?)` | `Promise<StrategyTestScenarioResult>`,顺序执行脚本并自动清理 Runtime |
|
|
389
|
+
| `assertStrategyReplay(definition, scenario, options?)` | 同上;独立运行两次,比较完整 trace 与最终快照,失败指出首个差异事件 |
|
|
390
|
+
| `captureStrategyScenario(runtime, seed?: number)` | `StrategyTestScenario`,释放前录制实际输入、异步边界、fixture 与选项 |
|
|
391
|
+
| `serializeStrategyScenario(scenario)` | `string`,导出 JSON,保留显式 `undefined` 盘口与异常数字 |
|
|
392
|
+
| `parseStrategyScenario(value: string)` | `StrategyTestScenario`,解析并校验脚本 |
|
|
393
|
+
|
|
394
|
+
`StrategyTestScenario` 含可选整数 `seed` 和 `steps`。每步为 `{ action, at?, task? }`;`at` 是按顺序递增或相等的绝对交付毫秒,通过同一时钟推进。`StrategyTestAction` 的字段如下:
|
|
395
|
+
|
|
396
|
+
| `action.type` | 其他字段 |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| `initialize` | 可选 `warmupBars` |
|
|
399
|
+
| `start`、`stop`、`restart`、`crashAndRestart` | 无 |
|
|
400
|
+
| `setTime`、`advanceTime` | 分别为 `datetime`、`milliseconds` |
|
|
401
|
+
| `market` | `tick`,可选 `closedBars` |
|
|
402
|
+
| `bar` | `value`,可选交付时间 `datetime` |
|
|
403
|
+
| `cancelOrder` | `vtOrderId`,支持提交引用 |
|
|
404
|
+
| `expectFault` | 完整故障文本 `message` |
|
|
405
|
+
| `broker`、`gateway` | `method` 与参数数组 `args`,签名对应上述控制方法 |
|
|
406
|
+
| `await` | `task`,可选预期错误文本 `error` |
|
|
407
|
+
|
|
408
|
+
局部范式:假设规格规定首条示例 Tick 触发开多 2 手,之后没有其他交易动作。`strategy`、`fixture` 已提供,片段需导入 `assertStrategyReplay` 和 `expect`:
|
|
409
|
+
|
|
410
|
+
~~~ts
|
|
411
|
+
const scenario = {
|
|
412
|
+
seed: 42,
|
|
413
|
+
steps: [
|
|
414
|
+
{ action: { type: 'initialize' } },
|
|
415
|
+
{ action: { type: 'start' } },
|
|
416
|
+
{ action: { type: 'market', tick: { lastPrice: 3500 } } },
|
|
417
|
+
{ action: { type: 'broker', method: 'partialFill', args: [{ submission: 0 }, 1] } },
|
|
418
|
+
{ action: { type: 'cancelOrder', vtOrderId: { submission: 0 } } },
|
|
419
|
+
{ action: { type: 'broker', method: 'cancel', args: [{ submission: 0 }] } },
|
|
420
|
+
{ action: { type: 'restart' } },
|
|
421
|
+
],
|
|
422
|
+
} as const;
|
|
423
|
+
const result = await assertStrategyReplay({ strategy, fixture }, scenario);
|
|
424
|
+
expect(result.snapshot).toMatchObject({ pos: 1, trading: false, activeOrders: 0 });
|
|
425
|
+
~~~
|
|
426
|
+
|
|
427
|
+
- `StrategyTestSubmissionRef` 的 `{ submission: 0 }` 引用本次第一笔成功提交;`StrategyTestRequestRef` 的 `{ request: 'queryOrder' }` 引用当前同类待响应网关请求。
|
|
428
|
+
- 步骤设置 `task: 'name'` 可启动异步动作而暂不等待;后续须用 `{ action: { type: 'await', task: 'name' } }` 汇合。任务名不能复用或遗漏汇合;预期拒绝时,`await` 动作的 `error` 须精确匹配错误文本。
|
|
429
|
+
- 两个步骤之间可等待网关请求、推进时间、交付回报或完成查询。Broker 的 `snapshot`、`pendingReports`,以及 Gateway 的 `requests`、`pendingRequests` 不作为脚本动作。
|
|
430
|
+
- 结果含 `snapshot`、`trace`、`submissions`、`cancelRequests` 和可选 `seed`。`StrategyScenarioError` 含失败步骤 `step`、可重放 `scenario`、完整 `trace`,错误文本带种子和失败原因。
|
|
431
|
+
- 录制的 `context` 含 fixture、Runtime 选项及可选 `customPaperExecution: true` 标记。回放使用录制上下文,显式 options 可覆盖;自定义 Paper 函数钩子须重新提供。
|
|
432
|
+
- 录制数据用于复现,不能自动成为业务预期。回放使用当前 SDK 语义,不提供旧版迁移;通用 SDK 回放不替代规格关键分支的业务测试。
|
|
433
|
+
|
|
434
|
+
## 场景检查与运行
|
|
435
|
+
|
|
436
|
+
按规格选择适用场景,不设固定数量:
|
|
437
|
+
|
|
438
|
+
- 多空入场、退出、精确阈值、信号复位、重复信号和已有活动订单。
|
|
439
|
+
- 提交失败、确认、部分及全部成交、拒单、撤单失败、有限重试、迟到及重复回报。
|
|
440
|
+
- 今昨仓拆单、剩余数量、仓位与风控上限、反转先平后开。
|
|
441
|
+
- 缺失盘口、涨跌停、停止状态、无效参数、行情缺口,以及高周期时段和交易日边界。
|
|
442
|
+
- 止损、止盈、收盘、隔夜、冷却、正常及崩溃恢复中的持久化状态与过期订单意图。
|
|
443
|
+
|
|
444
|
+
每个用例只安排必要前置状态,输入有意义的事件序列,再断言精确结果;类型检查和 Bun 测试均须通过。
|
|
445
|
+
|
|
446
|
+
从当前策略目录运行单场景或全部已写场景:
|
|
447
|
+
|
|
448
|
+
~~~bash
|
|
449
|
+
bun test ./tests/AC-001.test.ts
|
|
450
|
+
bun test ./tests
|
|
451
|
+
~~~
|
|
452
|
+
|
|
453
|
+
使用 Vibek 统一策略工程时,从当前策略目录检查该策略及其全部测试:
|
|
454
|
+
|
|
455
|
+
~~~bash
|
|
456
|
+
bun run --cwd ../.. typecheck:strategy -- <strategy-directory-name>
|
|
457
|
+
~~~
|
|
458
|
+
|
|
459
|
+
自建工程在工程根运行以下命令,`tsconfig.json` 须覆盖策略及测试,并启用严格检查:
|
|
460
|
+
|
|
461
|
+
~~~bash
|
|
462
|
+
bun run tsc --noEmit -p tsconfig.json
|
|
463
|
+
~~~
|