@openfinclaw/findoo-datahub-plugin 2026.3.2 → 2026.3.12

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.
Files changed (70) hide show
  1. package/DESIGN.md +492 -151
  2. package/_vendor/claude-skills-finance/SKILL.md +192 -0
  3. package/_vendor/claude-skills-finance/assets/dcf_analysis_template.md +184 -0
  4. package/_vendor/claude-skills-finance/assets/expected_output.json +161 -0
  5. package/_vendor/claude-skills-finance/assets/forecast_report_template.md +177 -0
  6. package/_vendor/claude-skills-finance/assets/sample_financial_data.json +219 -0
  7. package/_vendor/claude-skills-finance/assets/variance_report_template.md +122 -0
  8. package/_vendor/claude-skills-finance/references/financial-ratios-guide.md +396 -0
  9. package/_vendor/claude-skills-finance/references/forecasting-best-practices.md +294 -0
  10. package/_vendor/claude-skills-finance/references/valuation-methodology.md +255 -0
  11. package/_vendor/claude-skills-finance/scripts/budget_variance_analyzer.py +406 -0
  12. package/_vendor/claude-skills-finance/scripts/dcf_valuation.py +449 -0
  13. package/_vendor/claude-skills-finance/scripts/forecast_builder.py +494 -0
  14. package/_vendor/claude-skills-finance/scripts/ratio_calculator.py +432 -0
  15. package/index.ts +356 -20
  16. package/openclaw.plugin.json +12 -19
  17. package/package.json +1 -1
  18. package/references/cn-market-specifics.md +165 -0
  19. package/references/crypto-analysis.md +635 -0
  20. package/references/financial-ratios-cn.md +452 -0
  21. package/references/hk-market-specifics.md +166 -0
  22. package/references/macro-cycle-cn.md +409 -0
  23. package/references/valuation-cn.md +427 -0
  24. package/skills/README.md +294 -0
  25. package/skills/a-concept-cycle/skill.md +200 -0
  26. package/skills/a-convertible-arb/skill.md +294 -0
  27. package/skills/a-dividend-king/skill.md +187 -0
  28. package/skills/a-earnings-season/skill.md +221 -0
  29. package/skills/a-index-timer/skill.md +192 -0
  30. package/skills/a-ipo-new/skill.md +297 -0
  31. package/skills/a-northbound-decoder/skill.md +185 -0
  32. package/skills/a-quant-board/skill.md +286 -0
  33. package/skills/a-share/skill.md +347 -0
  34. package/skills/a-share-radar/skill.md +185 -0
  35. package/skills/cross-asset/skill.md +202 -0
  36. package/skills/crypto/skill.md +269 -0
  37. package/skills/crypto-altseason/skill.md +208 -0
  38. package/skills/crypto-btc-cycle/skill.md +231 -0
  39. package/skills/crypto-defi-yield/skill.md +181 -0
  40. package/skills/crypto-funding-arb/skill.md +158 -0
  41. package/skills/crypto-stablecoin-flow/skill.md +149 -0
  42. package/skills/data-query/skill.md +124 -30
  43. package/skills/derivatives/skill.md +188 -35
  44. package/skills/etf-fund/skill.md +216 -0
  45. package/skills/factor-screen/skill.md +186 -0
  46. package/skills/hk-china-internet/skill.md +190 -0
  47. package/skills/hk-dividend-harvest/skill.md +192 -0
  48. package/skills/hk-hsi-pulse/skill.md +154 -0
  49. package/skills/hk-southbound-alpha/skill.md +163 -0
  50. package/skills/hk-stock/skill.md +295 -0
  51. package/skills/macro/skill.md +244 -53
  52. package/skills/risk-monitor/skill.md +171 -0
  53. package/skills/us-dividend/skill.md +162 -0
  54. package/skills/us-earnings/skill.md +149 -0
  55. package/skills/us-equity/skill.md +235 -0
  56. package/skills/us-etf/skill.md +261 -0
  57. package/skills/us-sector-rotation/skill.md +223 -0
  58. package/src/config.ts +4 -5
  59. package/src/datahub-client.test.ts +4 -7
  60. package/src/datahub-client.ts +6 -1
  61. package/src/register-tools.ts +720 -0
  62. package/src/tool-helpers.ts +89 -0
  63. package/test/e2e/l3-gateway-bootstrap.live.test.ts +354 -0
  64. package/test/e2e/l4-skill-tool-chain.live.test.ts +461 -0
  65. package/test/e2e/l5-browser/data-freshness.live.test.ts +379 -0
  66. package/test/e2e/l5-browser/market-data-chat.live.test.ts +259 -0
  67. package/test/e2e/l5-browser/skills-registry.test.ts +282 -0
  68. package/skills/crypto-defi/skill.md +0 -69
  69. package/skills/equity/skill.md +0 -64
  70. package/skills/market-radar/skill.md +0 -47
@@ -0,0 +1,461 @@
1
+ /**
2
+ * L4 — Skill-driven Tool Chain E2E
3
+ *
4
+ * Simulates the multi-step tool_use sequences that an LLM would execute
5
+ * when following a skill.md decision tree. Each scenario mirrors a real
6
+ * skill's analysis pattern with real DataHub data.
7
+ *
8
+ * Zero LLM cost — no API key needed. Calls tool.execute() directly
9
+ * in the exact order a skill prescribes.
10
+ *
11
+ * Scenarios:
12
+ * A: fin-a-share skill — individual A-share deep analysis (4 steps)
13
+ * B: fin-hk-hsi-pulse skill — HSI valuation pulse (3 steps)
14
+ * C: fin-us-equity skill — US stock earnings + options (3 steps)
15
+ * D: fin-crypto skill — crypto market overview (3 steps)
16
+ * E: fin-cross-asset skill — multi-asset correlation (4 steps)
17
+ * F: fin-a-quant-board skill — limit-up board + theme (3 steps)
18
+ *
19
+ * Run:
20
+ * npx vitest run extensions/findoo-datahub-plugin/test/e2e/l4-skill-tool-chain.test.ts
21
+ */
22
+
23
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { join } from "node:path";
26
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
27
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
28
+ import findooDatahubPlugin from "../../index.js";
29
+
30
+ const SKIP = process.env.DATAHUB_SKIP_LIVE === "1";
31
+ const DEV_KEY = "98ffa5c5-1ec6-4735-8e0c-715a5eca1a8d";
32
+
33
+ /* ---------- helpers ---------- */
34
+
35
+ type ToolExecute = (id: string, params: Record<string, unknown>) => Promise<unknown>;
36
+ type ToolMap = Map<string, { execute: ToolExecute }>;
37
+
38
+ function createTestEnv(stateDir: string) {
39
+ const tools: ToolMap = new Map();
40
+ const services = new Map<string, unknown>();
41
+ const api = {
42
+ id: "findoo-datahub-plugin",
43
+ name: "findoo-datahub-plugin",
44
+ source: "test",
45
+ config: {},
46
+ pluginConfig: {
47
+ datahubApiKey: process.env.DATAHUB_API_KEY ?? process.env.DATAHUB_PASSWORD ?? DEV_KEY,
48
+ },
49
+ runtime: { version: "test", services },
50
+ logger: { info() {}, warn() {}, error() {}, debug() {} },
51
+ log() {},
52
+ registerTool(tool: { name: string; execute: ToolExecute }) {
53
+ tools.set(tool.name, tool);
54
+ },
55
+ registerHook() {},
56
+ registerHttpHandler() {},
57
+ registerHttpRoute() {},
58
+ registerChannel() {},
59
+ registerGatewayMethod() {},
60
+ registerCli() {},
61
+ registerService(svc: { id: string; instance: unknown }) {
62
+ services.set(svc.id, svc.instance);
63
+ },
64
+ registerProvider() {},
65
+ registerCommand() {},
66
+ resolvePath: (p: string) => {
67
+ const full = join(stateDir, p);
68
+ mkdirSync(join(full, ".."), { recursive: true });
69
+ return full;
70
+ },
71
+ on() {},
72
+ } as unknown as OpenClawPluginApi;
73
+ return { api, tools, services };
74
+ }
75
+
76
+ function parse(result: unknown): Record<string, unknown> {
77
+ const res = result as { content: Array<{ text: string }> };
78
+ return JSON.parse(res.content[0]!.text);
79
+ }
80
+
81
+ /** Asserts a tool call succeeded and returned data */
82
+ function assertSuccess(res: Record<string, unknown>, minCount = 0) {
83
+ expect(res.error, `Tool error: ${res.error}`).toBeUndefined();
84
+ if (res.success !== undefined) expect(res.success).toBe(true);
85
+ if (minCount > 0) expect(res.count as number).toBeGreaterThanOrEqual(minCount);
86
+ }
87
+
88
+ /* ---------- tests ---------- */
89
+
90
+ describe.skipIf(SKIP)("L4 — Skill-driven Tool Chain E2E", { timeout: 180_000 }, () => {
91
+ let tempDir: string;
92
+ let tools: ToolMap;
93
+
94
+ beforeAll(async () => {
95
+ tempDir = mkdtempSync(join(tmpdir(), "l4-chain-"));
96
+ const env = createTestEnv(tempDir);
97
+ tools = env.tools;
98
+ await findooDatahubPlugin.register(env.api);
99
+ });
100
+
101
+ afterAll(() => {
102
+ rmSync(tempDir, { recursive: true, force: true });
103
+ });
104
+
105
+ // ═══════════════════════════════════════════════════════════════
106
+ // Scenario A: fin-a-share — 个股全景分析 (600519.SH 茅台)
107
+ // Skill pattern: price → fundamentals → ownership → TA → regime
108
+ // ═══════════════════════════════════════════════════════════════
109
+
110
+ describe("A: fin-a-share — A-share deep analysis (茅台)", () => {
111
+ let priceData: Record<string, unknown>;
112
+ let fundamentals: Record<string, unknown>;
113
+ let regime: Record<string, unknown>;
114
+
115
+ it("A.1 fin_stock(price/historical) — get price data", async () => {
116
+ const res = parse(
117
+ await tools.get("fin_stock")!.execute("a1", {
118
+ symbol: "600519.SH",
119
+ endpoint: "price/historical",
120
+ limit: 30,
121
+ }),
122
+ );
123
+ assertSuccess(res, 1);
124
+ priceData = res;
125
+ const rows = res.results as Array<Record<string, unknown>>;
126
+ expect(rows[0]).toHaveProperty("close");
127
+ expect(Number(rows[0]!.close)).toBeGreaterThan(100);
128
+ }, 30_000);
129
+
130
+ it("A.2 fin_stock(fundamental/income) — get financials", async () => {
131
+ const res = parse(
132
+ await tools.get("fin_stock")!.execute("a2", {
133
+ symbol: "600519.SH",
134
+ endpoint: "fundamental/income",
135
+ limit: 4,
136
+ }),
137
+ );
138
+ assertSuccess(res, 1);
139
+ fundamentals = res;
140
+ }, 30_000);
141
+
142
+ it("A.3 fin_ta(rsi) — technical overlay", async () => {
143
+ const res = parse(
144
+ await tools.get("fin_ta")!.execute("a3", {
145
+ symbol: "600519.SH",
146
+ indicator: "rsi",
147
+ period: 14,
148
+ limit: 30,
149
+ }),
150
+ );
151
+ assertSuccess(res, 0);
152
+ expect(res.endpoint).toBe("ta/rsi");
153
+ }, 30_000);
154
+
155
+ it("A.4 fin_data_regime — trend context", async () => {
156
+ const res = parse(
157
+ await tools.get("fin_data_regime")!.execute("a4", {
158
+ symbol: "600519.SH",
159
+ market: "equity",
160
+ timeframe: "1d",
161
+ }),
162
+ );
163
+ expect(res.error).toBeUndefined();
164
+ expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
165
+ regime = res;
166
+ }, 30_000);
167
+
168
+ it("A.5 chain produces complete analysis inputs", () => {
169
+ // Verify all 4 steps produced data that an LLM could synthesize
170
+ expect(priceData.count as number).toBeGreaterThan(0);
171
+ expect(fundamentals.count as number).toBeGreaterThan(0);
172
+ expect(regime.regime).toBeDefined();
173
+ });
174
+ });
175
+
176
+ // ═══════════════════════════════════════════════════════════════
177
+ // Scenario B: fin-hk-hsi-pulse — 恒指估值脉搏
178
+ // Skill pattern: index valuation → HIBOR rate → regime
179
+ // ═══════════════════════════════════════════════════════════════
180
+
181
+ describe("B: fin-hk-hsi-pulse — HSI valuation pulse", () => {
182
+ let valuation: Record<string, unknown>;
183
+ let hibor: Record<string, unknown>;
184
+ let regime: Record<string, unknown>;
185
+
186
+ it("B.1 fin_index(daily_basic) — PE/PB percentile data", async () => {
187
+ const res = parse(
188
+ await tools.get("fin_index")!.execute("b1", {
189
+ symbol: "HSI",
190
+ endpoint: "daily_basic",
191
+ limit: 100,
192
+ }),
193
+ );
194
+ assertSuccess(res);
195
+ valuation = res;
196
+ }, 30_000);
197
+
198
+ it("B.2 fin_macro(hibor) — risk-free rate for ERP calc", async () => {
199
+ const res = parse(
200
+ await tools.get("fin_macro")!.execute("b2", {
201
+ endpoint: "hibor",
202
+ limit: 10,
203
+ }),
204
+ );
205
+ assertSuccess(res, 1);
206
+ hibor = res;
207
+ }, 30_000);
208
+
209
+ it("B.3 fin_data_regime(HSI) — trend overlay", async () => {
210
+ // HSI may not have OHLCV via the standard equity path, but we test the flow
211
+ try {
212
+ const res = parse(
213
+ await tools.get("fin_data_regime")!.execute("b3", {
214
+ symbol: "HSI",
215
+ market: "equity",
216
+ timeframe: "1d",
217
+ }),
218
+ );
219
+ if (!res.error) {
220
+ expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
221
+ }
222
+ regime = res;
223
+ } catch {
224
+ // HSI index may not have OHLCV data in the same format
225
+ regime = { regime: "unavailable" };
226
+ }
227
+ }, 30_000);
228
+
229
+ it("B.4 chain produces ERP calculation inputs", () => {
230
+ // Verify we have the data needed for ERP = 1/PE - HIBOR
231
+ expect(hibor.count as number).toBeGreaterThan(0);
232
+ // Valuation may be empty for HSI if not covered by tushare
233
+ });
234
+ });
235
+
236
+ // ═══════════════════════════════════════════════════════════════
237
+ // Scenario C: fin-us-equity — US stock analysis (AAPL)
238
+ // Skill pattern: earnings forecast → income → price → TA
239
+ // ═══════════════════════════════════════════════════════════════
240
+
241
+ describe("C: fin-us-equity — US stock analysis (AAPL)", () => {
242
+ it("C.1 fin_stock(fundamental/earnings_forecast) — consensus EPS", async () => {
243
+ const res = parse(
244
+ await tools.get("fin_stock")!.execute("c1", {
245
+ symbol: "AAPL",
246
+ endpoint: "fundamental/earnings_forecast",
247
+ limit: 5,
248
+ }),
249
+ );
250
+ // tushare may have limited US coverage
251
+ assertSuccess(res);
252
+ }, 30_000);
253
+
254
+ it("C.2 fin_stock(us/income) — GAAP financials", async () => {
255
+ const res = parse(
256
+ await tools.get("fin_stock")!.execute("c2", {
257
+ symbol: "AAPL",
258
+ endpoint: "us/income",
259
+ limit: 4,
260
+ }),
261
+ );
262
+ assertSuccess(res);
263
+ }, 30_000);
264
+
265
+ it("C.3 fin_macro(treasury_us) — risk-free rate for DCF", async () => {
266
+ const res = parse(
267
+ await tools.get("fin_macro")!.execute("c3", {
268
+ endpoint: "treasury_us",
269
+ limit: 5,
270
+ }),
271
+ );
272
+ assertSuccess(res, 1);
273
+ }, 30_000);
274
+ });
275
+
276
+ // ═══════════════════════════════════════════════════════════════
277
+ // Scenario D: fin-crypto — Crypto market overview
278
+ // Skill pattern: market overview → DeFi TVL → stablecoin flow
279
+ // ═══════════════════════════════════════════════════════════════
280
+
281
+ describe("D: fin-crypto — Crypto market overview", () => {
282
+ it("D.1 fin_crypto(coin/market) — top coins", async () => {
283
+ const res = parse(
284
+ await tools.get("fin_crypto")!.execute("d1", {
285
+ endpoint: "coin/market",
286
+ limit: 10,
287
+ }),
288
+ );
289
+ assertSuccess(res, 1);
290
+ }, 30_000);
291
+
292
+ it("D.2 fin_crypto(defi/protocols) — DeFi TVL ranking", async () => {
293
+ const res = parse(
294
+ await tools.get("fin_crypto")!.execute("d2", {
295
+ endpoint: "defi/protocols",
296
+ }),
297
+ );
298
+ assertSuccess(res, 1);
299
+ }, 30_000);
300
+
301
+ it("D.3 fin_crypto(defi/stablecoins) — stablecoin supply", async () => {
302
+ const res = parse(
303
+ await tools.get("fin_crypto")!.execute("d3", {
304
+ endpoint: "defi/stablecoins",
305
+ }),
306
+ );
307
+ assertSuccess(res);
308
+ }, 30_000);
309
+ });
310
+
311
+ // ═══════════════════════════════════════════════════════════════
312
+ // Scenario E: fin-cross-asset — Cross-asset correlation
313
+ // Skill pattern: macro rates → A-share index → US treasury → crypto
314
+ // ═══════════════════════════════════════════════════════════════
315
+
316
+ describe("E: fin-cross-asset — Multi-asset analysis", () => {
317
+ it("E.1 fin_macro(shibor) — China rate environment", async () => {
318
+ const res = parse(
319
+ await tools.get("fin_macro")!.execute("e1", {
320
+ endpoint: "shibor",
321
+ limit: 10,
322
+ }),
323
+ );
324
+ assertSuccess(res, 1);
325
+ }, 30_000);
326
+
327
+ it("E.2 fin_index(price/historical) — CSI 300 trend", async () => {
328
+ const res = parse(
329
+ await tools.get("fin_index")!.execute("e2", {
330
+ symbol: "000300.SH",
331
+ endpoint: "price/historical",
332
+ limit: 30,
333
+ }),
334
+ );
335
+ assertSuccess(res, 1);
336
+ }, 30_000);
337
+
338
+ it("E.3 fin_macro(treasury_us) — US 10Y for global anchor", async () => {
339
+ const res = parse(
340
+ await tools.get("fin_macro")!.execute("e3", {
341
+ endpoint: "treasury_us",
342
+ limit: 10,
343
+ }),
344
+ );
345
+ assertSuccess(res, 1);
346
+ }, 30_000);
347
+
348
+ it("E.4 fin_derivatives(futures/historical) — commodity context", async () => {
349
+ const res = parse(
350
+ await tools.get("fin_derivatives")!.execute("e4", {
351
+ symbol: "RB2501.SHF",
352
+ endpoint: "futures/historical",
353
+ limit: 10,
354
+ }),
355
+ );
356
+ assertSuccess(res, 1);
357
+ }, 30_000);
358
+ });
359
+
360
+ // ═══════════════════════════════════════════════════════════════
361
+ // Scenario F: fin-a-quant-board — 涨停板量化 + 题材跟踪
362
+ // Skill pattern: limit_list → top_list (龙虎榜) → ths_index (题材)
363
+ // ═══════════════════════════════════════════════════════════════
364
+
365
+ describe("F: fin-a-quant-board — Limit-up board + theme tracking", () => {
366
+ it("F.1 fin_market(market/limit_list) — limit-up/down stats", async () => {
367
+ const res = parse(
368
+ await tools.get("fin_market")!.execute("f1", {
369
+ endpoint: "market/limit_list",
370
+ trade_date: "2026-02-27",
371
+ }),
372
+ );
373
+ // May return error for non-trading days
374
+ if (!res.error) {
375
+ assertSuccess(res);
376
+ }
377
+ }, 30_000);
378
+
379
+ it("F.2 fin_market(market/top_list) — dragon-tiger list", async () => {
380
+ const res = parse(
381
+ await tools.get("fin_market")!.execute("f2", {
382
+ endpoint: "market/top_list",
383
+ trade_date: "2026-02-27",
384
+ }),
385
+ );
386
+ if (!res.error) {
387
+ assertSuccess(res);
388
+ }
389
+ }, 30_000);
390
+
391
+ it("F.3 fin_index(thematic/ths_index) — theme index list", async () => {
392
+ const res = parse(
393
+ await tools.get("fin_index")!.execute("f3", {
394
+ endpoint: "thematic/ths_index",
395
+ limit: 20,
396
+ }),
397
+ );
398
+ assertSuccess(res, 1);
399
+ }, 30_000);
400
+ });
401
+
402
+ // ═══════════════════════════════════════════════════════════════
403
+ // Scenario G: Error resilience in multi-step chains
404
+ // ═══════════════════════════════════════════════════════════════
405
+
406
+ describe("G: Error resilience", () => {
407
+ it("G.1 tool error doesn't crash — chain can continue", async () => {
408
+ // Step 1: Intentionally fail
409
+ const bad = parse(
410
+ await tools.get("fin_stock")!.execute("g1-bad", {
411
+ symbol: "NONEXISTENT_999",
412
+ endpoint: "price/historical",
413
+ limit: 5,
414
+ }),
415
+ );
416
+ // Should gracefully return (error or empty results)
417
+ const hasSomeResponse = bad.error !== undefined || bad.success !== undefined;
418
+ expect(hasSomeResponse).toBe(true);
419
+
420
+ // Step 2: Next tool in chain still works
421
+ const good = parse(
422
+ await tools.get("fin_macro")!.execute("g1-good", {
423
+ endpoint: "cpi",
424
+ limit: 3,
425
+ }),
426
+ );
427
+ assertSuccess(good, 1);
428
+ }, 30_000);
429
+
430
+ it("G.2 parallel tool calls produce independent results", async () => {
431
+ // Simulate LLM calling 3 tools in parallel (common pattern)
432
+ const [r1, r2, r3] = await Promise.all([
433
+ tools.get("fin_stock")!.execute("g2-1", {
434
+ symbol: "600519.SH",
435
+ endpoint: "price/historical",
436
+ limit: 5,
437
+ }),
438
+ tools.get("fin_macro")!.execute("g2-2", {
439
+ endpoint: "cpi",
440
+ limit: 5,
441
+ }),
442
+ tools.get("fin_crypto")!.execute("g2-3", {
443
+ endpoint: "coin/market",
444
+ limit: 5,
445
+ }),
446
+ ]);
447
+
448
+ const p1 = parse(r1);
449
+ const p2 = parse(r2);
450
+ const p3 = parse(r3);
451
+
452
+ assertSuccess(p1, 1);
453
+ assertSuccess(p2, 1);
454
+ assertSuccess(p3, 1);
455
+
456
+ // Results are independent
457
+ expect(p1.endpoint).not.toBe(p2.endpoint);
458
+ expect(p2.endpoint).not.toBe(p3.endpoint);
459
+ }, 30_000);
460
+ });
461
+ });