@dshtrading/connector-alpaca 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 +171 -25
- package/lib/dataplane.d.ts +2 -2
- package/lib/dataplane.js +5 -5
- package/lib/index.d.ts +34 -1
- package/lib/index.js +63 -3
- package/lib/rest.d.ts +6 -0
- package/lib/rest.js +5 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,34 +1,180 @@
|
|
|
1
|
-
# @dshtrading/connector-
|
|
1
|
+
# @dshtrading/connector-alpaca
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
美股(us)**行情 + 交易双面**连接器:直连 Alpaca Market Data v2(IEX feed)与 Alpaca Trading API v2,
|
|
4
|
+
实现 `@dshtrading/api` 的 `MarketDataService` / `TradeService` 两个契约,注册服务键
|
|
5
|
+
`tradingUsMarketData` / `tradingUsTrade`,注册 3 个 `us_*` 工具,并同时接入 base 的行情注册表
|
|
6
|
+
(`tradingMarketDataRegistry`)与交易注册表(`tradingTradeRegistry`)。被 us bundle 以
|
|
7
|
+
`workspace:*` 依赖(`packages/us/package.json`)。
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
## 状态
|
|
10
|
+
|
|
11
|
+
**未实证**:本包没有已提交的「真实 API key → 真机请求」原始响应;上游端点、字段映射与错误码全部取自
|
|
12
|
+
源码(下文逐条给出行号),**未经真机核对**。已提交的相关证据都在离线面:
|
|
13
|
+
|
|
14
|
+
- `spikes/impl-qmt-alpaca-dataplane-activation/run-loader-activation-probe.mjs`(2026-10-14):走真实 loader
|
|
15
|
+
激活路径(`plugin(模块, 行 config)` → `apply`),证明数据面行能同时注册行情与交易服务;**不触网**。
|
|
16
|
+
- `spikes/impl-orderbook-ticks/EVIDENCE.md`(2026-09-02,第 32 行):该轮 us 盘口/逐笔未接,理由是本连接器需凭证。
|
|
17
|
+
- `spikes/impl-service-seam-gate/NET-VERIFY.md`(2026-09-01,第 36 行):明确记录未对券商网关做真实下单验证。
|
|
18
|
+
- `.agents/notes/implemented/feature/2026-08-31-alpaca-us-and-futu-hk-connectors.md`:接入当日的离线测试记录。
|
|
19
|
+
|
|
20
|
+
## 配置
|
|
21
|
+
|
|
22
|
+
| 字段 | 默认 | 说明 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `enabled` | `false` | 总开关;`apply()` 首行判定,false 直接退出(`src/index.ts:451`) |
|
|
25
|
+
| `env` | `'demo'` | `demo` = Paper,`live` = 实盘;同时决定走哪一组凭证 ref(`src/index.ts:103`) |
|
|
26
|
+
| `dryRun` | `true` | 强制模拟:为 true 时即使请求 `dryRun:false` 也只回本地模拟回执(`src/index.ts:256`) |
|
|
27
|
+
| `liveTrading` | `false` | 实盘请求的**非权威镜像**;真实放行需 `@dshtrading/authority` 平面的人工签署授权(`liveTradingEnabled()`,`src/index.ts:249`) |
|
|
28
|
+
| `apiKeyRef` | `'ALPACA_API_KEY'` | live 环境 API key 的凭证 ref(只存 ref) |
|
|
29
|
+
| `secretRef` | `'ALPACA_SECRET_KEY'` | live 环境 secret 的凭证 ref |
|
|
30
|
+
| `demoApiKeyRef` | `'ALPACA_PAPER_API_KEY'` | demo(Paper)环境 API key 的凭证 ref |
|
|
31
|
+
| `demoSecretRef` | `'ALPACA_PAPER_SECRET_KEY'` | demo(Paper)环境 secret 的凭证 ref |
|
|
32
|
+
|
|
33
|
+
字段名与默认值见 `src/index.ts:56-65`。凭证面语义:
|
|
34
|
+
|
|
35
|
+
- **先按 env 选组,再三级解析**:`demo` 用 `demoApiKeyRef`/`demoSecretRef`,否则用 `apiKeyRef`/`secretRef`
|
|
36
|
+
(`src/index.ts:103-107`);解析顺序是 `tradingMarketRouter` 的 `alpaca` 凭据 → `credentials` 服务的
|
|
37
|
+
`resolve(ref)` → `process.env[ref]`(`src/index.ts:119-148`)。
|
|
38
|
+
- **ref 必须是指标识符**:不匹配 `^[A-Za-z_][A-Za-z0-9_]*$` 直接抛 `TRADING_CREDENTIALS_MISSING`
|
|
39
|
+
(`src/index.ts:92,126-131`);两者都拿不到时返回 undefined,apply 里的 `requireCreds` 抛
|
|
40
|
+
`TRADING_CREDENTIALS_MISSING`(`src/index.ts:456-465`),不降级到匿名请求。
|
|
41
|
+
- **凭证只进 ref、不落日志**:值只在请求头 `APCA-API-KEY-ID` / `APCA-API-SECRET-KEY` 内出现
|
|
42
|
+
(`src/rest.ts:102-111`),源码不含任何打印凭证的语句。
|
|
43
|
+
- **交易安全三件套**:下单默认 dry-run(工具参数 `dryRun` 默认 `true`,`src/index.ts:389`;服务内
|
|
44
|
+
`order.dryRun ?? true`,`src/index.ts:365`);`liveTrading` 必须显式开启且只是镜像,权威是签署平面
|
|
45
|
+
(`src/index.ts:364-377` 的 `evaluateOrderGate` 三态:`reject` / `simulate` / `live`);工具名
|
|
46
|
+
`us_place_order` 命中 base 统一审批闸门 `LIVE_ACTION_GATE_PATTERN`(该正则同时覆盖
|
|
47
|
+
`us_cancel_order`,`packages/base/src/index.ts:63`),无人在场时 ask 降级为 deny
|
|
48
|
+
(`packages/base/src/index.ts:20-24`)。
|
|
49
|
+
|
|
50
|
+
## 接线
|
|
51
|
+
|
|
52
|
+
- **数据面行**:`packages/us/cordis.patch.yml:26-29` → 行 id `dsh-trading-us-dataplane-alpaca`,
|
|
53
|
+
`name: '@dshtrading/connector-alpaca/dataplane'`,行 config 只写 `enabled: true`。
|
|
54
|
+
`src/dataplane.ts` 重导出 `Config`(第 34 行)让 loader 补全 schema 默认值,并在
|
|
55
|
+
`tradingUsMarketData` / `tradingUsTrade` 两个 isolate realm 内构造服务(第 68、77 行),
|
|
56
|
+
分别 `register('us', 'alpaca', …)` 进 `tradingMarketDataRegistry`(第 73 行)与
|
|
57
|
+
`tradingTradeRegistry`(第 88 行)。无注册表的老部署回退直接构造行情服务(第 64-67 行),
|
|
58
|
+
无交易注册表时跳过交易面(第 75-76 行)。
|
|
59
|
+
- **preset 行**:`packages/us/assets/preset/us-trader/agent.cordis.yml:47-59` → 组 id
|
|
60
|
+
`dsh-trading-us-connector-alpaca-group`,`isolate: {tradingUsMarketData, tradingUsTrade}`,
|
|
61
|
+
行 id `dsh-trading-us-connector-alpaca`,行 config `enabled: true` / `env: demo` / `dryRun: true` /
|
|
62
|
+
`liveTrading: false`。
|
|
63
|
+
- **provider 词汇**:`packages/router/src/index.ts:69`(`PROVIDER_VOCABULARY` 的 `'alpaca'`)。
|
|
64
|
+
- **设置页显示名**:`PROVIDER_LABELS` 的 `alpaca` 条目
|
|
65
|
+
(`packages/client-ui-settings/src/client/trading-settings-controller.ts:48`:`type: 'commercial'`、
|
|
66
|
+
`env: 'ALPACA_API_KEY'`、`markets: ['us']`)与 `packages/client-ui-settings/src/client/locales.ts`
|
|
67
|
+
的 `'provider.alpaca'` 条目(zh / en 都是 `Alpaca`)。凭据卡片字段 `apiKey` / `secretKey`
|
|
68
|
+
(`trading-settings-controller.ts:147-148`)由本包的 `resolveCredentials` 消费。
|
|
69
|
+
- **bundle 依赖**:`packages/us/package.json` 的 `@dshtrading/connector-alpaca: workspace:*`。
|
|
70
|
+
- **profile overrides**:`node scripts/sync-profile-overrides.mjs --all --dsh-home ~/.dsh-trading`
|
|
71
|
+
(注意 `--dsh-home`:Web 会话继承的 `DSH_HOME` 指向 ~/.dsh 会找错 profile 集)。
|
|
72
|
+
- **激活条件**:`routeAllows()` 要求 `enabled` 且 `tradingMarketRouter.activeProvider('us') === 'alpaca'`
|
|
73
|
+
(`src/index.ts:443-448`)。
|
|
74
|
+
|
|
75
|
+
## 上游端点
|
|
9
76
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
77
|
+
鉴权头 `APCA-API-KEY-ID` / `APCA-API-SECRET-KEY`(`src/rest.ts:102-111`)。数据面 host 固定
|
|
78
|
+
`https://data.alpaca.markets/v2`(`src/rest.ts:93`);交易面 host 由 `env` 决定(`src/rest.ts:94-97`):
|
|
79
|
+
`demo` → `https://paper-api.alpaca.markets/v2`,`live` → `https://api.alpaca.markets/v2`。
|
|
13
80
|
|
|
14
|
-
|
|
81
|
+
| 用途 | 端点 | 源码 |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| 最新成交 | `GET {data}/stocks/{sym}/trades/latest?feed=iex` | `src/rest.ts:161` |
|
|
84
|
+
| 最新报价(买一/卖一) | `GET {data}/stocks/{sym}/quotes/latest?feed=iex` | `src/rest.ts:162,246` |
|
|
85
|
+
| K 线 | `GET {data}/stocks/bars?symbols={sym}&timeframe={tf}&limit={n}&feed=iex&adjustment=split` | `src/rest.ts:209` |
|
|
86
|
+
| 标的名册 | `GET {trading}/assets?status=active&asset_class=us_equity` | `src/rest.ts:230` |
|
|
87
|
+
| 账户余额 | `GET {trading}/account` | `src/rest.ts:265` |
|
|
88
|
+
| 持仓 | `GET {trading}/positions` | `src/rest.ts:276` |
|
|
89
|
+
| 挂单 | `GET {trading}/orders?status=open[&symbols={sym}]` | `src/rest.ts:294` |
|
|
90
|
+
| 成交流水 | `GET {trading}/account/activities/FILL?page_size={n}` | `src/rest.ts:315` |
|
|
91
|
+
| 下单 | `POST {trading}/orders`(`time_in_force: gtc`,可带 `client_order_id`) | `src/rest.ts:348-365` |
|
|
92
|
+
| 撤单 | `DELETE {trading}/orders/{id}` | `src/rest.ts:392` |
|
|
93
|
+
| 单笔订单 | `GET {trading}/orders/{id}` | `src/rest.ts:402` |
|
|
15
94
|
|
|
16
|
-
|
|
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 用例。
|
|
95
|
+
## 周期与契约
|
|
22
96
|
|
|
23
|
-
|
|
97
|
+
- **周期词汇 9 个**:`1m/5m/15m/30m/1h/4h/1d/1w/1M` → Alpaca `timeframe` `1Min/5Min/15Min/30Min/1Hour/
|
|
98
|
+
4Hour/1Day/1Week/1Month`(`src/rest.ts:52-62`,词汇表 `src/rest.ts:76`)。未支持的周期抛
|
|
99
|
+
`TRADING_UNSUPPORTED_INTERVAL`(`src/rest.ts:205-207`),不静默降级。
|
|
100
|
+
- **K 线参数**:`limit` 夹取到 `[1, 1000]`、缺省 100(`src/rest.ts:208`,工具默认同为 100,
|
|
101
|
+
`src/index.ts:492`);固定 `feed=iex` 与 `adjustment=split`(`src/rest.ts:209`);
|
|
102
|
+
`closeTime = openTime + 周期毫秒 − 1`,其中 `1M` 按 30 天近似(`src/rest.ts:64-74,213-224`)。
|
|
103
|
+
- **符号形态**:`normalizeUsSymbol` 先 trim + 大写,再要求匹配 `^[A-Z]{1,10}(\.[A-Z]{1,4})?$`,
|
|
104
|
+
否则抛 `TRADING_UNSUPPORTED_SYMBOL`(`src/rest.ts:78-84`)。
|
|
105
|
+
- **取价语义**:`getTicker` 并发请求 trades 与 quotes(`Promise.allSettled`),两者都失败才抛错;无成交时
|
|
106
|
+
`price = (bid + ask) / 2` 派生(`src/rest.ts:160-199`)。
|
|
107
|
+
- **错误映射**(`src/rest.ts:132-152`):401/403 → `TRADING_AUTH_FAILED`;429 → `TRADING_RATE_LIMITED`;
|
|
108
|
+
404 → `TRADING_UNSUPPORTED_SYMBOL`;422 且消息含 `insufficient|buying power|balance` →
|
|
109
|
+
`TRADING_INSUFFICIENT_BALANCE`,否则 `TRADING_EXCHANGE_ERROR`;其余非 2xx → `TRADING_EXCHANGE_ERROR`;
|
|
110
|
+
请求本身抛错 → `TRADING_NETWORK`。单次请求超时 10 s(`src/rest.ts:99,121-122`)。
|
|
111
|
+
- **回执不许造 id**:下单响应没有真实非空 `id` 即抛 `TRADING_EXCHANGE_ERROR`(`src/rest.ts:369-375`);
|
|
112
|
+
撤单仅 404 幂等视作成功,401/429/网络/5xx 原样上抛(`src/rest.ts:390-399`)。
|
|
113
|
+
- **实盘幂等**:意图台账先落盘再触网(`src/index.ts:277-296`),账户定位固定占位 `'default'`
|
|
114
|
+
(`src/index.ts:86`),`client_order_id` 由 intentId 确定性派生、前缀 `dsht`、上限 48
|
|
115
|
+
(`src/index.ts:78,284`)。
|
|
116
|
+
- **服务类字段用 TS 编译期 `private`**(禁 ECMAScript `#`,cordis realm 代理会炸)。
|
|
24
117
|
|
|
25
|
-
|
|
26
|
-
`us`(工具前缀)/ `Us`(服务键 infix)。token 未展开也可编译;
|
|
27
|
-
生成器保证全部替换。
|
|
118
|
+
## 能力面
|
|
28
119
|
|
|
29
|
-
|
|
120
|
+
| 面 | 服务方法 | 状态 |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| 行情 | `getTicker` | 有(trades + quotes 聚合) |
|
|
123
|
+
| 行情 | `getKlines` | 有(9 个周期词汇) |
|
|
124
|
+
| 行情 | `subscribeTicker` | 有(快照轮询,缺省 5000 ms、下限 500 ms,`src/index.ts:196-204`) |
|
|
125
|
+
| 行情 | `listInstruments` | 有(走交易面 `/assets`,需凭证) |
|
|
126
|
+
| 行情 | `getOrderbook` | 有(仅一档:买一/卖一,`src/rest.ts:244-262`) |
|
|
127
|
+
| 行情 | `getTickers`(批量) | 缺席 |
|
|
128
|
+
| 行情 | `getFundamentals` / 衍生品指标 | 缺席 |
|
|
129
|
+
| 交易 | `placeOrder` | 有(三态闸门 + 意图台账) |
|
|
130
|
+
| 交易 | `cancelOrder` | 有(与下单同门槛:需 `liveTrading` 且 `dryRun=false`,`src/index.ts:300-305`) |
|
|
131
|
+
| 交易 | `getOrder` / `getPositions` / `listOpenOrders` / `listTradeFills` | 有 |
|
|
132
|
+
| 交易 | `getBalance` / `getBalances` | 有(`getBalances` 是单元素数组包装,`src/index.ts:241-245`) |
|
|
133
|
+
| 交易 | `setLeverage` | 缺席 |
|
|
134
|
+
|
|
135
|
+
注册的工具(`src/index.ts` 的 `register` 调用,同名已存在则跳过,第 472-474 行):
|
|
136
|
+
|
|
137
|
+
| 工具 | 注册位置 | 说明 |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `us_get_ticker` | `src/index.ts:476-484` | 最新价与买卖一档 |
|
|
140
|
+
| `us_get_klines` | `src/index.ts:486-499` | interval 用 9 词汇枚举,缺省 `1d`;limit 缺省 100 |
|
|
141
|
+
| `us_place_order` | `src/index.ts:379-439`(第 501 行注册) | 先过 `evaluateOrderGate`;模拟回执带行情参照价 `referencePrice` |
|
|
142
|
+
|
|
143
|
+
## 已知边界
|
|
144
|
+
|
|
145
|
+
- **未实证项**:全部上游端点、IEX feed 的实际覆盖、真机错误码分布均未在本出口验证过;接入前的
|
|
146
|
+
`spikes/impl-service-seam-gate/NET-VERIFY.md:36` 也记录了未对券商网关做真实下单验证。
|
|
147
|
+
- **`feed=iex` 是单所行情**:行情请求固定 `feed=iex`(`src/rest.ts:161-162,209`),非 SIP 全市场
|
|
148
|
+
consolidated feed;覆盖差异未测。
|
|
149
|
+
- **工具描述比词汇窄**:`us_get_klines` 的 description 只列 `5m/15m/30m/1h/1d/1w/1M`,而 enum 词汇
|
|
150
|
+
含 `1m` 与 `4h`(`src/index.ts:488-491` vs `src/rest.ts:76`);两者不一致,实际按 enum 放行。
|
|
151
|
+
- **盘口只有一档**:`getOrderbook` 用 `quotes/latest` 的 `bp`/`ap` 各造一行,数量字段缺省填 100
|
|
152
|
+
(`src/rest.ts:248-255`),不是多档深度(`spikes/impl-orderbook-ticks/EVIDENCE.md:32` 记的
|
|
153
|
+
「us 盘口未实现」指该轮桥面未接,与连接器自身的单档实现不冲突)。
|
|
154
|
+
- **读面静默吞错**:`getPositions` / `listOpenOrders` / `listTradeFills` 捕获所有异常返回 `[]`
|
|
155
|
+
(`src/index.ts:315-340`)——凭证缺失或网络故障会被读成「无持仓/无挂单/无成交」。
|
|
156
|
+
- **撤单没有工具入口**:`AlpacaTradeService.cancelOrder` 存在,但本包不注册 `us_cancel_order`
|
|
157
|
+
(工具只有 3 个);撤单只能经服务面且需实盘授权。
|
|
158
|
+
- **名册未暴露为工具**:`listInstruments` 只在服务面,工具面没有对应入口。
|
|
159
|
+
- **老部署回退**:无 `tradingMarketDataRegistry` 时直接构造服务(`src/dataplane.ts:64-67`),
|
|
160
|
+
此时 settings 切换交易所需重启才生效(数据面注释,第 6-10 行)。
|
|
161
|
+
- **`1M` 的 closeTime 按 30 天近似**(`src/rest.ts:73`),不是自然月长度。
|
|
162
|
+
|
|
163
|
+
## 合规记录
|
|
164
|
+
|
|
165
|
+
**Alpaca 是商业券商官方 API,需用户自备 key(BYOK)**:行情走 Market Data v2 的 IEX feed,交易走
|
|
166
|
+
Trading API v2 的 Paper/Live host;凭证只以 ref 形式进配置,值不落日志、不内置默认值。本仓不缓存、
|
|
167
|
+
不再分发任何行情或账户数据;使用边界以 Alpaca 服务条款(<https://alpaca.markets>,设置页同址登记)
|
|
168
|
+
为准。合规要求见 README 铁律 #5。
|
|
169
|
+
|
|
170
|
+
## 出网验证
|
|
171
|
+
|
|
172
|
+
本包暂无已提交的独立出网验证脚本。可复跑的入口(都不触网):
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
pnpm --filter @dshtrading/connector-alpaca test # 离线契约测试(fake venue,无真实网络)
|
|
176
|
+
node spikes/impl-qmt-alpaca-dataplane-activation/run-loader-activation-probe.mjs # loader 激活路径(需已构建 lib/)
|
|
177
|
+
```
|
|
30
178
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- 行 id = `dsh-trading-<market>-connector-<slug>`,全仓唯一(insert-only 铁律 #1)。
|
|
34
|
-
- 凭证只进 ref,绝不内置/落日志(铁律 #3/#5)。
|
|
179
|
+
真机出网需自备 `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`(或 Paper 组)并自写探针:本仓未提交任何
|
|
180
|
+
携带真实凭证的请求记录,也没有已提交的真实原始响应可供引用。
|
package/lib/dataplane.d.ts
CHANGED
|
@@ -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
|
|
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,6 +1,8 @@
|
|
|
1
|
-
import { AlpacaMarketDataService, AlpacaTradeService, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, credentialRefsFor, resolveCredentials } from "./index.js";
|
|
1
|
+
import { AlpacaMarketDataService, AlpacaTradeService, Config, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, credentialRefsFor, resolveCredentials } from "./index.js";
|
|
2
2
|
//#region src/dataplane.ts
|
|
3
3
|
const inject = [];
|
|
4
|
+
/** schema 的运行时可调用形态(schemastery 校验即补默认值);手工 ctx 不传 config 时用。 */
|
|
5
|
+
const withDefaults = (value) => Config(value);
|
|
4
6
|
/** 解析注册表服务;老部署(base/router 未升级)返回 undefined → 调用方回退直接 provide。 */
|
|
5
7
|
function resolveMarketDataRegistry(ctx) {
|
|
6
8
|
const candidate = ctx.get?.("tradingMarketDataRegistry", false);
|
|
@@ -11,11 +13,9 @@ function resolveTradeRegistry(ctx) {
|
|
|
11
13
|
const candidate = ctx.get?.("tradingTradeRegistry", false);
|
|
12
14
|
return candidate !== void 0 ? candidate : void 0;
|
|
13
15
|
}
|
|
14
|
-
/** 本连接器的路由 provider slug(路由层词汇 = 交易所 slug,docs/exchange-routing.md §2.2)。 */
|
|
15
|
-
const ROUTER_PROVIDER = "alpaca";
|
|
16
16
|
/** 市场短前缀。 */
|
|
17
17
|
const MARKET = "us";
|
|
18
|
-
function apply(ctx, config) {
|
|
18
|
+
function apply(ctx, config = withDefaults(void 0)) {
|
|
19
19
|
if (!config.enabled) return;
|
|
20
20
|
const registry = resolveMarketDataRegistry(ctx);
|
|
21
21
|
if (registry === void 0) {
|
|
@@ -44,4 +44,4 @@ function apply(ctx, config) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
//#endregion
|
|
47
|
-
export { apply, inject };
|
|
47
|
+
export { Config, apply, inject };
|
package/lib/index.d.ts
CHANGED
|
@@ -18,6 +18,23 @@ declare const Config: Schema<Config>;
|
|
|
18
18
|
declare const inject: string[];
|
|
19
19
|
declare const TRADING_US_MARKET_DATA_KEY = "tradingUsMarketData";
|
|
20
20
|
declare const TRADING_US_TRADE_KEY = "tradingUsTrade";
|
|
21
|
+
/**
|
|
22
|
+
* Alpaca 官方 `client_order_id` 规则:官方 OpenAPI 注明 `maxLength: 48`
|
|
23
|
+
* (alpaca-docs 仓的 oas/broker/openapi.yaml;"Automatically generated if not sent")。
|
|
24
|
+
* 官方文档**没有**承诺重复 client_order_id 幂等:同 id 只在订单仍活跃时被拒(422),
|
|
25
|
+
* 终态后可以再次使用——所以它只是第二层防护,跨时间 exactly-once 由本地意图台账保证。
|
|
26
|
+
*/
|
|
27
|
+
declare const ALPACA_CLIENT_ORDER_ID_RULE: {
|
|
28
|
+
readonly prefix: "dsht";
|
|
29
|
+
readonly maxLength: 48;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 台账账户定位:Alpaca 插件配置里没有非密账户字段(账户由 API key 决定),用稳定占位;
|
|
33
|
+
* 环境(demo/live)承担账户区分。凭据轮换不进身份,因此不会让未知意图看起来是新意图。
|
|
34
|
+
* 边界:把同一实例的凭据换到另一个 Alpaca 账户而 env 不变时台账无法区分——此时未知意图
|
|
35
|
+
* 仍需人工/读对账推进(台账本身依旧拒绝自动重发,最坏结果是保守地不重发)。
|
|
36
|
+
*/
|
|
37
|
+
declare const ALPACA_INTENT_ACCOUNT = "default";
|
|
21
38
|
interface CredentialResolverLike {
|
|
22
39
|
resolve(ref: string): Promise<{
|
|
23
40
|
value: string;
|
|
@@ -52,6 +69,11 @@ declare class AlpacaTradeService extends Service implements TradeService {
|
|
|
52
69
|
/** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
|
|
53
70
|
private readonly config;
|
|
54
71
|
private readonly getCredentials;
|
|
72
|
+
/**
|
|
73
|
+
* 订单意图台账(实盘幂等前提;写 agent 自己的 trading home,不写授权平面)。
|
|
74
|
+
* 环境进作用域:paper 与 live 是两套账户,同一 intentId 跨环境必须冲突而不是重发。
|
|
75
|
+
*/
|
|
76
|
+
private readonly intents;
|
|
55
77
|
constructor(ctx: Context, options: AlpacaRestOptions & {
|
|
56
78
|
config: Config;
|
|
57
79
|
}, getCredentials: () => Promise<AlpacaCredentials>, serviceName?: string);
|
|
@@ -71,6 +93,13 @@ interface PlaceOrderArgs {
|
|
|
71
93
|
readonly quantity: number;
|
|
72
94
|
readonly price?: number;
|
|
73
95
|
readonly dryRun?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Stable idempotency id for a live order (required when dryRun=false, optional for
|
|
98
|
+
* simulations). Retrying with the same id and the same normalized request returns the
|
|
99
|
+
* recorded venue receipt instead of placing a second order; a different payload,
|
|
100
|
+
* provider, environment or account under the same id is rejected as a conflict.
|
|
101
|
+
*/
|
|
102
|
+
readonly intentId?: string;
|
|
74
103
|
}
|
|
75
104
|
type OrderGateVerdict = {
|
|
76
105
|
action: 'reject';
|
|
@@ -87,8 +116,12 @@ declare function createPlaceOrderTool(deps: {
|
|
|
87
116
|
trade?: TradeService;
|
|
88
117
|
config: Config;
|
|
89
118
|
}): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
119
|
+
/**
|
|
120
|
+
* 本连接器在路由层的 provider slug(路由层词汇 = 交易所 slug;
|
|
121
|
+
* docs/guides/exchange-routing.md §2.2)。
|
|
122
|
+
*/
|
|
90
123
|
declare const ROUTER_PROVIDER = "alpaca";
|
|
91
124
|
declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
|
|
92
125
|
declare function apply(ctx: Context, config: Config): void;
|
|
93
126
|
//#endregion
|
|
94
|
-
export { type AccountBalance, AlpacaCredentials, AlpacaMarketDataService, AlpacaRestClient, AlpacaRestOptions, AlpacaTradeService, Config, CredentialResolverLike, CredentialsContext, INTERVAL_TO_ALPACA, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, OrderGateVerdict, type Orderbook, PlaceOrderArgs, type Position, ROUTER_PROVIDER, ResolvedCredentialRefs, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, type Ticker, type TradeFill, TradingServiceError, apply, createPlaceOrderTool, credentialRefsFor, evaluateOrderGate, inject, name, normalizeUsSymbol, resolveCredentials, routeAllows };
|
|
127
|
+
export { ALPACA_CLIENT_ORDER_ID_RULE, ALPACA_INTENT_ACCOUNT, type AccountBalance, AlpacaCredentials, AlpacaMarketDataService, AlpacaRestClient, AlpacaRestOptions, AlpacaTradeService, Config, CredentialResolverLike, CredentialsContext, INTERVAL_TO_ALPACA, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, OrderGateVerdict, type Orderbook, PlaceOrderArgs, type Position, ROUTER_PROVIDER, ResolvedCredentialRefs, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, type Ticker, type TradeFill, TradingServiceError, apply, createPlaceOrderTool, credentialRefsFor, evaluateOrderGate, inject, name, normalizeUsSymbol, resolveCredentials, routeAllows };
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AlpacaRestClient, INTERVAL_TO_ALPACA, INTERVAL_VOCABULARY, TradingServiceError, normalizeUsSymbol } 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";
|
|
@@ -23,6 +24,23 @@ const Config = Schema.object({
|
|
|
23
24
|
const inject = ["tools"];
|
|
24
25
|
const TRADING_US_MARKET_DATA_KEY = "tradingUsMarketData";
|
|
25
26
|
const TRADING_US_TRADE_KEY = "tradingUsTrade";
|
|
27
|
+
/**
|
|
28
|
+
* Alpaca 官方 `client_order_id` 规则:官方 OpenAPI 注明 `maxLength: 48`
|
|
29
|
+
* (alpaca-docs 仓的 oas/broker/openapi.yaml;"Automatically generated if not sent")。
|
|
30
|
+
* 官方文档**没有**承诺重复 client_order_id 幂等:同 id 只在订单仍活跃时被拒(422),
|
|
31
|
+
* 终态后可以再次使用——所以它只是第二层防护,跨时间 exactly-once 由本地意图台账保证。
|
|
32
|
+
*/
|
|
33
|
+
const ALPACA_CLIENT_ORDER_ID_RULE = {
|
|
34
|
+
prefix: "dsht",
|
|
35
|
+
maxLength: 48
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* 台账账户定位:Alpaca 插件配置里没有非密账户字段(账户由 API key 决定),用稳定占位;
|
|
39
|
+
* 环境(demo/live)承担账户区分。凭据轮换不进身份,因此不会让未知意图看起来是新意图。
|
|
40
|
+
* 边界:把同一实例的凭据换到另一个 Alpaca 账户而 env 不变时台账无法区分——此时未知意图
|
|
41
|
+
* 仍需人工/读对账推进(台账本身依旧拒绝自动重发,最坏结果是保守地不重发)。
|
|
42
|
+
*/
|
|
43
|
+
const ALPACA_INTENT_ACCOUNT = "default";
|
|
26
44
|
const CREDENTIAL_REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
27
45
|
function credentialRefsFor(config, env = config.env) {
|
|
28
46
|
return env === "demo" ? {
|
|
@@ -106,11 +124,21 @@ var AlpacaTradeService = class extends Service {
|
|
|
106
124
|
/** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
|
|
107
125
|
config;
|
|
108
126
|
getCredentials;
|
|
127
|
+
/**
|
|
128
|
+
* 订单意图台账(实盘幂等前提;写 agent 自己的 trading home,不写授权平面)。
|
|
129
|
+
* 环境进作用域:paper 与 live 是两套账户,同一 intentId 跨环境必须冲突而不是重发。
|
|
130
|
+
*/
|
|
131
|
+
intents;
|
|
109
132
|
constructor(ctx, options, getCredentials, serviceName = TRADING_US_TRADE_KEY) {
|
|
110
133
|
super(ctx, serviceName);
|
|
111
134
|
this.client = new AlpacaRestClient(options);
|
|
112
135
|
this.config = options.config;
|
|
113
136
|
this.getCredentials = getCredentials;
|
|
137
|
+
this.intents = new OrderIntentLedger({
|
|
138
|
+
provider: "alpaca",
|
|
139
|
+
environment: options.config.env,
|
|
140
|
+
account: ALPACA_INTENT_ACCOUNT
|
|
141
|
+
});
|
|
114
142
|
}
|
|
115
143
|
async getBalance() {
|
|
116
144
|
const creds = await this.getCredentials();
|
|
@@ -134,14 +162,37 @@ var AlpacaTradeService = class extends Service {
|
|
|
134
162
|
dryRun: true,
|
|
135
163
|
timestamp: Date.now()
|
|
136
164
|
};
|
|
165
|
+
const payload = {
|
|
166
|
+
symbol: order.symbol.toUpperCase(),
|
|
167
|
+
side: order.side,
|
|
168
|
+
type: order.type,
|
|
169
|
+
quantity: order.quantity,
|
|
170
|
+
...order.price !== void 0 ? { price: order.price } : {}
|
|
171
|
+
};
|
|
172
|
+
const decision = await this.intents.begin({
|
|
173
|
+
intentId: order.intentId,
|
|
174
|
+
payload
|
|
175
|
+
});
|
|
176
|
+
if (decision.action === "replay") return {
|
|
177
|
+
...orderFromReceipt(decision.receipt),
|
|
178
|
+
status: decision.receipt.status
|
|
179
|
+
};
|
|
180
|
+
const clientOrderId = deriveVenueClientKey(decision.intentId, ALPACA_CLIENT_ORDER_ID_RULE);
|
|
137
181
|
const creds = await this.getCredentials();
|
|
138
|
-
|
|
182
|
+
const placed = await this.client.placeOrder(creds, {
|
|
139
183
|
symbol: order.symbol,
|
|
140
184
|
side: order.side.toUpperCase(),
|
|
141
185
|
type: order.type.toUpperCase(),
|
|
142
186
|
quantity: order.quantity,
|
|
143
|
-
price: order.price
|
|
187
|
+
price: order.price,
|
|
188
|
+
clientOrderId
|
|
189
|
+
});
|
|
190
|
+
await this.intents.confirm({
|
|
191
|
+
intentId: decision.intentId,
|
|
192
|
+
payload,
|
|
193
|
+
receipt: receiptFromOrder(placed)
|
|
144
194
|
});
|
|
195
|
+
return placed;
|
|
145
196
|
}
|
|
146
197
|
async cancelOrder(orderId, _symbol) {
|
|
147
198
|
if (!liveTradingEnabled(this.config.liveTrading) || this.config.dryRun) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", "Alpaca 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.");
|
|
@@ -222,6 +273,10 @@ function createPlaceOrderTool(deps) {
|
|
|
222
273
|
type: "boolean",
|
|
223
274
|
default: true,
|
|
224
275
|
description: "Simulate order only (default true)"
|
|
276
|
+
},
|
|
277
|
+
intentId: {
|
|
278
|
+
type: "string",
|
|
279
|
+
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."
|
|
225
280
|
}
|
|
226
281
|
},
|
|
227
282
|
output: {
|
|
@@ -263,12 +318,17 @@ function createPlaceOrderTool(deps) {
|
|
|
263
318
|
type: args.type === "LIMIT" ? "limit" : "market",
|
|
264
319
|
quantity: args.quantity,
|
|
265
320
|
price: args.price,
|
|
321
|
+
...args.intentId !== void 0 ? { intentId: args.intentId } : {},
|
|
266
322
|
dryRun: false
|
|
267
323
|
});
|
|
268
324
|
return JSON.stringify(order);
|
|
269
325
|
}
|
|
270
326
|
});
|
|
271
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* 本连接器在路由层的 provider slug(路由层词汇 = 交易所 slug;
|
|
330
|
+
* docs/guides/exchange-routing.md §2.2)。
|
|
331
|
+
*/
|
|
272
332
|
const ROUTER_PROVIDER = "alpaca";
|
|
273
333
|
function routeAllows(ctx, config, market) {
|
|
274
334
|
if (!config.enabled) return false;
|
|
@@ -356,4 +416,4 @@ function apply(ctx, config) {
|
|
|
356
416
|
});
|
|
357
417
|
}
|
|
358
418
|
//#endregion
|
|
359
|
-
export { AlpacaMarketDataService, AlpacaRestClient, AlpacaTradeService, Config, INTERVAL_TO_ALPACA, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, TradingServiceError, apply, createPlaceOrderTool, credentialRefsFor, evaluateOrderGate, inject, name, normalizeUsSymbol, resolveCredentials, routeAllows };
|
|
419
|
+
export { ALPACA_CLIENT_ORDER_ID_RULE, ALPACA_INTENT_ACCOUNT, AlpacaMarketDataService, AlpacaRestClient, AlpacaTradeService, Config, INTERVAL_TO_ALPACA, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, TradingServiceError, apply, createPlaceOrderTool, credentialRefsFor, evaluateOrderGate, inject, name, normalizeUsSymbol, resolveCredentials, routeAllows };
|
package/lib/rest.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ declare class AlpacaRestClient {
|
|
|
43
43
|
type: 'MARKET' | 'LIMIT';
|
|
44
44
|
quantity: number;
|
|
45
45
|
price?: number;
|
|
46
|
+
/**
|
|
47
|
+
* 客户端订单 id(Alpaca 官方 `client_order_id`,maxLength 48)。由 TradeService 从
|
|
48
|
+
* `OrderRequest.intentId` 确定性派生;官方只在订单**仍活跃**时拒绝重复 id,
|
|
49
|
+
* 因此它是第二层防护,不是跨时间 exactly-once。
|
|
50
|
+
*/
|
|
51
|
+
clientOrderId?: string;
|
|
46
52
|
}): Promise<Order$1>;
|
|
47
53
|
cancelOrder(credentials: AlpacaCredentials, orderId: string): Promise<void>;
|
|
48
54
|
getOrder(credentials: AlpacaCredentials, orderId: string): Promise<Order$1>;
|
package/lib/rest.js
CHANGED
|
@@ -250,18 +250,21 @@ var AlpacaRestClient = class {
|
|
|
250
250
|
time_in_force: "gtc"
|
|
251
251
|
};
|
|
252
252
|
if (req.type === "LIMIT" && req.price !== void 0) body.limit_price = String(req.price);
|
|
253
|
+
if (req.clientOrderId !== void 0) body.client_order_id = req.clientOrderId;
|
|
253
254
|
const data = await this.request(this.tradingBaseUrl, "/orders", {
|
|
254
255
|
method: "POST",
|
|
255
256
|
headers: { "Content-Type": "application/json" },
|
|
256
257
|
body: JSON.stringify(body)
|
|
257
258
|
}, credentials);
|
|
259
|
+
const id = typeof data.id === "string" && data.id.trim() !== "" ? data.id : void 0;
|
|
260
|
+
if (id === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Alpaca place order for ${sym}: response carries no real order id — the order is not confirmed`);
|
|
258
261
|
return {
|
|
259
|
-
id
|
|
262
|
+
id,
|
|
260
263
|
symbol: sym,
|
|
261
264
|
side: req.side.toLowerCase() === "sell" ? "sell" : "buy",
|
|
262
265
|
type: req.type.toLowerCase() === "limit" ? "limit" : "market",
|
|
263
266
|
quantity: req.quantity,
|
|
264
|
-
price: req.price,
|
|
267
|
+
...req.price !== void 0 ? { price: req.price } : {},
|
|
265
268
|
status: String(data.status ?? "pending"),
|
|
266
269
|
dryRun: false,
|
|
267
270
|
timestamp: Date.now()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/connector-alpaca",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.6.
|
|
3
|
+
"description": "美股(us)行情与交易连接器:直连 Alpaca Market Data v2(IEX feed)与 Trading API v2(Paper/Live),实现 MarketDataService/TradeService 双契约,服务键 tradingUsMarketData/tradingUsTrade,三态闸门 + dry-run 缺省 + BYOK 凭证 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.
|
|
25
|
-
"@dshtrading/api": "^0.6.
|
|
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.
|
|
29
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.2",
|
|
30
30
|
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|