@dshtrading/connector-polygon 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,178 @@
1
- # @dshtrading/connector-template
1
+ # @dshtrading/connector-polygon
2
2
 
3
- **本包是脚手架源,不是可安装的连接器**:不入任何 bundle 依赖,仅在 `pnpm -r build/test`
4
- 基线内保证可编译、冒烟测试绿。真实接入用生成器:
3
+ > **状态:未实证(本仓无该上游的出网证据)**。截至 2026-10-14 检索:`spikes/` 下没有针对 Polygon.io
4
+ > 的出网脚本或原始响应目录;[`spikes/impl-service-seam-gate/NET-VERIFY.md`](../../spikes/impl-service-seam-gate/NET-VERIFY.md)
5
+ > 的真实网络验证只覆盖 OKX 公共端点,其「范围与未验证项」一节明确写明 fmp/polygon/finnhub 等模拟 stub
6
+ > 的服务缝三态由离线矩阵 `test/trade-gate.test.ts` 覆盖。另 [`spikes/impl-orderbook-ticks/EVIDENCE.md`](../../spikes/impl-orderbook-ticks/EVIDENCE.md)
7
+ > 第 32 行记有「us 盘口未实现:alpaca/polygon/ibkr 均需凭证或本地网关,本轮不接」——那是**未接**的结论,
8
+ > 不是能力证据。本 README 的端点、字段与能力面全部读自源码,不含任何实测结论。
5
9
 
6
- ```sh
7
- node scripts/new-connector.mjs --slug bybit --title Bybit --market crypto
8
- ```
10
+ Polygon.io(Massive)商业数据源美股连接器(`us` 市场,BYOK),实现 `@dshtrading/api` 的
11
+ `MarketDataService` 与 `TradeService`,被 `packages/us` bundle 以 `workspace:*` 依赖
12
+ (`packages/us/package.json` 第 35 行)。对外提供报价 / K 线 / 标的资料三个工具;
13
+ **不注册任何下单或撤单工具**,交易服务只经服务注入面可达。
14
+
15
+ ## 配置
16
+
17
+ | 字段 | 默认 | 说明 |
18
+ |---|---|---|
19
+ | `enabled` | `true` | 是否激活 Polygon 连接器(`src/index.ts:42`);`false` 时 `apply` 静默退出(`src/index.ts:165`) |
20
+ | `dryRun` | `true` | 默认模拟下单(`src/index.ts:43`);为真时 `placeOrder` 走本地模拟回执(`src/index.ts:116`) |
21
+ | `liveTrading` | `false` | 是否允许实盘交易(`src/index.ts:44`);非权威镜像,实盘授权由 `@dshtrading/authority` 的人工签署平面裁定(`src/index.ts:88`) |
22
+ | `apiKeyRef` | `'POLYGON_API_KEY'` | Polygon API Key 的环境变量名(BYOK)(`src/index.ts:45`) |
23
+
24
+ ## 接线
25
+
26
+ - **数据面行**:`packages/us/cordis.patch.yml` 第 38-41 行 → 行 id `dsh-trading-us-dataplane-polygon`,
27
+ 入口 `@dshtrading/connector-polygon/dataplane`,行 config 只写 `enabled: true`;
28
+ 该入口把 `(us, polygon)` 注册进 `tradingMarketDataRegistry`(`src/dataplane.ts:24-25,38-40`)。
29
+ - **凭证解析只有一份**(`createApiKeyProvider`,`src/index.ts:172-176`):设置中心
30
+ `dshtrading.credentials.polygon.apiKey`(router `getCredential('polygon').apiKey`)优先、
31
+ `process.env[config.apiKeyRef]` 兜底,**每次请求解析**(惰性,`src/rest.ts:116`)——settings 用户层
32
+ 加载/修改晚于插件 `apply` 也生效。主入口(`src/index.ts:183`)与数据面行(`src/dataplane.ts:32`)
33
+ 同用这一份,故设置页填的 key 在宿主面行情注册行与 preset 工具面都生效。
34
+ - **preset 行**:`packages/us/assets/preset/us-trader/agent.cordis.yml` 第 90-102 行 → 组 id
35
+ `dsh-trading-us-connector-polygon-group`(`isolate` 键 `tradingUsMarketData` + `tradingUsTrade`,第 93-95 行),
36
+ 组内行 id `dsh-trading-us-connector-polygon`(第 97 行),行 config 只写 `enabled/dryRun/liveTrading`(第 100-102 行)。
37
+ - **bundle 依赖**:`packages/us/package.json:35`(`workspace:*`)。
38
+ - **provider 词汇**:`packages/router/src/index.ts:72` 的 `PROVIDER_VOCABULARY` 含 `polygon`;
39
+ 本包导出 `ROUTER_PROVIDER = 'polygon'`(`src/index.ts:155`),`routeAllows` 只在 `us` 市场当前激活
40
+ provider 为 `polygon` 时激活本实例(`src/index.ts:157-162`)。
41
+ - **设置页显示名**:`PROVIDER_LABELS` 的 `polygon` 条目
42
+ (`packages/client-ui-settings/src/client/trading-settings-controller.ts:51`:
43
+ `{ id: 'polygon', label: 'provider.polygon', url: 'https://polygon.io', env: 'POLYGON_API_KEY', type: 'commercial', markets: ['us'] }`),
44
+ 文案见 `packages/client-ui-settings/src/client/locales.ts` 的 `'provider.polygon'` 条目
45
+ (zh / en 都是 `Polygon.io (Massive)`),界面显示 `Polygon.io (Massive)`。凭据卡片字段 `apiKey`
46
+ (`trading-settings-controller.ts:157`)由本包的 `getCredential` 读点消费。
47
+ - **服务键**:`tradingUsMarketData`(`src/index.ts:48`)、`tradingUsTrade`(`src/index.ts:49`)。
48
+
49
+ ## 上游端点
50
+
51
+ 基址 `https://api.polygon.io`(`src/rest.ts:96`);API key 以 `apiKey=` 查询参数附加(`src/rest.ts:103`)。
52
+
53
+ | 用途 | 端点 | 源码 |
54
+ |---|---|---|
55
+ | 报价(上一交易日收盘聚合,默认 `adjusted=true` 复权;`t` 为聚合窗口起点 Unix 毫秒) | `GET /v2/aggs/ticker/<SYM>/prev` | `src/rest.ts:141` |
56
+ | K 线 | `GET /v2/aggs/ticker/<SYM>/range/<multiplier>/<timespan>/<from>/<to>?limit=<limit>&sort=asc` | `src/rest.ts:173` |
57
+ | 标的资料 | `GET /v3/reference/tickers/<SYM>` | `src/rest.ts:192` |
58
+
59
+ ## 周期与契约
60
+
61
+ - **周期词汇** `1m/5m/15m/30m/1h/4h/1d/1w/1M`(`src/rest.ts:29`),映射到 Polygon
62
+ `multiplier/timespan`:分钟 1/5/15/30,小时 1/4,日/周/月 1 day / 1 week / 1 month
63
+ (`src/rest.ts:48-61`)。**周线与月线是上游原生粒度**(与 connector-fmp 的日线退化不同)。
64
+ `toPolygonTimespan` / `parseIntervalMs` 先过 `assertSupportedInterval`(`src/rest.ts:39-46`):
65
+ 与 connector-xysz 同**错误码与消息风格**,但签名不同——xysz 那支是 `: void`
66
+ (`packages/connector-xysz/src/rest.ts:34`),本包是类型断言
67
+ `asserts interval is SupportedInterval`,把 `interval` 收窄到 9 档联合,使下游 `switch` 穷尽、
68
+ 不留产出 `undefined` 的路径。**表外周期(`3m/2h/6h/8h/12h/3d`)显式抛
69
+ `TRADING_UNSUPPORTED_INTERVAL`**;`getKlines` 顶部同样先过闸
70
+ (`src/rest.ts:157`),表外周期在发请求之前就抛。工具参数的 enum 直接用该词汇表
71
+ (`src/index.ts:195`),表外周期在工具参数校验处即被拒。
72
+ - **报价是「上一交易日聚合」,不是实时报价**:`getTicker` 打的是 `/prev`(`src/rest.ts:141`),
73
+ 返回体 `results[0]` 的 `c/h/l/o/v/t` 即价格与上一交易日成交量;`Ticker.timestamp` 直接取 `row.t`
74
+ (`src/rest.ts:147-153`)。已核对官方字段定义:`/prev` = Previous Close,返回**上一交易日**的
75
+ OHLC 聚合,默认 `adjusted=true` → `c` 是**复权**价;`results[].t` 是聚合窗口起点的 **Unix 毫秒**,
76
+ 与本仓 `Ticker.timestamp` 的 epoch ms 契约一致,**无需秒→毫秒换算**。该语义已写进
77
+ `us_get_ticker` 的工具描述(`src/index.ts:182`),工具不再暗示实时报价。
78
+ - **K 线窗口**:`to = 今天`、`from = 今天 - limit × 周期毫秒 × 2`,都是 `YYYY-MM-DD`(`src/rest.ts:161-162`),
79
+ `limit` 原样透传给上游(`src/rest.ts:173`);`results` 缺失或非数组时静默返回 `[]`(`src/rest.ts:175-177`),
80
+ `closeTime = t + 周期 - 1 ms`(`src/rest.ts:186`)。
81
+ - **符号形态**:`trim()` + 大写,只做规范化、不校验美股形态(`src/rest.ts:78-82`);空串抛
82
+ `TRADING_INVALID_ARGUMENT`。
83
+ - **报价取值**:`results` 缺失或空数组 → `TRADING_SYMBOL_NOT_FOUND`(`src/rest.ts:143-145`)。
84
+ - **错误映射**:上游非 2xx → `TRADING_UPSTREAM_ERROR`,消息带 HTTP 状态与 `statusText`(`src/rest.ts:110-112`);
85
+ `fetch` 抛错(不可达、响应非 JSON 等)→ `TRADING_NETWORK`(`src/rest.ts:114-120`)。错误载体
86
+ `TradingServiceError` 携带 `code`(`src/rest.ts:17-26`)。
87
+ - 服务类成员用 TS 编译期 `private`(禁 ECMAScript `#`,cordis realm 代理会炸;`src/index.ts:52,90`)。
9
88
 
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 连接器。
89
+ ## 能力面
13
90
 
14
- ## 结构
91
+ **注册工具**(`apply` 内 `ctx.inject(['tools'])`,`src/index.ts:174-215`,注册前先 `tools.get` 判重):
15
92
 
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 用例。
93
+ | 工具 | 源码 |
94
+ |---|---|
95
+ | `us_get_ticker` | `src/index.ts:180-188` |
96
+ | `us_get_klines` | `src/index.ts:190-203` |
97
+ | `us_get_ticker_details` | `src/index.ts:205-214` |
22
98
 
23
- ## Token
99
+ **服务方法**:
24
100
 
25
- `polygon`(slug)/ `Polygon`(title)/ `POLYGON`(ref 前缀)/
26
- `crypto`(工具前缀)/ `Crypto`(服务键 infix)。token 未展开也可编译;
27
- 生成器保证全部替换。
101
+ | 方法 | 有/缺席 | 说明 |
102
+ |---|---|---|
103
+ | `MarketDataService.getTicker` | 有 | `src/index.ts:63-65` → `src/rest.ts:124-154`;底层是 `/prev` 上一交易日收盘聚合 |
104
+ | `MarketDataService.getKlines` | 有 | `src/index.ts:67-69` → `src/rest.ts:156-188`(顶部过周期闸,`src/rest.ts:157`) |
105
+ | `MarketDataService.subscribeTicker` | 有 | 快照轮询,`intervalMs` 下限 1000 ms、默认 5000 ms,`dispose()` 清定时器(`src/index.ts:75-83`) |
106
+ | `getDetails`(契约外扩展) | 有 | `src/index.ts:71-73` → `src/rest.ts:190-194`(内部方法名 `getTickerDetails`);对应工具 `us_get_ticker_details`;上游无 `results` 时回落 `{}`(`src/rest.ts:193`) |
107
+ | `MarketDataService.listInstruments`(可选契约方法,`packages/api/src/index.ts:741`) | **缺席** | 无名册端点实现;`/v3/reference/tickers` 需按符号单取 |
108
+ | `MarketDataService.getFundamentals` / `getTickers`(可选契约方法) | 未实现 | 无对应服务方法;报价批量回落逐只 `getTicker` |
109
+ | `TradeService.getBalance` | 有 | 固定返回 `{ currency: 'USD', available: 100000, total: 100000 }`(`src/rest.ts:196-198`),非真实券商余额 |
110
+ | `TradeService.placeOrder` | 有 | 服务缝三态闸门(`src/index.ts:105-132`);无工具入口 |
111
+ | `TradeService.cancelOrder` | 有 | 与真实下单同门槛,可抛 `TRADING_LIVE_TRADING_DISABLED`;放行时返回 `{ orderId, status: 'canceled' }`(`src/index.ts:134-144`);无工具入口 |
112
+ | `TradeService.getPositions` / `getOrders` | 有 | 恒返回 `[]`(`src/index.ts:146-152`) |
113
+ | 盘口 / 分笔 / 主动买盘 | 未实现 | 契约可选方法全缺(`packages/api/src/index.ts:768,774,792`);见「已知边界」 |
28
114
 
29
- ## 注意(勿改的坑)
115
+ ## 已知边界(源码级,未经网络实测)
116
+
117
+ - **报价语义是上一交易日收盘聚合**:底层打 `/v2/aggs/ticker/<SYM>/prev`(`src/rest.ts:141`),
118
+ 所以 `price` 是上一交易日收盘聚合、默认 `adjusted=true` 的复权价,`volume` 是上一交易日成交量,
119
+ 不能当盘中最新价用。该语义已由 `us_get_ticker` 的工具描述显式声明(`src/index.ts:182`),
120
+ 工具面不再暗示实时报价;`timestamp` 是聚合窗口起点的 Unix 毫秒,不需要换算。
121
+ **未改端点的理由**(官方文档所述,本仓未出网实测):免费 Basic 档下实时快照
122
+ `/v2/snapshot/...`、`/v2/last/trade`、`/v2/last/nbbo` 全部 Not included;延迟档从 Starter $29
123
+ 起(15 分钟延迟),`/v2/last/trade` 需 Developer $79,NBBO 需 Advanced $199;且这些端点的
124
+ 时间字段是纳秒(需 `/1e6` 换算)。本机无 `POLYGON_API_KEY`,替换端点也拿不到真实响应证据。
125
+ - **rest 层是 sim stub**:`placeOrder` 恒返回 `dryRun: true` 的模拟回执(`src/rest.ts:200-212`)、
126
+ `getBalance` 恒 100000(`src/rest.ts:196-198`);
127
+ `.agents/notes/implemented/feature/2026-09-01-service-seam-order-gate.md` 第 20 行把本包 rest 层记为
128
+ 「模拟 stub」。因此闸门 ③ 即使放行,也不会产生真实券商成交。
129
+ - **盘口未接**:`spikes/impl-orderbook-ticks/EVIDENCE.md` 第 32 行记明 us 盘口(含 polygon)未实现,
130
+ 需凭证或本地网关;本包也没有 `getOrderbook` / `getRecentTrades`。
131
+ - **无名册**:未实现 `listInstruments`,无法一次取回全部可交易标的。
132
+ - **无工具层交易面**:本包只注册 3 个行情/资料工具;全仓 `us_place_order` 只出现在 connector-alpaca /
133
+ connector-ibkr / connector-stooq / connector-yahoo。本包 `TradeService` 仅经 `tradingUsTrade`
134
+ 服务注入面被调用。
135
+ - **K 线窗口在短周期上偏窄**:`from = 今天 - limit × 周期毫秒 × 2`(`src/rest.ts:162`)只是请求下界,
136
+ 真实可取根数由上游历史深度与账号权限裁决;`limit` 原样透传,超限由服务端处理(`src/rest.ts:173`)。
137
+ - **未实证项**:`/prev`、`/range`、`/v3/reference/tickers` 的真实响应形态、字段口径、限流与额度表现,
138
+ 本仓均无出网证据。
139
+
140
+ ## 合规记录
141
+
142
+ - **需自备 API key(BYOK)**:配置只存 ref 名 `POLYGON_API_KEY`;运行期按请求取
143
+ `tradingMarketRouter.getCredential('polygon')` 的 `apiKey`,缺失时回落同名环境变量
144
+ (`src/index.ts:172-176`;主入口与数据面行共用这一份,`src/index.ts:183`、`src/dataplane.ts:32`)。
145
+ 本仓不内置、不落库、不打印 key。
146
+ - **key 随查询参数出网**:每次请求把 key 以 `apiKey=<key>` 拼进 URL(`src/rest.ts:117`),会进入上游访问
147
+ 日志与进程环境可见范围。上游是商业注册制数据源(设置页标为 `type: 'commercial'`,
148
+ `packages/client-ui-settings/src/client/trading-settings-controller.ts:51`)。
149
+ - **免费额度与再分发边界以 Polygon.io 服务条款为准**:本仓不缓存、不再分发任何行情或标的资料数据。
150
+ `docs/guides/connectors-guide.md` 第 36 行列有一句免费计划额度描述——该数字来自文档而非源码或 spike,
151
+ 本仓未复验,**一律以 Polygon.io 官方条款为准**。
152
+ - **标的资料按上游原样透传**:`/v3/reference/tickers` 的 `results` 对象直接返回(`src/rest.ts:190-194`),
153
+ 不做字段筛选、不落盘。
154
+
155
+ ## 出网验证
156
+
157
+ 本包暂无已提交的独立出网验证脚本(`spikes/` 下没有 `impl-polygon*` 目录;
158
+ `spikes/impl-service-seam-gate/net-verify.mjs` 只 import `connector-okx` 的构建产物)。
159
+
160
+ 可复跑的最小入口:
161
+
162
+ ```sh
163
+ # 1) 离线契约(不需要 key,不出网)
164
+ pnpm --filter @dshtrading/connector-polygon test
165
+
166
+ # 2) 真实出网(需自备 key)。lib/ 是 tsdown 构建产物、被 .gitignore 忽略,先构建再 import:
167
+ pnpm --filter @dshtrading/connector-polygon build
168
+ POLYGON_API_KEY='<your-key>' node --input-type=module -e "
169
+ import { PolygonRestClient } from './packages/connector-polygon/lib/rest.js'
170
+ const client = new PolygonRestClient({ apiKey: process.env.POLYGON_API_KEY })
171
+ console.log(JSON.stringify(await client.getTicker('AAPL')))
172
+ console.log(JSON.stringify((await client.getKlines('AAPL', '1d', 5)).slice(-2)))
173
+ console.log(JSON.stringify(await client.getTickerDetails('AAPL')))
174
+ "
175
+ ```
30
176
 
31
- - 服务类用 TS 编译期 `private`(禁 ECMAScript `#`,realm 代理会炸)。
32
- - 工具名 = 市场前缀 + 语义词(不带交易所名)——base 闸门正则只认市场前缀。
33
- - 行 id = `dsh-trading-<market>-connector-<slug>`,全仓唯一(insert-only 铁律 #1)。
34
- - 凭证只进 ref,绝不内置/落日志(铁律 #3/#5)。
177
+ 未设 `POLYGON_API_KEY` 时 URL 不会附加 `apiKey`(`src/rest.ts:103`),请求照发、由上游裁决,
178
+ 非 2xx 一律映射为 `TRADING_UPSTREAM_ERROR`(`src/rest.ts:110-112`)。
@@ -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,23 +1,24 @@
1
- import { PolygonMarketDataService, TRADING_US_MARKET_DATA_KEY } from "./index.js";
1
+ import { Config, PolygonMarketDataService, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, createApiKeyProvider } 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 = "polygon";
9
10
  const MARKET = "us";
10
- function apply(ctx, config) {
11
+ function apply(ctx, config = withDefaults(void 0)) {
11
12
  if (!config.enabled) return;
12
- const apiKey = process.env[config.apiKeyRef];
13
+ const apiKeyProvider = createApiKeyProvider(ctx, config);
13
14
  const registry = resolveMarketDataRegistry(ctx);
14
15
  if (registry === void 0) {
15
- new PolygonMarketDataService(ctx, { apiKey });
16
+ new PolygonMarketDataService(ctx, { apiKeyProvider });
16
17
  return;
17
18
  }
18
19
  const inner = ctx.isolate(TRADING_US_MARKET_DATA_KEY);
19
- const service = new PolygonMarketDataService(inner, { apiKey });
20
+ const service = new PolygonMarketDataService(inner, { apiKeyProvider });
20
21
  ctx.effect(() => registry.register(MARKET, ROUTER_PROVIDER, service));
21
22
  }
22
23
  //#endregion
23
- export { apply, inject };
24
+ export { Config, apply, inject };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccountBalance, INTERVAL_VOCABULARY, Interval, Kline, Order, PolygonRestClient, PolygonRestOptions, Position, Ticker, TradingServiceError, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan } from "./rest.js";
1
+ import { AccountBalance, INTERVAL_VOCABULARY, Interval, Kline, Order, PolygonRestClient, PolygonRestOptions, Position, SupportedInterval, Ticker, TradingServiceError, assertSupportedInterval, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan } 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";
@@ -6,7 +6,6 @@ import { AccountBalance as AccountBalance$1, Disposable, Interval as Interval$1,
6
6
  declare const name = "dsh-trading-us-connector-polygon";
7
7
  interface Config {
8
8
  enabled: boolean;
9
- env: 'demo' | 'live';
10
9
  dryRun: boolean;
11
10
  liveTrading: boolean;
12
11
  apiKeyRef: string;
@@ -42,6 +41,15 @@ declare class PolygonTradeService extends Service implements TradeService {
42
41
  }
43
42
  declare const ROUTER_PROVIDER = "polygon";
44
43
  declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
44
+ /**
45
+ * API Key 的**惰性**取值源:设置中心 `dshtrading.credentials.polygon.apiKey`(router `getCredential`)
46
+ * 优先、`process.env[config.apiKeyRef]` 兜底,每次请求解析一次。
47
+ *
48
+ * 为什么是惰性而不是 apply 期快照:settings 用户层(settings.yaml)的加载与修改都晚于插件
49
+ * apply(2026-09-12 实证根因),快照会永久漏掉面板里刚填的 key。主入口与数据面行共用这一份,
50
+ * 包内不存在第二份凭证解析(同 connector-tushare 的 `createTokenProvider`)。
51
+ */
52
+ declare function createApiKeyProvider(ctx: Context, config: Config): () => string | undefined;
45
53
  declare function apply(ctx: Context, config: Config): void;
46
54
  //#endregion
47
- export { type AccountBalance, Config, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, PolygonMarketDataService, PolygonRestClient, PolygonRestOptions, PolygonTradeService, type Position, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, type Ticker, TradingServiceError, apply, name, normalizeUsSymbol, parseIntervalMs, routeAllows, toPolygonTimespan };
55
+ export { type AccountBalance, Config, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, PolygonMarketDataService, PolygonRestClient, PolygonRestOptions, PolygonTradeService, type Position, ROUTER_PROVIDER, SupportedInterval, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, type Ticker, TradingServiceError, apply, assertSupportedInterval, createApiKeyProvider, name, normalizeUsSymbol, parseIntervalMs, routeAllows, toPolygonTimespan };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { INTERVAL_VOCABULARY, PolygonRestClient, TradingServiceError, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan } from "./rest.js";
1
+ import { INTERVAL_VOCABULARY, PolygonRestClient, TradingServiceError, assertSupportedInterval, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan } from "./rest.js";
2
2
  import { liveTradingEnabled } from "@dshtrading/authority";
3
3
  import { Service } from "@deepseek-ai/cordis";
4
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -11,7 +11,6 @@ import Schema from "@deepseek-ai/schemastery";
11
11
  const name = "dsh-trading-us-connector-polygon";
12
12
  const Config = Schema.object({
13
13
  enabled: Schema.boolean().default(true).description("是否激活 Polygon 连接器"),
14
- env: Schema.union(["demo", "live"]).default("demo").description("运行环境"),
15
14
  dryRun: Schema.boolean().default(true).description("默认模拟下单"),
16
15
  liveTrading: Schema.boolean().default(false).description("是否允许实盘交易"),
17
16
  apiKeyRef: Schema.string().default("POLYGON_API_KEY").description("Polygon API Key 环境变量名 (BYOK)")
@@ -89,13 +88,26 @@ function routeAllows(ctx, config, market) {
89
88
  if (router === void 0) return true;
90
89
  return router.activeProvider(market) === ROUTER_PROVIDER;
91
90
  }
91
+ /**
92
+ * API Key 的**惰性**取值源:设置中心 `dshtrading.credentials.polygon.apiKey`(router `getCredential`)
93
+ * 优先、`process.env[config.apiKeyRef]` 兜底,每次请求解析一次。
94
+ *
95
+ * 为什么是惰性而不是 apply 期快照:settings 用户层(settings.yaml)的加载与修改都晚于插件
96
+ * apply(2026-09-12 实证根因),快照会永久漏掉面板里刚填的 key。主入口与数据面行共用这一份,
97
+ * 包内不存在第二份凭证解析(同 connector-tushare 的 `createTokenProvider`)。
98
+ */
99
+ function createApiKeyProvider(ctx, config) {
100
+ return () => {
101
+ return (ctx.get?.("tradingMarketRouter", false))?.getCredential?.("polygon")?.apiKey || process.env[config.apiKeyRef];
102
+ };
103
+ }
92
104
  function apply(ctx, config) {
93
105
  if (!config.enabled) return;
94
106
  if (!routeAllows(ctx, config, "us")) return;
95
- const apiKey = ((ctx.get?.("tradingMarketRouter", false))?.getCredential?.("polygon"))?.apiKey || process.env[config.apiKeyRef];
96
- const marketData = new PolygonMarketDataService(ctx, { apiKey });
107
+ const apiKeyProvider = createApiKeyProvider(ctx, config);
108
+ const marketData = new PolygonMarketDataService(ctx, { apiKeyProvider });
97
109
  new PolygonTradeService(ctx, {
98
- apiKey,
110
+ apiKeyProvider,
99
111
  config
100
112
  });
101
113
  ctx.inject(["tools"], (ctx) => {
@@ -105,7 +117,7 @@ function apply(ctx, config) {
105
117
  };
106
118
  register(defineTool({
107
119
  name: "us_get_ticker",
108
- description: "Get the latest trade price and quote for a US stock via Polygon.io API.",
120
+ description: "Get the previous trading day close aggregate for a US stock via Polygon.io API (/v2/aggs/ticker/<SYM>/prev). price is the previous session close (adjusted by default) and timestamp is the aggregate window start in Unix milliseconds — not a realtime quote.",
109
121
  parameters: { symbol: {
110
122
  type: "string",
111
123
  required: true,
@@ -178,4 +190,4 @@ function apply(ctx, config) {
178
190
  });
179
191
  }
180
192
  //#endregion
181
- export { Config, INTERVAL_VOCABULARY, PolygonMarketDataService, PolygonRestClient, PolygonTradeService, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, TradingServiceError, apply, name, normalizeUsSymbol, parseIntervalMs, routeAllows, toPolygonTimespan };
193
+ export { Config, INTERVAL_VOCABULARY, PolygonMarketDataService, PolygonRestClient, PolygonTradeService, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, TradingServiceError, apply, assertSupportedInterval, createApiKeyProvider, name, normalizeUsSymbol, parseIntervalMs, routeAllows, toPolygonTimespan };
package/lib/rest.d.ts CHANGED
@@ -4,7 +4,16 @@ declare class TradingServiceError extends Error {
4
4
  readonly code: TradingErrorCode;
5
5
  constructor(code: TradingErrorCode, message: string, cause?: unknown);
6
6
  }
7
+ /** 本包声明的周期词汇:Polygon aggregates 的 multiplier/timespan 9 档。 */
7
8
  declare const INTERVAL_VOCABULARY: readonly ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"];
9
+ /** `INTERVAL_VOCABULARY` 的类型面:调用方在断言后即可按穷尽联合处理周期。 */
10
+ type SupportedInterval = (typeof INTERVAL_VOCABULARY)[number];
11
+ /**
12
+ * 表外周期显式拒绝(fail-closed):`Interval` 是 15 档规范联合,比本包声明的 9 档宽,
13
+ * 本包不接受静默降级。断言签名同时把 `interval` 收窄到受支持联合,
14
+ * 使下游 `switch` 穷尽——漏掉一档映射是编译错误而非运行期回落。
15
+ */
16
+ declare function assertSupportedInterval(interval: Interval$1): asserts interval is SupportedInterval;
8
17
  declare function toPolygonTimespan(interval: Interval$1): {
9
18
  multiplier: number;
10
19
  timespan: string;
@@ -14,13 +23,22 @@ declare function normalizeUsSymbol(raw: string): string;
14
23
  interface PolygonRestOptions {
15
24
  baseUrl?: string;
16
25
  apiKey?: string;
26
+ /**
27
+ * 惰性凭证源:**每次请求**解析一次(BYOK)。设置中心 `dshtrading.credentials.polygon.apiKey`
28
+ * 由它取、`process.env[apiKeyRef]` 兜底(见 `createApiKeyProvider`)。静态 `apiKey` 保留作
29
+ * 直调用法(README 出网复跑命令、离线用例)的构造期兜底。
30
+ */
31
+ apiKeyProvider?: () => string | undefined;
17
32
  fetchImpl?: typeof fetch;
18
33
  }
19
34
  declare class PolygonRestClient {
20
35
  readonly baseUrl: string;
21
36
  private readonly apiKey?;
37
+ private readonly apiKeyProvider?;
22
38
  private readonly fetchImpl;
23
39
  constructor(options?: PolygonRestOptions);
40
+ /** 取值顺序:惰性 provider(设置中心优先、env 兜底)→ 构造期静态快照。 */
41
+ private resolvedApiKey;
24
42
  private requestJson;
25
43
  getTicker(symbol: string): Promise<Ticker$1>;
26
44
  getKlines(symbol: string, interval?: Interval$1, limit?: number): Promise<Kline$1[]>;
@@ -33,4 +51,4 @@ declare class PolygonRestClient {
33
51
  }>;
34
52
  }
35
53
  //#endregion
36
- export { type AccountBalance$1 as AccountBalance, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, type Order$1 as Order, PolygonRestClient, PolygonRestOptions, type Position$1 as Position, type Ticker$1 as Ticker, TradingServiceError, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan };
54
+ export { type AccountBalance$1 as AccountBalance, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, type Order$1 as Order, PolygonRestClient, PolygonRestOptions, type Position$1 as Position, SupportedInterval, type Ticker$1 as Ticker, TradingServiceError, assertSupportedInterval, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan };
package/lib/rest.js CHANGED
@@ -8,6 +8,7 @@ var TradingServiceError = class extends Error {
8
8
  if (cause !== void 0) this.cause = cause;
9
9
  }
10
10
  };
11
+ /** 本包声明的周期词汇:Polygon aggregates 的 multiplier/timespan 9 档。 */
11
12
  const INTERVAL_VOCABULARY = [
12
13
  "1m",
13
14
  "5m",
@@ -19,7 +20,16 @@ const INTERVAL_VOCABULARY = [
19
20
  "1w",
20
21
  "1M"
21
22
  ];
23
+ /**
24
+ * 表外周期显式拒绝(fail-closed):`Interval` 是 15 档规范联合,比本包声明的 9 档宽,
25
+ * 本包不接受静默降级。断言签名同时把 `interval` 收窄到受支持联合,
26
+ * 使下游 `switch` 穷尽——漏掉一档映射是编译错误而非运行期回落。
27
+ */
28
+ function assertSupportedInterval(interval) {
29
+ if (!INTERVAL_VOCABULARY.includes(interval)) throw new TradingServiceError("TRADING_UNSUPPORTED_INTERVAL", "Polygon upstream does not serve interval \"" + interval + "\"; supported: " + INTERVAL_VOCABULARY.join(", "));
30
+ }
22
31
  function toPolygonTimespan(interval) {
32
+ assertSupportedInterval(interval);
23
33
  switch (interval) {
24
34
  case "1m": return {
25
35
  multiplier: 1,
@@ -60,6 +70,7 @@ function toPolygonTimespan(interval) {
60
70
  }
61
71
  }
62
72
  function parseIntervalMs(interval) {
73
+ assertSupportedInterval(interval);
63
74
  switch (interval) {
64
75
  case "1m": return 6e4;
65
76
  case "5m": return 3e5;
@@ -80,15 +91,22 @@ function normalizeUsSymbol(raw) {
80
91
  var PolygonRestClient = class {
81
92
  baseUrl;
82
93
  apiKey;
94
+ apiKeyProvider;
83
95
  fetchImpl;
84
96
  constructor(options = {}) {
85
97
  this.baseUrl = options.baseUrl ?? "https://api.polygon.io";
86
98
  this.apiKey = options.apiKey;
99
+ this.apiKeyProvider = options.apiKeyProvider;
87
100
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
88
101
  }
102
+ /** 取值顺序:惰性 provider(设置中心优先、env 兜底)→ 构造期静态快照。 */
103
+ resolvedApiKey() {
104
+ return this.apiKeyProvider?.() ?? this.apiKey;
105
+ }
89
106
  async requestJson(path) {
90
107
  const sep = path.includes("?") ? "&" : "?";
91
- const keyQuery = this.apiKey ? `${sep}apiKey=${encodeURIComponent(this.apiKey)}` : "";
108
+ const apiKey = this.resolvedApiKey();
109
+ const keyQuery = apiKey ? `${sep}apiKey=${encodeURIComponent(apiKey)}` : "";
92
110
  const url = `${this.baseUrl}${path}${keyQuery}`;
93
111
  try {
94
112
  const res = await this.fetchImpl(url, { headers: { "Accept": "application/json" } });
@@ -112,6 +130,7 @@ var PolygonRestClient = class {
112
130
  };
113
131
  }
114
132
  async getKlines(symbol, interval = "1d", limit = 100) {
133
+ assertSupportedInterval(interval);
115
134
  const sym = normalizeUsSymbol(symbol);
116
135
  const { multiplier, timespan } = toPolygonTimespan(interval);
117
136
  const stepMs = parseIntervalMs(interval);
@@ -161,4 +180,4 @@ var PolygonRestClient = class {
161
180
  }
162
181
  };
163
182
  //#endregion
164
- export { INTERVAL_VOCABULARY, PolygonRestClient, TradingServiceError, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan };
183
+ export { INTERVAL_VOCABULARY, PolygonRestClient, TradingServiceError, assertSupportedInterval, normalizeUsSymbol, parseIntervalMs, toPolygonTimespan };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/connector-polygon",
3
- "description": "【模板】交易所连接器插件脚手架:Polygon(polygon)v2 插件骨架 —— 市场数据服务 + 交易服务 + 三态闸门 + BYOK 凭证 ref。生成器:node scripts/new-connector.mjs --slug <exchange>",
4
- "version": "0.6.1",
3
+ "description": "Polygon.io(Massive)商业数据源美股连接器(BYOK):tradingUsMarketData / tradingUsTrade 双服务 + us_get_ticker / us_get_klines / us_get_ticker_details 三个工具,服务缝三态下单闸门;凭证只存 ref 名 POLYGON_API_KEY,不内置密钥。",
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": {