@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 CHANGED
@@ -1,34 +1,180 @@
1
- # @dshtrading/connector-template
1
+ # @dshtrading/connector-alpaca
2
2
 
3
- **本包是脚手架源,不是可安装的连接器**:不入任何 bundle 依赖,仅在 `pnpm -r build/test`
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
- ```sh
7
- node scripts/new-connector.mjs --slug bybit --title Bybit --market crypto
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
- 生成后按 [docs/guides/connector-playbook.md](../../docs/guides/connector-playbook.md) 的填写顺序
11
- (src/rest.ts 头部清单 7 项)实现 TODO。每个 TODO 的参照段都在 connector-okx
12
- (packages/connector-okx/),它是本仓第一个真实 TradeService 连接器。
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
- - `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 用例。
95
+ ## 周期与契约
22
96
 
23
- ## Token
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
- `alpaca`(slug)/ `Alpaca`(title)/ `ALPACA`(ref 前缀)/
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
- - 服务类用 TS 编译期 `private`(禁 ECMAScript `#`,realm 代理会炸)。
32
- - 工具名 = 市场前缀 + 语义词(不带交易所名)——base 闸门正则只认市场前缀。
33
- - 行 id = `dsh-trading-<market>-connector-<slug>`,全仓唯一(insert-only 铁律 #1)。
34
- - 凭证只进 ref,绝不内置/落日志(铁律 #3/#5)。
179
+ 真机出网需自备 `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`(或 Paper 组)并自写探针:本仓未提交任何
180
+ 携带真实凭证的请求记录,也没有已提交的真实原始响应可供引用。
@@ -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,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
- return this.client.placeOrder(creds, {
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: String(data.id ?? `alpaca-${Date.now()}`),
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": "【模板】交易所连接器插件脚手架:Alpaca(alpaca)v2 插件骨架 —— 市场数据服务 + 交易服务 + 三态闸门 + BYOK 凭证 ref。生成器:node scripts/new-connector.mjs --slug <exchange>",
4
- "version": "0.6.1",
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.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": {