@dshtrading/connector-finnhub 0.6.0 → 0.6.2

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,160 @@
1
- # @dshtrading/connector-template
1
+ # @dshtrading/connector-finnhub
2
2
 
3
- **本包是脚手架源,不是可安装的连接器**:不入任何 bundle 依赖,仅在 `pnpm -r build/test`
4
- 基线内保证可编译、冒烟测试绿。真实接入用生成器:
3
+ > **状态:未实证(本仓无该上游的出网证据)**。截至 2026-10-14 检索:`spikes/` 下没有针对 Finnhub 的
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` 覆盖。本 README 的端点、字段与能力面全部读自源码,
7
+ > 不含任何实测结论。
5
8
 
6
- ```sh
7
- node scripts/new-connector.mjs --slug bybit --title Bybit --market crypto
8
- ```
9
+ Finnhub 商业数据源美股连接器(`us` 市场,BYOK),实现 `@dshtrading/api` 的 `MarketDataService`
10
+ 与 `TradeService`,被 `packages/us` bundle 以 `workspace:*` 依赖(`packages/us/package.json` 第 32 行)。
11
+ 对外提供报价 / K 线 / 公司新闻三个行情工具;**不注册任何下单或撤单工具**,交易服务只经服务注入面可达。
12
+
13
+ ## 配置
14
+
15
+ | 字段 | 默认 | 说明 |
16
+ |---|---|---|
17
+ | `enabled` | `true` | 是否激活 Finnhub 连接器(`src/index.ts:43`);`false` 时 `apply` 静默退出(`src/index.ts:174`) |
18
+ | `dryRun` | `true` | 默认模拟下单(`src/index.ts:44`);为真时 `placeOrder` 走本地模拟回执(`src/index.ts:125`) |
19
+ | `liveTrading` | `false` | 是否允许实盘交易(`src/index.ts:45`);非权威镜像,实盘授权由 `@dshtrading/authority` 的人工签署平面裁定(`src/index.ts:97-98`) |
20
+ | `apiKeyRef` | `'FINNHUB_API_KEY'` | Finnhub API Key 的环境变量名(BYOK)(`src/index.ts:46`) |
21
+
22
+ ## 接线
23
+
24
+ - **数据面行**:`packages/us/cordis.patch.yml` 第 34-37 行 → 行 id `dsh-trading-us-dataplane-finnhub`,
25
+ 入口 `@dshtrading/connector-finnhub/dataplane`,行 config 只写 `enabled: true`;
26
+ 该入口把 `(us, finnhub)` 注册进 `tradingMarketDataRegistry`(`src/dataplane.ts:24-25,38-40`)。
27
+ - **凭证解析只有一份**(`createApiKeyProvider`,`src/index.ts:181-185`):设置中心
28
+ `dshtrading.credentials.finnhub.apiKey`(router `getCredential('finnhub').apiKey`)优先、
29
+ `process.env[config.apiKeyRef]` 兜底,**每次请求解析**(惰性,`src/rest.ts:95`)——settings 用户层
30
+ 加载/修改晚于插件 `apply` 也生效。主入口(`src/index.ts:192`)与数据面行(`src/dataplane.ts:32`)
31
+ 同用这一份,故设置页填的 key 在宿主面行情注册行与 preset 工具面都生效。
32
+ - **preset 行**:`packages/us/assets/preset/us-trader/agent.cordis.yml` 第 76-88 行 → 组 id
33
+ `dsh-trading-us-connector-finnhub-group`(`isolate` 键 `tradingUsMarketData` + `tradingUsTrade`,第 79-81 行),
34
+ 组内行 id `dsh-trading-us-connector-finnhub`(第 83 行),行 config 只写 `enabled/dryRun/liveTrading`(第 86-88 行)。
35
+ - **bundle 依赖**:`packages/us/package.json:32`(`workspace:*`)。
36
+ - **provider 词汇**:`packages/router/src/index.ts:71` 的 `PROVIDER_VOCABULARY` 含 `finnhub`;
37
+ 本包导出 `ROUTER_PROVIDER = 'finnhub'`(`src/index.ts:164`),`routeAllows` 只在 `us` 市场当前激活
38
+ provider 为 `finnhub` 时激活本实例(`src/index.ts:166-171`)。
39
+ - **设置页显示名**:`PROVIDER_LABELS` 的 `finnhub` 条目
40
+ (`packages/client-ui-settings/src/client/trading-settings-controller.ts:50`:
41
+ `{ id: 'finnhub', label: 'provider.finnhub', url: 'https://finnhub.io/register', env: 'FINNHUB_API_KEY', type: 'commercial', markets: ['us'] }`),
42
+ 文案见 `packages/client-ui-settings/src/client/locales.ts` 的 `'provider.finnhub'` 条目
43
+ (zh / en 都是 `Finnhub`),界面显示 `Finnhub`。凭据卡片字段 `apiKey`
44
+ (`trading-settings-controller.ts:154`)由本包的 `getCredential` 读点消费。
45
+ - **服务键**:`tradingUsMarketData`(`src/index.ts:49`)、`tradingUsTrade`(`src/index.ts:50`)。
46
+
47
+ ## 上游端点
48
+
49
+ 基址 `https://finnhub.io/api/v1`(`src/rest.ts:75`);API key 以 `token=` 查询参数附加(`src/rest.ts:82`)。
50
+
51
+ | 用途 | 端点 | 源码 |
52
+ |---|---|---|
53
+ | 报价 | `GET /quote?symbol=<SYM>` | `src/rest.ts:104-106` |
54
+ | K 线 | `GET /stock/candle?symbol=<SYM>&resolution=<R>&from=<sec>&to=<sec>` | `src/rest.ts:129-137` |
55
+ | 公司新闻 | `GET /company-news?symbol=<SYM>&from=<YYYY-MM-DD>&to=<YYYY-MM-DD>`(近 7 天,取前 10 条) | `src/rest.ts:163-170` |
56
+ | 基本面指标 | `GET /stock/metric?symbol=<SYM>&metric=all` | `src/rest.ts:175-177` |
57
+ | 标的检索 | `GET /search?q=<query>` | `src/rest.ts:195-197` |
58
+
59
+ ## 周期与契约
60
+
61
+ - **周期词汇** `1m/5m/15m/30m/1h/1d/1w/1M`(`src/rest.ts:29`),映射到 Finnhub resolution
62
+ `1/5/15/30/60/D/W/M`(`src/rest.ts:31-42`)。**不含 `4h`**:工具参数的 enum 直接用该词汇表
63
+ (`src/index.ts:206`),传 `4h` 在工具参数校验处即被拒,不会静默降级。
64
+ - **K 线窗口与截取**:请求区间 `to = now`、`from = now - limit × 周期秒数 × 2`(`src/rest.ts:126-127`);
65
+ 上游 `s !== 'ok'` 或时间数组为空时**返回空数组而不抛错**(`src/rest.ts:139-141`);
66
+ 成功时取最后 `limit` 根(`src/rest.ts:143-145`),`closeTime = openTime + 周期 - 1 ms`(`src/rest.ts:156`)。
67
+ - **符号形态**:`trim()` + 大写,只做规范化、不校验美股形态(`src/rest.ts:57-60`);空串抛
68
+ `TRADING_INVALID_ARGUMENT`。Finnhub 支持的指数/外汇/加密等符号会原样透传。
69
+ - **报价取值**:`c === undefined || c === 0` → `TRADING_SYMBOL_NOT_FOUND`(`src/rest.ts:108-110`);
70
+ 返回 `{ symbol, price: c, timestamp }`,`t`(秒)转毫秒、缺失时回落 `Date.now()`(`src/rest.ts:113-119`)。
71
+ - **错误映射**:上游非 2xx → `TRADING_UPSTREAM_ERROR`(`src/rest.ts:89`);`fetch` 抛错
72
+ (不可达、响应非 JSON 等)→ `TRADING_NETWORK`(`src/rest.ts:94-98`)。错误载体
73
+ `TradingServiceError` 携带 `code`(`src/rest.ts:18-27`)。
74
+ - 服务类成员用 TS 编译期 `private`(禁 ECMAScript `#`,cordis realm 代理会炸;`src/index.ts:53,100`)。
9
75
 
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 连接器。
76
+ ## 能力面
13
77
 
14
- ## 结构
78
+ **注册工具**(`apply` 内 `ctx.inject(['tools'])`,`src/index.ts:185-226`,注册前先 `tools.get` 判重):
15
79
 
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 用例。
80
+ | 工具 | 源码 |
81
+ |---|---|
82
+ | `us_get_ticker` | `src/index.ts:191-199` |
83
+ | `us_get_klines` | `src/index.ts:201-214` |
84
+ | `us_get_news` | `src/index.ts:216-225` |
22
85
 
23
- ## Token
86
+ **服务方法**:
24
87
 
25
- `finnhub`(slug)/ `Finnhub`(title)/ `FINNHUB`(ref 前缀)/
26
- `us`(工具前缀)/ `Us`(服务键 infix)。token 未展开也可编译;
27
- 生成器保证全部替换。
88
+ | 方法 | 有/缺席 | 说明 |
89
+ |---|---|---|
90
+ | `MarketDataService.getTicker` | 有 | `src/index.ts:64-66` → `src/rest.ts:102-120` |
91
+ | `MarketDataService.getKlines` | 有 | `src/index.ts:68-70` → `src/rest.ts:122-161` |
92
+ | `MarketDataService.subscribeTicker` | 有 | 快照轮询,`intervalMs` 下限 1000 ms、默认 5000 ms,`dispose()` 清定时器(`src/index.ts:84-92`) |
93
+ | `getNews`(契约外扩展) | 有 | `src/index.ts:72-74` → `src/rest.ts:163-171`;对应工具 `us_get_news` |
94
+ | `MarketDataService.getFundamentals`(可选契约方法,`packages/api/src/index.ts:748`) | 有 | `src/index.ts:80-82` → `src/rest.ts:173-191`;**未注册工具**,`/stock/metric` 失败时静默回落空指标(`src/rest.ts:177,179-180`) |
95
+ | `MarketDataService.listInstruments`(可选契约方法,`packages/api/src/index.ts:741`) | 有 | `src/index.ts:76-78` → `src/rest.ts:193-200`;本包签名多收一个可选 `query`,返回 `{ symbol, name }` 行而非 `InstrumentRef`;无 query 时直接返回 `[]`;**未注册工具** |
96
+ | `MarketDataService.getTickers`(可选批量报价,`packages/api/src/index.ts:733`) | 未实现 | 无批量端点实现,调用方回落逐只 `getTicker` |
97
+ | `getOrderbook` / `getRecentTrades` | 未实现 | 契约可选方法全缺(`packages/api/src/index.ts:768,774`) |
98
+ | `TradeService.getBalance` | 有 | 固定返回 `{ currency: 'USD', available: 100000, total: 100000 }`(`src/rest.ts:202-204`),非真实券商余额 |
99
+ | `TradeService.placeOrder` | 有 | 服务缝三态闸门(`src/index.ts:114-141`);无工具入口 |
100
+ | `TradeService.cancelOrder` | 有 | 与真实下单同门槛,可抛 `TRADING_LIVE_TRADING_DISABLED`(`src/index.ts:143-153`);无工具入口 |
101
+ | `TradeService.getPositions` / `getOrders` | 有 | 恒返回 `[]`(`src/index.ts:155-161`) |
102
+ | 盘口 / 主动买盘 / 委托簿 | 未实现 | 无对应服务方法与工具 |
28
103
 
29
- ## 注意(勿改的坑)
104
+ ## 已知边界(源码级,未经网络实测)
105
+
106
+ - **rest 层是 sim stub**:`placeOrder` 恒返回 `dryRun: true` 的模拟回执(`src/rest.ts:206-218`)、
107
+ `getBalance` 恒 100000(`src/rest.ts:202-204`);
108
+ `.agents/notes/implemented/feature/2026-09-01-service-seam-order-gate.md` 第 20 行把本包 rest 层记为
109
+ 「模拟 stub」。因此闸门 ③ 即使放行,也不会产生真实券商成交。
110
+ - **无工具层交易面**:本包只注册 3 个行情工具;全仓 `us_place_order` 只出现在 connector-alpaca /
111
+ connector-ibkr / connector-stooq / connector-yahoo。本包 `TradeService` 仅经 `tradingUsTrade`
112
+ 服务注入面被调用——服务缝闸门正是为这条绕过工具层的路径设的。
113
+ - **周期词汇比同市场另两个商业源少一个 `4h`**:fmp / polygon 的 `INTERVAL_VOCABULARY` 含 `4h`,本包不含。
114
+ - **0 价被当作未找到**:`c === 0` 同样判为 `TRADING_SYMBOL_NOT_FOUND`(`src/rest.ts:108`)。
115
+ - **K 线空回不区分原因**:上游 `s !== 'ok'`(无数据、额度或权限受限等)一律静默返回 `[]`
116
+ (`src/rest.ts:139-141`),调用方拿不到区分信号。
117
+ - **`us_get_news` 与 `@dshtrading/kit-us` 同名双源**:
118
+ `.agents/notes/implemented/feature/2026-09-08-agent-native-tool-surface.md` 第 63 行记录该同名**不做合并**——
119
+ kit 侧走 Yahoo + Google News RSS 的当前新闻,本包走 Finnhub `/company-news` 的近 7 天历史序列,
120
+ 语义与供应商都不同。
121
+ - **未实证项**:报价 / K 线 / 新闻 / 指标端点的真实响应形态、字段口径、限流与额度表现,本仓均无出网证据。
122
+
123
+ ## 合规记录
124
+
125
+ - **需自备 API key(BYOK)**:配置只存 ref 名 `FINNHUB_API_KEY`;运行期按请求取
126
+ `tradingMarketRouter.getCredential('finnhub')` 的 `apiKey`,缺失时回落同名环境变量
127
+ (`src/index.ts:181-185`;主入口与数据面行共用这一份,`src/index.ts:192`、`src/dataplane.ts:32`)。
128
+ 本仓不内置、不落库、不打印 key。
129
+ - **key 随查询参数出网**:每次请求把 key 以 `token=<key>` 拼进 URL(`src/rest.ts:96`),会进入上游访问
130
+ 日志与进程环境可见范围。上游是商业注册制数据源,设置页把该源标为 `type: 'commercial'`
131
+ (`packages/client-ui-settings/src/client/trading-settings-controller.ts:50`)。
132
+ - **免费额度与再分发边界以 Finnhub 服务条款为准**:本仓不缓存、不再分发任何行情或新闻数据。
133
+ `docs/guides/connectors-guide.md` 第 35 行列有一句免费额度描述——该数字来自文档而非源码或 spike,
134
+ 本仓未复验,**一律以 Finnhub 官方条款为准**。
135
+ - **新闻条目不做本地持久化**:`/company-news` 返回体按上游原样取前 10 条即返回(`src/rest.ts:170`),
136
+ 不落盘。
137
+
138
+ ## 出网验证
139
+
140
+ 本包暂无已提交的独立出网验证脚本(`spikes/` 下没有 `impl-finnhub*` 目录;
141
+ `spikes/impl-service-seam-gate/net-verify.mjs` 只 import `connector-okx` 的构建产物)。
142
+
143
+ 可复跑的最小入口:
144
+
145
+ ```sh
146
+ # 1) 离线契约(不需要 key,不出网)
147
+ pnpm --filter @dshtrading/connector-finnhub test
148
+
149
+ # 2) 真实出网(需自备 key)。lib/ 是 tsdown 构建产物、被 .gitignore 忽略,先构建再 import:
150
+ pnpm --filter @dshtrading/connector-finnhub build
151
+ FINNHUB_API_KEY='<your-key>' node --input-type=module -e "
152
+ import { FinnhubRestClient } from './packages/connector-finnhub/lib/rest.js'
153
+ const client = new FinnhubRestClient({ apiKey: process.env.FINNHUB_API_KEY })
154
+ console.log(JSON.stringify(await client.getTicker('AAPL')))
155
+ console.log(JSON.stringify((await client.getKlines('AAPL', '1d', 5)).slice(-2)))
156
+ "
157
+ ```
30
158
 
31
- - 服务类用 TS 编译期 `private`(禁 ECMAScript `#`,realm 代理会炸)。
32
- - 工具名 = 市场前缀 + 语义词(不带交易所名)——base 闸门正则只认市场前缀。
33
- - 行 id = `dsh-trading-<market>-connector-<slug>`,全仓唯一(insert-only 铁律 #1)。
34
- - 凭证只进 ref,绝不内置/落日志(铁律 #3/#5)。
159
+ 未设 `FINNHUB_API_KEY` 时 URL 不会附加 `token`(`src/rest.ts:82`),请求照发、由上游裁决,
160
+ 非 2xx 一律映射为 `TRADING_UPSTREAM_ERROR`(`src/rest.ts:88-90`)。
@@ -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 { FinnhubMarketDataService, TRADING_US_MARKET_DATA_KEY } from "./index.js";
1
+ import { Config, FinnhubMarketDataService, 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 = "finnhub";
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 FinnhubMarketDataService(ctx, { apiKey });
16
+ new FinnhubMarketDataService(ctx, { apiKeyProvider });
16
17
  return;
17
18
  }
18
19
  const inner = ctx.isolate(TRADING_US_MARKET_DATA_KEY);
19
- const service = new FinnhubMarketDataService(inner, { apiKey });
20
+ const service = new FinnhubMarketDataService(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
@@ -6,7 +6,6 @@ import { AccountBalance as AccountBalance$1, Disposable, Interval as Interval$1,
6
6
  declare const name = "dsh-trading-us-connector-finnhub";
7
7
  interface Config {
8
8
  enabled: boolean;
9
- env: 'demo' | 'live';
10
9
  dryRun: boolean;
11
10
  liveTrading: boolean;
12
11
  apiKeyRef: string;
@@ -44,6 +43,15 @@ declare class FinnhubTradeService extends Service implements TradeService {
44
43
  }
45
44
  declare const ROUTER_PROVIDER = "finnhub";
46
45
  declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
46
+ /**
47
+ * API Key 的**惰性**取值源:设置中心 `dshtrading.credentials.finnhub.apiKey`(router `getCredential`)
48
+ * 优先、`process.env[config.apiKeyRef]` 兜底,每次请求解析一次。
49
+ *
50
+ * 为什么是惰性而不是 apply 期快照:settings 用户层(settings.yaml)的加载与修改都晚于插件
51
+ * apply(2026-09-12 实证根因),快照会永久漏掉面板里刚填的 key。主入口与数据面行共用这一份,
52
+ * 包内不存在第二份凭证解析(同 connector-tushare 的 `createTokenProvider`)。
53
+ */
54
+ declare function createApiKeyProvider(ctx: Context, config: Config): () => string | undefined;
47
55
  declare function apply(ctx: Context, config: Config): void;
48
56
  //#endregion
49
- export { type AccountBalance, Config, FinnhubMarketDataService, FinnhubRestClient, FinnhubRestOptions, FinnhubTradeService, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, type Position, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, type Ticker, TradingServiceError, apply, name, normalizeUsSymbol, parseIntervalSeconds, routeAllows, toFinnhubResolution };
57
+ export { type AccountBalance, Config, FinnhubMarketDataService, FinnhubRestClient, FinnhubRestOptions, FinnhubTradeService, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, type Position, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, type Ticker, TradingServiceError, apply, createApiKeyProvider, name, normalizeUsSymbol, parseIntervalSeconds, routeAllows, toFinnhubResolution };
package/lib/index.js CHANGED
@@ -11,7 +11,6 @@ import Schema from "@deepseek-ai/schemastery";
11
11
  const name = "dsh-trading-us-connector-finnhub";
12
12
  const Config = Schema.object({
13
13
  enabled: Schema.boolean().default(true).description("是否激活 Finnhub 连接器"),
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("FINNHUB_API_KEY").description("Finnhub API Key 环境变量名 (BYOK)")
@@ -95,13 +94,26 @@ function routeAllows(ctx, config, market) {
95
94
  if (router === void 0) return true;
96
95
  return router.activeProvider(market) === ROUTER_PROVIDER;
97
96
  }
97
+ /**
98
+ * API Key 的**惰性**取值源:设置中心 `dshtrading.credentials.finnhub.apiKey`(router `getCredential`)
99
+ * 优先、`process.env[config.apiKeyRef]` 兜底,每次请求解析一次。
100
+ *
101
+ * 为什么是惰性而不是 apply 期快照:settings 用户层(settings.yaml)的加载与修改都晚于插件
102
+ * apply(2026-09-12 实证根因),快照会永久漏掉面板里刚填的 key。主入口与数据面行共用这一份,
103
+ * 包内不存在第二份凭证解析(同 connector-tushare 的 `createTokenProvider`)。
104
+ */
105
+ function createApiKeyProvider(ctx, config) {
106
+ return () => {
107
+ return (ctx.get?.("tradingMarketRouter", false))?.getCredential?.("finnhub")?.apiKey || process.env[config.apiKeyRef];
108
+ };
109
+ }
98
110
  function apply(ctx, config) {
99
111
  if (!config.enabled) return;
100
112
  if (!routeAllows(ctx, config, "us")) return;
101
- const apiKey = ((ctx.get?.("tradingMarketRouter", false))?.getCredential?.("finnhub"))?.apiKey || process.env[config.apiKeyRef];
102
- const marketData = new FinnhubMarketDataService(ctx, { apiKey });
113
+ const apiKeyProvider = createApiKeyProvider(ctx, config);
114
+ const marketData = new FinnhubMarketDataService(ctx, { apiKeyProvider });
103
115
  new FinnhubTradeService(ctx, {
104
- apiKey,
116
+ apiKeyProvider,
105
117
  config
106
118
  });
107
119
  ctx.inject(["tools"], (ctx) => {
@@ -184,4 +196,4 @@ function apply(ctx, config) {
184
196
  });
185
197
  }
186
198
  //#endregion
187
- export { Config, FinnhubMarketDataService, FinnhubRestClient, FinnhubTradeService, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, TradingServiceError, apply, name, normalizeUsSymbol, parseIntervalSeconds, routeAllows, toFinnhubResolution };
199
+ export { Config, FinnhubMarketDataService, FinnhubRestClient, FinnhubTradeService, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_US_MARKET_DATA_KEY, TRADING_US_TRADE_KEY, TradingServiceError, apply, createApiKeyProvider, name, normalizeUsSymbol, parseIntervalSeconds, routeAllows, toFinnhubResolution };
package/lib/rest.d.ts CHANGED
@@ -11,13 +11,22 @@ declare function normalizeUsSymbol(raw: string): string;
11
11
  interface FinnhubRestOptions {
12
12
  baseUrl?: string;
13
13
  apiKey?: string;
14
+ /**
15
+ * 惰性凭证源:**每次请求**解析一次(BYOK)。设置中心 `dshtrading.credentials.finnhub.apiKey`
16
+ * 由它取、`process.env[apiKeyRef]` 兜底(见 `createApiKeyProvider`)。静态 `apiKey` 保留作
17
+ * 直调用法(README 出网复跑命令、离线用例)的构造期兜底。
18
+ */
19
+ apiKeyProvider?: () => string | undefined;
14
20
  fetchImpl?: typeof fetch;
15
21
  }
16
22
  declare class FinnhubRestClient {
17
23
  readonly baseUrl: string;
18
24
  private readonly apiKey?;
25
+ private readonly apiKeyProvider?;
19
26
  private readonly fetchImpl;
20
27
  constructor(options?: FinnhubRestOptions);
28
+ /** 取值顺序:惰性 provider(设置中心优先、env 兜底)→ 构造期静态快照。 */
29
+ private resolvedApiKey;
21
30
  private requestJson;
22
31
  getTicker(symbol: string): Promise<Ticker$1>;
23
32
  getKlines(symbol: string, interval?: Interval$1, limit?: number): Promise<Kline$1[]>;
package/lib/rest.js CHANGED
@@ -50,15 +50,22 @@ function normalizeUsSymbol(raw) {
50
50
  var FinnhubRestClient = class {
51
51
  baseUrl;
52
52
  apiKey;
53
+ apiKeyProvider;
53
54
  fetchImpl;
54
55
  constructor(options = {}) {
55
56
  this.baseUrl = options.baseUrl ?? "https://finnhub.io/api/v1";
56
57
  this.apiKey = options.apiKey;
58
+ this.apiKeyProvider = options.apiKeyProvider;
57
59
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
58
60
  }
61
+ /** 取值顺序:惰性 provider(设置中心优先、env 兜底)→ 构造期静态快照。 */
62
+ resolvedApiKey() {
63
+ return this.apiKeyProvider?.() ?? this.apiKey;
64
+ }
59
65
  async requestJson(path) {
60
66
  const sep = path.includes("?") ? "&" : "?";
61
- const tokenQuery = this.apiKey ? `${sep}token=${encodeURIComponent(this.apiKey)}` : "";
67
+ const apiKey = this.resolvedApiKey();
68
+ const tokenQuery = apiKey ? `${sep}token=${encodeURIComponent(apiKey)}` : "";
62
69
  const url = `${this.baseUrl}${path}${tokenQuery}`;
63
70
  try {
64
71
  const res = await this.fetchImpl(url, { headers: { "Accept": "application/json" } });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/connector-finnhub",
3
- "description": "【模板】交易所连接器插件脚手架:Finnhub(finnhub)v2 插件骨架 —— 市场数据服务 + 交易服务 + 三态闸门 + BYOK 凭证 ref。生成器:node scripts/new-connector.mjs --slug <exchange>",
4
- "version": "0.6.0",
3
+ "description": "Finnhub 商业数据源美股连接器(BYOK):tradingUsMarketData / tradingUsTrade 双服务 + us_get_ticker / us_get_klines / us_get_news 三个行情工具,服务缝三态下单闸门;凭证只存 ref 名 FINNHUB_API_KEY,不内置密钥。",
4
+ "version": "0.6.2",
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.0",
25
- "@dshtrading/api": "^0.6.0"
24
+ "@dshtrading/authority": "^0.6.2",
25
+ "@dshtrading/api": "^0.6.2"
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": {