@dshtrading/connector-longbridge 0.6.1 → 0.6.3

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 CHANGED
@@ -1,34 +1,228 @@
1
- # @dshtrading/connector-template
1
+ # @dshtrading/connector-longbridge
2
2
 
3
- **本包是脚手架源,不是可安装的连接器**:不入任何 bundle 依赖,仅在 `pnpm -r build/test`
4
- 基线内保证可编译、冒烟测试绿。真实接入用生成器:
3
+ 长桥(Longbridge / LongPort)连接器:**港股(hk)行情 + 交易双面**。上游是长桥**券商 OpenAPI**
4
+ (官方 HTTP 端点,签名鉴权),实现 `@dshtrading/api` 的 `MarketDataService` / `TradeService`
5
+ 两个契约,注册服务键 `tradingHkMarketData` / `tradingHkTrade`,注册 5 个 `hk_*` 工具。
6
+ 被 hk bundle 以 `workspace:*` 依赖(`packages/hk/package.json:33`)。
5
7
 
6
- ```sh
7
- node scripts/new-connector.mjs --slug bybit --title Bybit --market crypto
8
- ```
8
+ ## 状态
9
+
10
+ **未实证**:本包没有已提交的真实券商请求记录(无原始响应、无真机错误码分布)。上游路径、
11
+ 签名规范与字段映射全部取自源码(下文逐条给出行号),**未经真机核对**。已提交的相关材料只有两类:
12
+
13
+ - `spikes/impl-service-seam-gate/NET-VERIFY.md`(2026-09-01,第 36 行):明确记录**未**
14
+ 对各家券商网关做真实下单验证,该轮唯一真实网络对象是 OKX;本包的服务缝三态由离线
15
+ `test/trade-gate.test.ts` 覆盖,不是出网证据。
16
+ - `spikes/verify-connectors-suite/EVIDENCE.md`(2026-08-31,第 37-39 行):当时的静态评审把
17
+ 「longbridge 未注册下单工具但 Note 声称有交易通道」列为发现项。该结论对应 commit `b80000e`;
18
+ 当前源码注册了 `hk_place_order` / `hk_cancel_order`(`src/index.ts:250-288`、`310-319`),
19
+ 与那时已不一致——但**现状同样没有实证**,本 README 不据此声明可用。
20
+
21
+ 本仓 `spikes/` 下没有 `impl-longbridge*` 目录,也没有任何针对长桥端点的探针脚本。
22
+
23
+ ## 配置
24
+
25
+ 字段名与默认值见 `src/index.ts:51-59`。
26
+
27
+ | 字段 | 默认 | 说明 |
28
+ |---|---|---|
29
+ | `enabled` | `true` | 总开关(`src/index.ts:52`) |
30
+ | `env` | `'demo'` | 运行环境标签(`src/index.ts:53`)。它**只进订单意图台账的作用域**(`src/index.ts:122-126` 的 `environment: options.config.env`),**不改变请求地址**——见「已知边界」 |
31
+ | `dryRun` | `true` | 强制模拟:为 true 时即使请求 `dryRun:false` 也只回本地模拟回执(`src/index.ts:144`) |
32
+ | `liveTrading` | `false` | 实盘请求的**非权威镜像**;真实放行需 `@dshtrading/authority` 平面的人工签署授权(`liveTradingEnabled()`,`src/index.ts:137`、`182`) |
33
+ | `appKeyRef` | `'LONGBRIDGE_APP_KEY'` | App Key 的**环境变量名**(BYOK,`src/index.ts:56`),进 `x-api-key` 头;设置中心 `dshtrading.credentials.longbridge.appKey` 优先级更高 |
34
+ | `appSecretRef` | `'LONGBRIDGE_APP_SECRET'` | App Secret 的环境变量名,用于 HMAC-SHA256 签名(`src/index.ts:57`、`src/rest.ts:149-159`);设置中心同名键优先 |
35
+ | `accessTokenRef` | `'LONGBRIDGE_ACCESS_TOKEN'` | Access Token 的环境变量名,进 `authorization` 头(`src/index.ts:58`、`src/rest.ts:142-144`);设置中心同名键优先 |
36
+
37
+ 交易安全三件套在本包的落点:
38
+
39
+ - **dry-run 缺省**:工具参数 `dryRun` 默认 `true`(`src/index.ts:259`),工具执行里
40
+ `args.dryRun !== false` 即走模拟分支(`src/index.ts:268`);服务内 `order.dryRun ?? true`
41
+ (`src/index.ts:136`)。
42
+ - **liveTrading 必须显式开启且非权威**:工具层再查一次(`src/index.ts:290-296`),服务缝
43
+ 内第一步也查(`src/index.ts:137-143`);撤单与下单同门槛(`src/index.ts:182-187`)。
44
+ - **base 审批闸门**:`hk_place_order` / `hk_cancel_order` 命中
45
+ `LIVE_ACTION_GATE_PATTERN`(`packages/base/src/index.ts:63`),无人在场时 ask 降级为 deny
46
+ (`packages/base/src/index.ts:20-24`)。
47
+ - **凭证解析面**:设置中心 `dshtrading.credentials.longbridge`(router `getCredential`)优先、
48
+ `process.env[<ref>]` 兜底,由 `resolveCredentials` 统一解析(`src/index.ts:213-221`);数据面行与
49
+ preset 行**都**走它(`src/dataplane.ts:29-31`),所以设置页填的 key 在 GUI 行情面同样生效。
50
+ 值只交给 rest 客户端,不落日志、不进台账身份(`src/index.ts:120-121`)。
51
+
52
+ ## 接线
53
+
54
+ - **数据面行**:`packages/hk/cordis.patch.yml:38-41` → 行 id `dsh-trading-hk-dataplane-longbridge`,
55
+ `name: '@dshtrading/connector-longbridge/dataplane'`,行 config 只写 `enabled: true`。
56
+ `src/dataplane.ts` 重导出 `Config`(第 10 行)让 loader 补全 schema 默认值,在
57
+ `tradingHkMarketData` isolate realm 内构造行情服务(第 37-38 行)并 `register('hk', 'longbridge', …)`
58
+ 进 `tradingMarketDataRegistry`(第 39 行);无注册表的老部署回退直接构造(第 33-36 行)。
59
+ **该文件不解析也不注册 `tradingTradeRegistry`**(全文件无此键)——交易服务只在 preset 面可用。
60
+ - **preset 行**:`packages/hk/assets/preset/hk-trader/agent.cordis.yml:65-77` → 组 id
61
+ `dsh-trading-hk-connector-longbridge-group`,`isolate: {tradingHkMarketData, tradingHkTrade}`
62
+ (第 68-70 行),行 id `dsh-trading-hk-connector-longbridge`,
63
+ `name: '@dshtrading/connector-longbridge'`,行 config `enabled: true` / `dryRun: true` /
64
+ `liveTrading: false`(第 74-77 行)。该行的 `apply` 直接构造行情与交易两个服务
65
+ (`src/index.ts:216-217`),不经注册表。
66
+ - **provider 词汇**:`packages/router/src/index.ts:80`(`PROVIDER_VOCABULARY` 的 `'longbridge'`)。
67
+ - **设置页显示名**:`PROVIDER_LABELS` 的 `longbridge` 条目
68
+ (`packages/client-ui-settings/src/client/trading-settings-controller.ts:68`:`type: 'commercial'`、
69
+ `env: 'LONGBRIDGE_APP_KEY'`、`markets: ['hk','us']`)与
70
+ `packages/client-ui-settings/src/client/locales.ts` 的 `'provider.longbridge'` 条目
71
+ (zh `Longbridge (长桥)` / en `Longbridge`)。凭据卡片字段为 `appKey` / `appSecret` /
72
+ `accessToken`(`trading-settings-controller.ts:163-165`,后两者 `secret: true`),
73
+ 由本包的 `resolveCredentials` 消费(settings 优先、env 兜底,两条挂载路径都读)。
74
+ - **bundle 依赖**:`packages/hk/package.json:33` 的 `@dshtrading/connector-longbridge: workspace:*`。
75
+ - **激活条件**:`routeAllows()` 要求 `enabled` 且
76
+ `tradingMarketRouter.activeProvider('hk') === 'longbridge'`(`src/index.ts:202-207`)。
77
+
78
+ ## 上游端点
79
+
80
+ 基地址 `process.env.LONGBRIDGE_API_URL || 'https://openapi.longportapp.com'`(`src/rest.ts:123`),
81
+ 也可由 `LongbridgeRestOptions.baseUrl` 直接覆盖。以下是源码里的全部路径(本包没有公开互联网
82
+ 以外的第三方回落源):
83
+
84
+ | 用途 | 端点 | 源码 |
85
+ |---|---|---|
86
+ | 实时报价 | `GET /v1/quote/realtime?symbol=00700.HK` | `src/rest.ts:184` |
87
+ | K 线 | `GET /v1/quote/candlesticks?symbol=&period=&count=` | `src/rest.ts:221` |
88
+ | 账户资产 | `GET /v1/asset/account` | `src/rest.ts:255` |
89
+ | 持仓 | `GET /v1/trade/stock/position` | `src/rest.ts:289` |
90
+ | 下单 | `POST /v1/trade/order`(`symbol` / `order_type`(MO\|LO) / `side`(Buy\|Sell) / `submitted_quantity` / `submitted_price` / `time_in_force: 'Day'` / `remark?`) | `src/rest.ts:317-329` |
91
+ | 撤单 | `DELETE /v1/trade/order?order_id=` | `src/rest.ts:352-354` |
92
+
93
+ 请求头(`src/rest.ts:136-161`):`Accept` / `Content-Type` 恒发;`authorization: Bearer <token>`
94
+ 在有 access token 时发;`x-api-key` + `x-timestamp`(`Date.now()`)在有 app key 时发;
95
+ `x-api-signature` **只在 appKey、accessToken 与 appSecret 三者齐备**时发(第 148-160 行)。
9
96
 
10
- 生成后按 [docs/guides/connector-playbook.md](../../docs/guides/connector-playbook.md) 的填写顺序
11
- (src/rest.ts 头部清单 7 项)实现 TODO。每个 TODO 的参照段都在 connector-okx
12
- (packages/connector-okx/),它是本仓第一个真实 TradeService 连接器。
97
+ ## 周期与契约
13
98
 
14
- ## 结构
99
+ - **周期词汇 8 个**:`1m/5m/15m/30m/1h/1d/1w/1M`(`src/rest.ts:24`),到上游的映射是
100
+ `1m/5m/15m/30m/**60m**/day/week/month`(`src/rest.ts:43-55`)。
101
+ - **表外周期显式拒绝**:`@dshtrading/api` 的 `Interval` 有 15 档(`packages/api/src/index.ts:25-40`),
102
+ 本包只服务 8 档。`assertSupportedInterval`(`src/rest.ts:34-41`)是唯一闸门,签名是类型断言
103
+ `asserts interval is SupportedInterval`(`SupportedInterval` 为词汇表联合,`src/rest.ts:26-27`),
104
+ 所以 `toLongbridgePeriod`(`src/rest.ts:43-55`)与 `parseIntervalMs`(`src/rest.ts:57-69`)过闸门后
105
+ 的 `switch` 对收窄联合穷尽,两条路径都返回 `string` / `number`,表外周期不再产出 `undefined`。
106
+ 拒绝码是 `TRADING_UNSUPPORTED_INTERVAL`,消息为
107
+ `Longbridge upstream does not serve interval "<interval>"; supported: 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M`。
108
+ `getKlines` 在取 period 之前先过闸门(`src/rest.ts:204`),服务直调传表外周期在发出任何请求
109
+ 之前就抛(测试断言出站 URL 数为 0)。工具面的 enum 也只有这 8 个(`src/index.ts:240`)。
110
+ - **为什么不映射 `3m` / `2h` / `4h`**:官方文档有两套互相矛盾的口径。现代口径(objects 页 Period
111
+ 定义表、proto `quote/api.proto`、Node/Rust/Python SDK 枚举、官方 skills 字符串表)共 18 个周期,
112
+ **含** `3 Minutes` / `Two Hours` / `Four Hours`(字符串 `3m` / `120m` / `240m`);旧口径(CLI 文档、
113
+ Go SDK 注释、objects 页 Protobuf 代码块)只承认 `1m/5m/15m/30m/60m/day/week/month/year`。
114
+ `6h/8h/12h/3d` 在任何口径里都不存在。更关键的是 **K 线在官方 REST 参考里没有登记 HTTP 端点**
115
+ (官方 39 个登记了 `HTTP URL` 的接口里没有 `/v1/quote/candlesticks`,candlestick 页只给
116
+ gRPC/protobuf 接口),本包打的这条路径属**文档未载**,「官方文档所述」的 period 字面量在本仓
117
+ 无凭证、未实测。所以本轮不新增映射:本地清晰拒绝优于依赖未实证的上游行为。
118
+ - **符号形态**:`toLongbridgeSymbol`(`src/rest.ts:71-82`)——`00700.HK` 与裸 1–5 位数字都补零
119
+ 到 5 位并加 `.HK`;其余只 trim + 大写、**无形态白名单**(美股代码原样透传,与 futu 的
120
+ `normalizeUsSymbol` 不同)。
121
+ - **签名规范**:`generateLongbridgeSignature` 拼 `METHOD\nPATH\ncanonicalHeaders\nsignedHeaders\nbodyHash`
122
+ 后 HMAC-SHA256(`src/rest.ts:84-105`),`signedHeaders` 恒为
123
+ `authorization;x-api-key;x-timestamp`(第 97 行),body 的 SHA-256 进规范串(第 99 行)。
124
+ - **错误映射**:HTTP 非 2xx → `TRADING_UPSTREAM_ERROR`(`src/rest.ts:166`);请求抛错 →
125
+ `TRADING_NETWORK`(第 171-175 行);行情 `code !== 0` 或空数组 → `TRADING_SYMBOL_NOT_FOUND`
126
+ (第 187 行);K 线 `code !== 0` 或没有 `candlesticks` → **返回空数组(静默降级,第 223-225 行)**;
127
+ 资产/持仓/下单/撤单 `code !== 0` → `TRADING_EXCHANGE_ERROR`(第 258、332、357 行);
128
+ 缺 appKey 或 accessToken(资产与交易面)→ `TRADING_AUTH_FAILED`(第 243、274、310、350 行)。
129
+ - **无本地超时**:`requestJson` 不设 `AbortController`/超时(`src/rest.ts:130-177`),
130
+ 上游无响应时请求没有连接器侧上限(与 futu 的 10 s 不同)。
131
+ - **服务类字段用 TS 编译期 `private`**(禁 ECMAScript `#`,cordis realm 代理会炸)。
15
132
 
16
- - `src/rest.ts` —— 错误载体(TradingServiceError)+ 通用请求管线(超时/错误映射/
17
- 模拟盘头/注入 fetch)直接可用;签名与端点 TODO。
18
- - `src/index.ts` —— Config(enabled/env/dryRun/liveTrading + 三 ref 凭证组)、
19
- 闸门三态判定、凭证解析(BYOK + 环境变量回落)、MarketDataService/TradeService 骨架、
20
- 7 个市场前缀工具注册(duplicate-safe)、apply(enabled=false 静默退出)。
21
- - `test/template.test.ts` —— 结构冒烟 7 用例。
133
+ ## 能力面
22
134
 
23
- ## Token
135
+ 行情面服务方法(`src/index.ts:73-102`):
24
136
 
25
- `longbridge`(slug)/ `Longbridge`(title)/ `LONGBRIDGE`(ref 前缀)/
26
- `hk`(工具前缀)/ `Hk`(服务键 infix)。token 未展开也可编译;
27
- 生成器保证全部替换。
137
+ | 服务方法 | 状态 |
138
+ |---|---|
139
+ | `getTicker` | 有(`src/index.ts:85-87`) |
140
+ | `getKlines` | 有(`src/index.ts:89-91`) |
141
+ | `subscribeTicker` | 有(快照轮询,缺省 5000 ms、下限 1000 ms,`src/index.ts:93-101`) |
142
+ | `listInstruments` / `getTickers` / `getOrderbook` / `getFundamentals` / `getDerivatives` / `getRecentTrades` | 缺席 |
28
143
 
29
- ## 注意(勿改的坑)
144
+ 交易面服务方法(`src/index.ts:104-198`):
145
+
146
+ | 服务方法 | 状态 | 有对应工具 |
147
+ |---|---|---|
148
+ | `placeOrder` | 有(三态闸门 + 意图台账,`src/index.ts:133-177`) | `hk_place_order` |
149
+ | `cancelOrder` | 有(与下单同门槛,`src/index.ts:179-189`) | `hk_cancel_order` |
150
+ | `getBalance` | 有(`src/index.ts:129-131`) | `hk_get_balance` |
151
+ | `getPositions` | 有(`src/index.ts:191-193`) | **无工具** |
152
+ | `getOrders` | 恒返回 `[]`(`src/index.ts:195-197`) | 无 |
153
+ | `getOrder` | **未实现**:类里没有这个方法,而它是 `TradeService` 的**必选**成员(`packages/api/src/index.ts:839`) | 无 |
154
+ | `getBalances` / `listOpenOrders` / `listTradeFills` / `setLeverage` | 缺席 | 无 |
155
+
156
+ 注册的工具 5 个(`src/index.ts:219-331`,同名已存在则跳过,第 221-223 行):
157
+
158
+ | 工具 | 注册位置 | 说明 |
159
+ |---|---|---|
160
+ | `hk_get_ticker` | `src/index.ts:225-233` | 最新价与成交量 |
161
+ | `hk_get_klines` | `src/index.ts:235-248` | interval 用 8 词汇枚举,缺省 `1d`;limit 缺省 100 |
162
+ | `hk_place_order` | `src/index.ts:250-308` | `dryRun !== false` 时回本地模拟回执(带参照价,`sim-lb-` 前缀);否则要求 `liveTrading` |
163
+ | `hk_cancel_order` | `src/index.ts:310-319` | 参数名 `ordId`;实盘动作,同样需 `liveTrading` 且 `dryRun=false` |
164
+ | `hk_get_balance` | `src/index.ts:321-330` | 账户余额(返回形状与 `AccountBalance` 契约不一致,见「已知边界」) |
165
+
166
+ ## 已知边界
167
+
168
+ - **未实证项**:全部端点、签名是否被上游接受、真机错误码分布、`code` 字段的实际取值约定,
169
+ 均未经真机核对。`spikes/impl-service-seam-gate/NET-VERIFY.md:36` 明确记录未对券商网关做真实下单验证。
170
+ - **`env` 不改变地址**:`env: 'demo'` 只进意图台账作用域(`src/index.ts:122-126`),
171
+ 基地址恒为 `openapi.longportapp.com`(`src/rest.ts:123`)。即实盘放行后,
172
+ `env` 仍为 `demo` 的部署也会把订单发往**生产**端点。
173
+ - **`getOrder` 未实现**:`TradeService` 要求它(`packages/api/src/index.ts:839`),
174
+ `LongbridgeTradeService` 没有该方法(`src/index.ts:104-198` 全文无 `getOrder`)。消费方
175
+ 按契约调用会得到 `undefined is not a function` 形态的失败——**该运行期后果未实证**。
176
+ - **`getBalance` / `getPositions` 返回形状与契约不符**:`getBalance` 返回
177
+ `{currency, available, total}`(`src/rest.ts:265-269`),而 `AccountBalance` 是
178
+ `{asset, free, locked}`(`packages/api/src/index.ts:578-582`);`getPositions` 每行返回
179
+ `{symbol, quantity, entryPrice, unrealizedPnl}`(`src/rest.ts:296-301`),而 `Position` 要求
180
+ `{symbol, side, size, entryPrice, timestamp}`(`packages/api/src/index.ts:499-520`)。
181
+ 消费方按契约字段读会读到 `undefined`(**运行期表现未实证**)。
182
+ - **行情面无凭证也发请求**:`getTicker` / `getKlines` 不做凭证前置检查(`src/rest.ts:179-239`),
183
+ 三凭证缺失时请求照发(只是不带 `authorization` / `x-api-key` / `x-api-signature` 头)。
184
+ 只有资产与交易面在缺凭证时抛 `TRADING_AUTH_FAILED`(`src/rest.ts:243-245`、`274-276`、`310-312`、`350-352`)。
185
+ - **K 线失败静默转空数组**:`code !== 0` 与「确实无数据」在 `getKlines` 里不可区分(`src/rest.ts:223-225`)。
186
+ - **`candlesticks[].timestamp` 按 Unix 秒解析**:解析处为 `c.timestamp * 1000`(`src/rest.ts:228`)、
187
+ `closeTime = openTime + stepMs - 1`(第 236 行)。「官方文档所述」所有时间字段用 Unix Timestamp
188
+ 且官方示例为 10 位值,故秒转毫秒与上游口径一致;该口径**未经真机核对**(见「未实证项」)。
189
+ - **无本地超时**(`src/rest.ts:130-177`)。
190
+ - **`getOrders` 恒空**(`src/index.ts:195-197`):空数组在消费方读作「无挂单」,而不是「本包不支持」。
191
+ - **持仓面有服务、无工具**:`getPositions` 只经 `tradingHkTrade` 服务可达(`src/index.ts:191-193`),
192
+ 工具面没有对应的 `hk_get_positions`。
193
+ - **交易面不进交易注册表**:`src/dataplane.ts` 全文件没有 `tradingTradeRegistry`,注册表里没有
194
+ `(hk, longbridge)` 交易条目;读该注册表的消费方看不到本包的交易服务。
195
+ - **`baseUrl` 环境变量在插件路径下被绕过**:`LONGBRIDGE_API_URL`(`src/rest.ts:123`)只在
196
+ 直连 `LongbridgeRestClient` 且不传 `baseUrl` 时命中;`apply` 传的是
197
+ `{appKey, appSecret, accessToken}`(`src/index.ts:216-217`),不含 `baseUrl`,所以环境变量
198
+ 在本路径下确实生效——但 `Config` 里**没有**可配置的基地址字段,改地址只能改环境变量。
199
+
200
+ ## 合规记录
201
+
202
+ **长桥 OpenAPI 是券商官方接口,走 BYOK**:三条凭证(App Key / App Secret / Access Token)
203
+ 由用户自备、只进 `*Ref` 环境变量名(`src/index.ts:56-58`、`213-215`),仓库永不内置、不落日志
204
+ (铁律 #3/#5);本仓不缓存、不再分发任何行情或账户数据。使用边界以长桥开放平台的服务条款与
205
+ 用户自身的行情/交易权限为准;行情与交易权限由长桥账户等级决定,连接器不做权限降级或绕行。
206
+
207
+ **下单备注只作对账锚,不是幂等键**:`remark` 由本地意图 id 确定性派生
208
+ (`LONGBRIDGE_REMARK_RULE`,`src/index.ts:71`),官方**没有**承诺按 remark 去重,
209
+ 所以「同 ID 只下一单」完全由本地意图台账保证(`src/index.ts:166-175`)。
210
+
211
+ **实盘需人工签署授权**:`liveTrading` 只是非权威镜像,权威是 `@dshtrading/authority` 平面;
212
+ 未签署时下单与撤单都在服务缝结构化拒绝(`src/index.ts:137-143`、`182-187`)。
213
+
214
+ `package.json` 的 license 是 `PolyForm-Noncommercial-1.0.0`(非商业免费,商用需书面授权)。
215
+
216
+ ## 出网验证
217
+
218
+ 本包暂无已提交的独立出网验证脚本:`spikes/` 下没有 `impl-longbridge*` 目录,
219
+ `spikes/impl-service-seam-gate/net-verify.mjs` 只覆盖 OKX,`spikes/verify-connectors-suite/`
220
+ 只提交了 `EVIDENCE.md`(无脚本)。可复跑的最小入口(不触网):
221
+
222
+ ```sh
223
+ pnpm --filter @dshtrading/connector-longbridge test # 离线契约测试(契约化假网关,无真实网络)
224
+ ```
30
225
 
31
- - 服务类用 TS 编译期 `private`(禁 ECMAScript `#`,realm 代理会炸)。
32
- - 工具名 = 市场前缀 + 语义词(不带交易所名)——base 闸门正则只认市场前缀。
33
- - 行 id = `dsh-trading-<market>-connector-<slug>`,全仓唯一(insert-only 铁律 #1)。
34
- - 凭证只进 ref,绝不内置/落日志(铁律 #3/#5)。
226
+ 真机复跑需要用户自备长桥凭证(`LONGBRIDGE_APP_KEY` / `LONGBRIDGE_APP_SECRET` /
227
+ `LONGBRIDGE_ACCESS_TOKEN`),自写探针直调 `LongbridgeRestClient`;本仓未提交任何长桥端点的
228
+ 请求或响应记录,本 README 不为该链路声明任何可用性结论。
@@ -2,6 +2,6 @@ import { Config } from "./index.js";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/dataplane.d.ts
4
4
  declare const inject: string[];
5
- declare function apply(ctx: Context, config: Config): void;
5
+ declare function apply(ctx: Context, config?: Config): void;
6
6
  //#endregion
7
- export { apply, inject };
7
+ export { Config, apply, inject };
package/lib/dataplane.js CHANGED
@@ -1,17 +1,16 @@
1
- import { LongbridgeMarketDataService, TRADING_HK_MARKET_DATA_KEY } from "./index.js";
1
+ import { Config, LongbridgeMarketDataService, ROUTER_PROVIDER, TRADING_HK_MARKET_DATA_KEY, resolveCredentials } from "./index.js";
2
2
  //#region src/dataplane.ts
3
3
  const inject = [];
4
+ /** schema 的运行时可调用形态(校验即补默认值);手工构造 ctx 不传 config 时按 schema 默认值处理。 */
5
+ const withDefaults = (value) => Config(value);
4
6
  function resolveMarketDataRegistry(ctx) {
5
7
  const candidate = ctx.get?.("tradingMarketDataRegistry", false);
6
8
  return candidate !== void 0 ? candidate : void 0;
7
9
  }
8
- const ROUTER_PROVIDER = "longbridge";
9
10
  const MARKET = "hk";
10
- function apply(ctx, config) {
11
+ function apply(ctx, config = withDefaults(void 0)) {
11
12
  if (!config.enabled) return;
12
- const appKey = process.env[config.appKeyRef];
13
- const appSecret = process.env[config.appSecretRef];
14
- const accessToken = process.env[config.accessTokenRef];
13
+ const { appKey, appSecret, accessToken } = resolveCredentials(ctx, config);
15
14
  const registry = resolveMarketDataRegistry(ctx);
16
15
  if (registry === void 0) {
17
16
  new LongbridgeMarketDataService(ctx, {
@@ -30,4 +29,4 @@ function apply(ctx, config) {
30
29
  ctx.effect(() => registry.register(MARKET, ROUTER_PROVIDER, service));
31
30
  }
32
31
  //#endregion
33
- export { apply, inject };
32
+ export { Config, apply, inject };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccountBalance, INTERVAL_VOCABULARY, Interval, Kline, LongbridgeRestClient, LongbridgeRestOptions, Order, Position, Ticker, TradingServiceError, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol } from "./rest.js";
1
+ import { AccountBalance, INTERVAL_VOCABULARY, Interval, Kline, LongbridgeRestClient, LongbridgeRestOptions, Order, Position, SupportedInterval, Ticker, TradingServiceError, assertSupportedInterval, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol } from "./rest.js";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import Schema from "@deepseek-ai/schemastery";
4
4
  import { AccountBalance as AccountBalance$1, Disposable, Interval as Interval$1, Kline as Kline$1, MarketDataService, Order as Order$1, OrderRequest, Position as Position$1, Ticker as Ticker$1, TradeService } from "@dshtrading/api";
@@ -16,6 +16,17 @@ interface Config {
16
16
  declare const Config: Schema<Config>;
17
17
  declare const TRADING_HK_MARKET_DATA_KEY = "tradingHkMarketData";
18
18
  declare const TRADING_HK_TRADE_KEY = "tradingHkTrade";
19
+ /**
20
+ * Longbridge `submit_order` 的 `remark` 规则:官方 SDK/文档的下单参数里有 `remark`
21
+ * (open.longbridge.com「Submit Order」:`submit_order(..., remark="Hello from Python SDK")`),
22
+ * 但官方**没有**承诺按 remark 去重——它只是能带出去、能读回来的标签。
23
+ * 因此 remark 只用于对账,不构成 venue 幂等;exactly-once 由本地意图台账保证。
24
+ * 官方未给长度上限,取 32 保持短小。
25
+ */
26
+ declare const LONGBRIDGE_REMARK_RULE: {
27
+ readonly prefix: "dsht";
28
+ readonly maxLength: 32;
29
+ };
19
30
  declare class LongbridgeMarketDataService extends Service implements MarketDataService {
20
31
  private readonly client;
21
32
  constructor(ctx: Context, options?: LongbridgeRestOptions, serviceName?: string);
@@ -29,6 +40,8 @@ declare class LongbridgeTradeService extends Service implements TradeService {
29
40
  private readonly client;
30
41
  /** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
31
42
  private readonly config;
43
+ /** 订单意图台账(实盘幂等前提;写 agent 自己的 trading home,不写授权平面)。 */
44
+ private readonly intents;
32
45
  constructor(ctx: Context, options: LongbridgeRestOptions & {
33
46
  config: Config;
34
47
  }, serviceName?: string);
@@ -42,7 +55,18 @@ declare class LongbridgeTradeService extends Service implements TradeService {
42
55
  getOrders(): Promise<Order$1[]>;
43
56
  }
44
57
  declare const ROUTER_PROVIDER = "longbridge";
58
+ /**
59
+ * 本连接器的三件凭证解析:设置中心 `dshtrading.credentials.longbridge`
60
+ * (router `getCredential`)优先、`process.env[<ref>]` 兜底。
61
+ * 值分别进 rest 层的 `x-api-key` 头、HMAC-SHA256 签名(appSecret)与 `authorization` 头
62
+ * (accessToken),见 src/rest.ts 的 signing 段——设置页写的键不是纯承诺。
63
+ */
64
+ declare function resolveCredentials(ctx: Context, config: Config): {
65
+ appKey?: string;
66
+ appSecret?: string;
67
+ accessToken?: string;
68
+ };
45
69
  declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
46
70
  declare function apply(ctx: Context, config: Config): void;
47
71
  //#endregion
48
- export { type AccountBalance, Config, INTERVAL_VOCABULARY, type Interval, type Kline, LongbridgeMarketDataService, LongbridgeRestClient, LongbridgeRestOptions, LongbridgeTradeService, type Order, type Position, ROUTER_PROVIDER, TRADING_HK_MARKET_DATA_KEY, TRADING_HK_TRADE_KEY, type Ticker, TradingServiceError, apply, generateLongbridgeSignature, name, parseIntervalMs, routeAllows, toLongbridgePeriod, toLongbridgeSymbol };
72
+ export { type AccountBalance, Config, INTERVAL_VOCABULARY, type Interval, type Kline, LONGBRIDGE_REMARK_RULE, LongbridgeMarketDataService, LongbridgeRestClient, LongbridgeRestOptions, LongbridgeTradeService, type Order, type Position, ROUTER_PROVIDER, SupportedInterval, TRADING_HK_MARKET_DATA_KEY, TRADING_HK_TRADE_KEY, type Ticker, TradingServiceError, apply, assertSupportedInterval, generateLongbridgeSignature, name, parseIntervalMs, resolveCredentials, routeAllows, toLongbridgePeriod, toLongbridgeSymbol };
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { INTERVAL_VOCABULARY, LongbridgeRestClient, TradingServiceError, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol as toLongbridgeSymbol$1 } from "./rest.js";
1
+ import { INTERVAL_VOCABULARY, LongbridgeRestClient, TradingServiceError, assertSupportedInterval, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol as toLongbridgeSymbol$1 } from "./rest.js";
2
2
  import { liveTradingEnabled } from "@dshtrading/authority";
3
+ import { OrderIntentLedger, deriveVenueClientKey, orderFromReceipt, receiptFromOrder } from "@dshtrading/authority/order-intents";
3
4
  import { Service } from "@deepseek-ai/cordis";
4
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
6
  import Schema from "@deepseek-ai/schemastery";
@@ -20,6 +21,17 @@ const Config = Schema.object({
20
21
  });
21
22
  const TRADING_HK_MARKET_DATA_KEY = "tradingHkMarketData";
22
23
  const TRADING_HK_TRADE_KEY = "tradingHkTrade";
24
+ /**
25
+ * Longbridge `submit_order` 的 `remark` 规则:官方 SDK/文档的下单参数里有 `remark`
26
+ * (open.longbridge.com「Submit Order」:`submit_order(..., remark="Hello from Python SDK")`),
27
+ * 但官方**没有**承诺按 remark 去重——它只是能带出去、能读回来的标签。
28
+ * 因此 remark 只用于对账,不构成 venue 幂等;exactly-once 由本地意图台账保证。
29
+ * 官方未给长度上限,取 32 保持短小。
30
+ */
31
+ const LONGBRIDGE_REMARK_RULE = {
32
+ prefix: "dsht",
33
+ maxLength: 32
34
+ };
23
35
  var LongbridgeMarketDataService = class extends Service {
24
36
  client;
25
37
  constructor(ctx, options = {}, serviceName = TRADING_HK_MARKET_DATA_KEY) {
@@ -46,10 +58,17 @@ var LongbridgeTradeService = class extends Service {
46
58
  client;
47
59
  /** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
48
60
  config;
61
+ /** 订单意图台账(实盘幂等前提;写 agent 自己的 trading home,不写授权平面)。 */
62
+ intents;
49
63
  constructor(ctx, options, serviceName = TRADING_HK_TRADE_KEY) {
50
64
  super(ctx, serviceName);
51
65
  this.client = new LongbridgeRestClient(options);
52
66
  this.config = options.config;
67
+ this.intents = new OrderIntentLedger({
68
+ provider: "longbridge",
69
+ environment: options.config.env,
70
+ account: "default"
71
+ });
53
72
  }
54
73
  async getBalance() {
55
74
  return this.client.getBalance();
@@ -68,7 +87,29 @@ var LongbridgeTradeService = class extends Service {
68
87
  dryRun: true,
69
88
  timestamp: Date.now()
70
89
  };
71
- return this.client.placeOrder(void 0, order);
90
+ const payload = {
91
+ symbol: order.symbol,
92
+ side: order.side,
93
+ type: order.type,
94
+ quantity: order.quantity,
95
+ ...order.price !== void 0 ? { price: order.price } : {}
96
+ };
97
+ const decision = await this.intents.begin({
98
+ intentId: order.intentId,
99
+ payload
100
+ });
101
+ if (decision.action === "replay") return {
102
+ ...orderFromReceipt(decision.receipt),
103
+ status: decision.receipt.status
104
+ };
105
+ const remark = deriveVenueClientKey(decision.intentId, LONGBRIDGE_REMARK_RULE);
106
+ const placed = await this.client.placeOrder(void 0, order, remark);
107
+ await this.intents.confirm({
108
+ intentId: decision.intentId,
109
+ payload,
110
+ receipt: receiptFromOrder(placed)
111
+ });
112
+ return placed;
72
113
  }
73
114
  async cancelOrder(orderId) {
74
115
  if (!liveTradingEnabled(this.config.liveTrading) || this.config.dryRun) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", "Longbridge TradeService.cancelOrder rejected at the service seam: cancel is a live action and requires a signed live-trading grant (see @dshtrading/authority) with dryRun=false.");
@@ -82,6 +123,20 @@ var LongbridgeTradeService = class extends Service {
82
123
  }
83
124
  };
84
125
  const ROUTER_PROVIDER = "longbridge";
126
+ /**
127
+ * 本连接器的三件凭证解析:设置中心 `dshtrading.credentials.longbridge`
128
+ * (router `getCredential`)优先、`process.env[<ref>]` 兜底。
129
+ * 值分别进 rest 层的 `x-api-key` 头、HMAC-SHA256 签名(appSecret)与 `authorization` 头
130
+ * (accessToken),见 src/rest.ts 的 signing 段——设置页写的键不是纯承诺。
131
+ */
132
+ function resolveCredentials(ctx, config) {
133
+ const creds = (ctx.get?.("tradingMarketRouter", false))?.getCredential?.(ROUTER_PROVIDER);
134
+ return {
135
+ appKey: creds?.appKey || process.env[config.appKeyRef],
136
+ appSecret: creds?.appSecret || process.env[config.appSecretRef],
137
+ accessToken: creds?.accessToken || process.env[config.accessTokenRef]
138
+ };
139
+ }
85
140
  function routeAllows(ctx, config, market) {
86
141
  if (!config.enabled) return false;
87
142
  const router = ctx.get?.("tradingMarketRouter", false);
@@ -91,9 +146,7 @@ function routeAllows(ctx, config, market) {
91
146
  function apply(ctx, config) {
92
147
  if (!config.enabled) return;
93
148
  if (!routeAllows(ctx, config, "hk")) return;
94
- const appKey = process.env[config.appKeyRef];
95
- const appSecret = process.env[config.appSecretRef];
96
- const accessToken = process.env[config.accessTokenRef];
149
+ const { appKey, appSecret, accessToken } = resolveCredentials(ctx, config);
97
150
  const marketData = new LongbridgeMarketDataService(ctx, {
98
151
  appKey,
99
152
  appSecret,
@@ -196,6 +249,10 @@ function apply(ctx, config) {
196
249
  type: "boolean",
197
250
  default: true,
198
251
  description: "Dry run"
252
+ },
253
+ intentId: {
254
+ type: "string",
255
+ description: "Stable idempotency id, required when dryRun=false. Same id + same request returns the recorded receipt and never places a second order; a different payload/provider/environment/account under the same id is rejected (TRADING_INTENT_CONFLICT); an unresolved id stays blocked as unknown until reconciled."
199
256
  }
200
257
  },
201
258
  output: {
@@ -236,6 +293,7 @@ function apply(ctx, config) {
236
293
  type: args.type,
237
294
  quantity: args.quantity,
238
295
  price: args.price,
296
+ ...typeof args.intentId === "string" ? { intentId: args.intentId } : {},
239
297
  dryRun: false
240
298
  });
241
299
  return JSON.stringify(order);
@@ -280,4 +338,4 @@ function apply(ctx, config) {
280
338
  });
281
339
  }
282
340
  //#endregion
283
- export { Config, INTERVAL_VOCABULARY, LongbridgeMarketDataService, LongbridgeRestClient, LongbridgeTradeService, ROUTER_PROVIDER, TRADING_HK_MARKET_DATA_KEY, TRADING_HK_TRADE_KEY, TradingServiceError, apply, generateLongbridgeSignature, name, parseIntervalMs, routeAllows, toLongbridgePeriod, toLongbridgeSymbol$1 as toLongbridgeSymbol };
341
+ export { Config, INTERVAL_VOCABULARY, LONGBRIDGE_REMARK_RULE, LongbridgeMarketDataService, LongbridgeRestClient, LongbridgeTradeService, ROUTER_PROVIDER, TRADING_HK_MARKET_DATA_KEY, TRADING_HK_TRADE_KEY, TradingServiceError, apply, assertSupportedInterval, generateLongbridgeSignature, name, parseIntervalMs, resolveCredentials, routeAllows, toLongbridgePeriod, toLongbridgeSymbol$1 as toLongbridgeSymbol };
package/lib/rest.d.ts CHANGED
@@ -5,6 +5,14 @@ declare class TradingServiceError extends Error {
5
5
  constructor(code: TradingErrorCode, message: string, cause?: unknown);
6
6
  }
7
7
  declare const INTERVAL_VOCABULARY: readonly ["1m", "5m", "15m", "30m", "1h", "1d", "1w", "1M"];
8
+ /** `INTERVAL_VOCABULARY` 的类型面:调用方在断言后即可按穷尽联合处理周期。 */
9
+ type SupportedInterval = (typeof INTERVAL_VOCABULARY)[number];
10
+ /**
11
+ * 周期闸门:`@dshtrading/api` 的 `Interval` 有 15 档,本包只覆盖 8 档,表外 7 档
12
+ * (3m/2h/4h/6h/8h/12h/3d)显式抛 `TRADING_UNSUPPORTED_INTERVAL`,不静默降级。
13
+ * 理由:长桥 K 线在本包使用的 HTTP 路径上属文档未载,上游 `period` 接受什么字面量无凭证可测。
14
+ */
15
+ declare function assertSupportedInterval(interval: Interval$1): asserts interval is SupportedInterval;
8
16
  declare function toLongbridgePeriod(interval: Interval$1): string;
9
17
  declare function parseIntervalMs(interval: Interval$1): number;
10
18
  declare function toLongbridgeSymbol(raw: string): {
@@ -35,11 +43,11 @@ declare class LongbridgeRestClient {
35
43
  getKlines(symbol: string, interval?: Interval$1, limit?: number): Promise<Kline$1[]>;
36
44
  getBalance(): Promise<AccountBalance$1>;
37
45
  getPositions(): Promise<Position$1[]>;
38
- placeOrder(_creds: unknown, req: OrderRequest): Promise<Order$1>;
46
+ placeOrder(_creds: unknown, req: OrderRequest, remark?: string): Promise<Order$1>;
39
47
  cancelOrder(_creds: unknown, orderId: string): Promise<{
40
48
  orderId: string;
41
49
  status: 'canceled';
42
50
  }>;
43
51
  }
44
52
  //#endregion
45
- export { type AccountBalance$1 as AccountBalance, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, LongbridgeRestClient, LongbridgeRestOptions, type Order$1 as Order, type Position$1 as Position, type Ticker$1 as Ticker, TradingServiceError, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol };
53
+ export { type AccountBalance$1 as AccountBalance, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, LongbridgeRestClient, LongbridgeRestOptions, type Order$1 as Order, type Position$1 as Position, SupportedInterval, type Ticker$1 as Ticker, TradingServiceError, assertSupportedInterval, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol };
package/lib/rest.js CHANGED
@@ -19,7 +19,16 @@ const INTERVAL_VOCABULARY = [
19
19
  "1w",
20
20
  "1M"
21
21
  ];
22
+ /**
23
+ * 周期闸门:`@dshtrading/api` 的 `Interval` 有 15 档,本包只覆盖 8 档,表外 7 档
24
+ * (3m/2h/4h/6h/8h/12h/3d)显式抛 `TRADING_UNSUPPORTED_INTERVAL`,不静默降级。
25
+ * 理由:长桥 K 线在本包使用的 HTTP 路径上属文档未载,上游 `period` 接受什么字面量无凭证可测。
26
+ */
27
+ function assertSupportedInterval(interval) {
28
+ if (!INTERVAL_VOCABULARY.includes(interval)) throw new TradingServiceError("TRADING_UNSUPPORTED_INTERVAL", "Longbridge upstream does not serve interval \"" + interval + "\"; supported: " + INTERVAL_VOCABULARY.join(", "));
29
+ }
22
30
  function toLongbridgePeriod(interval) {
31
+ assertSupportedInterval(interval);
23
32
  switch (interval) {
24
33
  case "1m": return "1m";
25
34
  case "5m": return "5m";
@@ -32,6 +41,7 @@ function toLongbridgePeriod(interval) {
32
41
  }
33
42
  }
34
43
  function parseIntervalMs(interval) {
44
+ assertSupportedInterval(interval);
35
45
  switch (interval) {
36
46
  case "1m": return 6e4;
37
47
  case "5m": return 3e5;
@@ -132,6 +142,7 @@ var LongbridgeRestClient = class {
132
142
  };
133
143
  }
134
144
  async getKlines(symbol, interval = "1d", limit = 100) {
145
+ assertSupportedInterval(interval);
135
146
  const { symbol: lbSymbol } = toLongbridgeSymbol(symbol);
136
147
  const period = toLongbridgePeriod(interval);
137
148
  const stepMs = parseIntervalMs(interval);
@@ -176,7 +187,7 @@ var LongbridgeRestClient = class {
176
187
  });
177
188
  return positions;
178
189
  }
179
- async placeOrder(_creds, req) {
190
+ async placeOrder(_creds, req, remark) {
180
191
  if (!this.appKey || !this.accessToken) throw new TradingServiceError("TRADING_AUTH_FAILED", "Longbridge: appKey and accessToken are required for placeOrder");
181
192
  const { symbol: lbSymbol, canonical } = toLongbridgeSymbol(req.symbol);
182
193
  const res = await this.requestJson("/v1/trade/order", {
@@ -187,7 +198,8 @@ var LongbridgeRestClient = class {
187
198
  side: req.side === "buy" ? "Buy" : "Sell",
188
199
  submitted_quantity: String(req.quantity),
189
200
  submitted_price: req.price ? String(req.price) : void 0,
190
- time_in_force: "Day"
201
+ time_in_force: "Day",
202
+ ...remark !== void 0 ? { remark } : {}
191
203
  })
192
204
  });
193
205
  if (res.code !== 0 || !res.data?.order_id) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Longbridge placeOrder failed: ${res.message ?? `code ${res.code}`}`);
@@ -214,4 +226,4 @@ var LongbridgeRestClient = class {
214
226
  }
215
227
  };
216
228
  //#endregion
217
- export { INTERVAL_VOCABULARY, LongbridgeRestClient, TradingServiceError, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol };
229
+ export { INTERVAL_VOCABULARY, LongbridgeRestClient, TradingServiceError, assertSupportedInterval, generateLongbridgeSignature, parseIntervalMs, toLongbridgePeriod, toLongbridgeSymbol };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/connector-longbridge",
3
- "description": "【模板】交易所连接器插件脚手架:Longbridge(longbridge)v2 插件骨架 —— 市场数据服务 + 交易服务 + 三态闸门 + BYOK 凭证 ref。生成器:node scripts/new-connector.mjs --slug <exchange>",
4
- "version": "0.6.1",
3
+ "description": "长桥(Longbridge)港股连接器:直连券商 OpenAPI(openapi.longportapp.com,HMAC-SHA256 签名 + BYOK 三凭证 ref),实现 MarketDataService/TradeService 与 5 个 hk_* 工具;dry-run 缺省、liveTrading 只是非权威镜像(放行归 @dshtrading/authority 人工签署)、凭证只进 ref。",
4
+ "version": "0.6.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -21,12 +21,12 @@
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
23
  "dependencies": {
24
- "@dshtrading/authority": "^0.6.1",
25
- "@dshtrading/api": "^0.6.1"
24
+ "@dshtrading/authority": "^0.6.3",
25
+ "@dshtrading/api": "^0.6.3"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@deepseek-ai/cordis": ">=4.0.0",
29
- "@deepseek-ai/dsh-tools": ">=0.2.0-rc.2",
29
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.2",
30
30
  "@deepseek-ai/schemastery": ">=3.18.0"
31
31
  },
32
32
  "devDependencies": {