@openfinclaw/findoo-datahub-plugin 2026.3.2 → 2026.3.10

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 (67) 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 +332 -14
  16. package/openclaw.plugin.json +2 -2
  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 +339 -0
  64. package/test/e2e/l4-skill-tool-chain.live.test.ts +465 -0
  65. package/skills/crypto-defi/skill.md +0 -69
  66. package/skills/equity/skill.md +0 -64
  67. package/skills/market-radar/skill.md +0 -47
@@ -0,0 +1,89 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { TObject } from "@sinclair/typebox";
3
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
4
+ import type { DataHubClient } from "./datahub-client.js";
5
+
6
+ /* ---------- response helper ---------- */
7
+
8
+ export const json = (payload: unknown) => ({
9
+ content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
10
+ details: payload,
11
+ });
12
+
13
+ /* ---------- param builder ---------- */
14
+
15
+ /** Build query params from user-facing tool params, forwarding all non-empty string/number values. */
16
+ export function buildParams(params: Record<string, unknown>): Record<string, string> {
17
+ const out: Record<string, string> = {};
18
+ for (const [k, v] of Object.entries(params)) {
19
+ if (k === "endpoint" || k === "indicator") continue; // routing keys, not query params
20
+ if (v == null || v === "") continue;
21
+ out[k] = String(v);
22
+ }
23
+ return out;
24
+ }
25
+
26
+ /* ---------- tool factory ---------- */
27
+
28
+ export type CategoryToolDef = {
29
+ name: string;
30
+ label: string;
31
+ description: string;
32
+ parameters: TObject;
33
+ category: string;
34
+ clientMethod: (
35
+ client: DataHubClient,
36
+ endpoint: string,
37
+ qp: Record<string, string>,
38
+ ) => Promise<unknown[]>;
39
+ defaultEndpoint: string;
40
+ /** Optional pre-execute hook to transform params before calling client. */
41
+ transformParams?: (endpoint: string, qp: Record<string, string>) => void;
42
+ };
43
+
44
+ /**
45
+ * Register a standard DataHub category tool.
46
+ * Covers the common pattern: parse endpoint + buildParams → client call → json response.
47
+ */
48
+ export function registerCategoryTool(
49
+ api: OpenClawPluginApi,
50
+ client: DataHubClient,
51
+ def: CategoryToolDef,
52
+ ) {
53
+ api.registerTool(
54
+ {
55
+ name: def.name,
56
+ label: def.label,
57
+ description: def.description,
58
+ parameters: def.parameters,
59
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
60
+ try {
61
+ const endpoint = String(params.endpoint ?? def.defaultEndpoint);
62
+ const qp = buildParams(params);
63
+ def.transformParams?.(endpoint, qp);
64
+ const results = await def.clientMethod(client, endpoint, qp);
65
+ return json({
66
+ success: true,
67
+ endpoint: `${def.category}/${endpoint}`,
68
+ count: results.length,
69
+ results,
70
+ });
71
+ } catch (err) {
72
+ return json({ error: err instanceof Error ? err.message : String(err) });
73
+ }
74
+ },
75
+ },
76
+ { names: [def.name] },
77
+ );
78
+ }
79
+
80
+ /* ---------- shared parameter fragments ---------- */
81
+
82
+ export const dateRangeParams = {
83
+ start_date: Type.Optional(Type.String({ description: "Start date, e.g. 2025-01-01" })),
84
+ end_date: Type.Optional(Type.String({ description: "End date, e.g. 2025-12-31" })),
85
+ limit: Type.Optional(Type.Number({ description: "Max records to return" })),
86
+ };
87
+
88
+ export const symbolParam = Type.String({ description: "Stock/index/fund code" });
89
+ export const optionalSymbol = Type.Optional(Type.String({ description: "Symbol (optional)" }));
@@ -0,0 +1,339 @@
1
+ /**
2
+ * L3 — Gateway Bootstrap E2E
3
+ *
4
+ * Verifies findoo-datahub-plugin loads correctly in a gateway-like environment:
5
+ * 1. Plugin registers all 12 tools
6
+ * 2. Plugin registers both services (fin-data-provider, fin-regime-detector)
7
+ * 3. Tools are callable and return well-formed responses
8
+ * 4. Services are consumable by other extensions
9
+ * 5. Config resolution works with various env var combinations
10
+ *
11
+ * No real gateway process — uses the same fake API pattern as integration tests
12
+ * but validates the full plugin bootstrap contract.
13
+ *
14
+ * Run:
15
+ * npx vitest run extensions/findoo-datahub-plugin/test/e2e/l3-gateway-bootstrap.test.ts
16
+ */
17
+
18
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
22
+ import findooDatahubPlugin from "../../index.js";
23
+
24
+ const SKIP = process.env.DATAHUB_SKIP_LIVE === "1";
25
+ const DEV_KEY = "98ffa5c5-1ec6-4735-8e0c-715a5eca1a8d";
26
+
27
+ /* ---------- helpers ---------- */
28
+
29
+ type ToolDef = {
30
+ name: string;
31
+ label?: string;
32
+ description?: string;
33
+ parameters?: unknown;
34
+ execute: (id: string, params: Record<string, unknown>) => Promise<unknown>;
35
+ };
36
+
37
+ function createGatewayApi(stateDir: string, pluginConfig: Record<string, unknown> = {}) {
38
+ const tools = new Map<string, ToolDef>();
39
+ const services = new Map<string, unknown>();
40
+ const logs: Array<{ level: string; msg: string }> = [];
41
+
42
+ const api = {
43
+ id: "findoo-datahub-plugin",
44
+ name: "Findoo DataHub",
45
+ source: "gateway",
46
+ config: {},
47
+ pluginConfig: {
48
+ datahubApiKey: process.env.DATAHUB_API_KEY ?? process.env.DATAHUB_PASSWORD ?? DEV_KEY,
49
+ ...pluginConfig,
50
+ },
51
+ runtime: { version: "test-gateway", services },
52
+ logger: {
53
+ info: (...args: unknown[]) => logs.push({ level: "info", msg: String(args[0]) }),
54
+ warn: (...args: unknown[]) => logs.push({ level: "warn", msg: String(args[0]) }),
55
+ error: (...args: unknown[]) => logs.push({ level: "error", msg: String(args[0]) }),
56
+ debug: (...args: unknown[]) => logs.push({ level: "debug", msg: String(args[0]) }),
57
+ },
58
+ log: (level: string, msg: string) => logs.push({ level, msg }),
59
+ registerTool(tool: ToolDef) {
60
+ tools.set(tool.name, tool);
61
+ },
62
+ registerHook: vi.fn(),
63
+ registerHttpHandler: vi.fn(),
64
+ registerHttpRoute: vi.fn(),
65
+ registerChannel: vi.fn(),
66
+ registerGatewayMethod: vi.fn(),
67
+ registerCli: vi.fn(),
68
+ registerService(svc: { id: string; instance: unknown }) {
69
+ services.set(svc.id, svc.instance);
70
+ },
71
+ registerProvider: vi.fn(),
72
+ registerCommand: vi.fn(),
73
+ resolvePath: (p: string) => {
74
+ const full = join(stateDir, p);
75
+ mkdirSync(join(full, ".."), { recursive: true });
76
+ return full;
77
+ },
78
+ on: vi.fn(),
79
+ };
80
+ return { api: api as never, tools, services, logs };
81
+ }
82
+
83
+ function parseResult(result: unknown): Record<string, unknown> {
84
+ const res = result as { content: Array<{ text: string }> };
85
+ return JSON.parse(res.content[0]!.text);
86
+ }
87
+
88
+ /* ---------- tests ---------- */
89
+
90
+ describe.skipIf(SKIP)(
91
+ "L3 — Gateway Bootstrap E2E",
92
+ { timeout: 120_000 },
93
+ () => {
94
+ let tempDir: string;
95
+ let tools: Map<string, ToolDef>;
96
+ let services: Map<string, unknown>;
97
+ let logs: Array<{ level: string; msg: string }>;
98
+
99
+ beforeAll(async () => {
100
+ tempDir = mkdtempSync(join(tmpdir(), "l3-gateway-"));
101
+ const ctx = createGatewayApi(tempDir);
102
+ tools = ctx.tools;
103
+ services = ctx.services;
104
+ logs = ctx.logs;
105
+ // Simulate gateway calling plugin.register()
106
+ await findooDatahubPlugin.register(ctx.api);
107
+ });
108
+
109
+ afterAll(() => {
110
+ rmSync(tempDir, { recursive: true, force: true });
111
+ });
112
+
113
+ /* === Section 1: Plugin registration contract === */
114
+
115
+ describe("1. Registration contract", () => {
116
+ it("1.1 plugin has correct metadata", () => {
117
+ expect(findooDatahubPlugin.id).toBe("findoo-datahub-plugin");
118
+ expect(findooDatahubPlugin.name).toBe("Findoo DataHub");
119
+ expect(findooDatahubPlugin.kind).toBe("financial");
120
+ });
121
+
122
+ it("1.2 registers exactly 12 tools", () => {
123
+ expect(tools.size).toBe(12);
124
+ });
125
+
126
+ it("1.3 all 12 tool names match specification", () => {
127
+ const expected = [
128
+ "fin_stock",
129
+ "fin_index",
130
+ "fin_macro",
131
+ "fin_derivatives",
132
+ "fin_crypto",
133
+ "fin_market",
134
+ "fin_query",
135
+ "fin_data_ohlcv",
136
+ "fin_data_regime",
137
+ "fin_ta",
138
+ "fin_etf",
139
+ "fin_data_markets",
140
+ ];
141
+ for (const name of expected) {
142
+ expect(tools.has(name), `Missing tool: ${name}`).toBe(true);
143
+ }
144
+ // No extra tools
145
+ for (const name of tools.keys()) {
146
+ expect(expected.includes(name), `Unexpected tool: ${name}`).toBe(true);
147
+ }
148
+ });
149
+
150
+ it("1.4 each tool has name, description, and execute function", () => {
151
+ for (const [name, tool] of tools) {
152
+ expect(typeof tool.name, `${name}.name`).toBe("string");
153
+ expect(typeof tool.description, `${name}.description`).toBe("string");
154
+ expect(tool.description!.length, `${name}.description length`).toBeGreaterThan(10);
155
+ expect(typeof tool.execute, `${name}.execute`).toBe("function");
156
+ }
157
+ });
158
+
159
+ it("1.5 registers both required services", () => {
160
+ expect(services.has("fin-data-provider")).toBe(true);
161
+ expect(services.has("fin-regime-detector")).toBe(true);
162
+ });
163
+
164
+ it("1.6 fin-data-provider service has required methods", () => {
165
+ const provider = services.get("fin-data-provider") as Record<string, unknown>;
166
+ expect(typeof provider.getOHLCV).toBe("function");
167
+ expect(typeof provider.getTicker).toBe("function");
168
+ expect(typeof provider.detectRegime).toBe("function");
169
+ expect(typeof provider.getSupportedMarkets).toBe("function");
170
+ });
171
+
172
+ it("1.7 fin-regime-detector service has detect method", () => {
173
+ const detector = services.get("fin-regime-detector") as Record<string, unknown>;
174
+ expect(typeof detector.detect).toBe("function");
175
+ });
176
+ });
177
+
178
+ /* === Section 2: Tool response format contract === */
179
+
180
+ describe("2. Tool response format", () => {
181
+ it("2.1 category tools return {success, endpoint, count, results}", async () => {
182
+ const tool = tools.get("fin_stock")!;
183
+ const raw = await tool.execute("fmt-1", {
184
+ symbol: "600519.SH",
185
+ endpoint: "price/historical",
186
+ limit: 3,
187
+ });
188
+ const res = parseResult(raw);
189
+ expect(res).toHaveProperty("success");
190
+ expect(res).toHaveProperty("endpoint");
191
+ expect(res).toHaveProperty("count");
192
+ expect(res).toHaveProperty("results");
193
+ expect(res.success).toBe(true);
194
+ expect(typeof res.count).toBe("number");
195
+ expect(Array.isArray(res.results)).toBe(true);
196
+ }, 30_000);
197
+
198
+ it("2.2 fin_data_ohlcv returns {symbol, market, timeframe, count, candles}", async () => {
199
+ const tool = tools.get("fin_data_ohlcv")!;
200
+ const raw = await tool.execute("fmt-2", {
201
+ symbol: "600519.SH",
202
+ market: "equity",
203
+ timeframe: "1d",
204
+ limit: 5,
205
+ });
206
+ const res = parseResult(raw);
207
+ expect(res.symbol).toBe("600519.SH");
208
+ expect(res.market).toBe("equity");
209
+ expect(res.timeframe).toBe("1d");
210
+ expect(typeof res.count).toBe("number");
211
+ expect(Array.isArray(res.candles)).toBe(true);
212
+ const candle = (res.candles as Array<Record<string, unknown>>)[0]!;
213
+ expect(candle).toHaveProperty("timestamp");
214
+ expect(candle).toHaveProperty("open");
215
+ expect(candle).toHaveProperty("high");
216
+ expect(candle).toHaveProperty("low");
217
+ expect(candle).toHaveProperty("close");
218
+ expect(candle).toHaveProperty("volume");
219
+ }, 30_000);
220
+
221
+ it("2.3 fin_data_regime returns {symbol, market, timeframe, regime}", async () => {
222
+ const tool = tools.get("fin_data_regime")!;
223
+ const raw = await tool.execute("fmt-3", {
224
+ symbol: "600519.SH",
225
+ market: "equity",
226
+ timeframe: "1d",
227
+ });
228
+ const res = parseResult(raw);
229
+ expect(res.symbol).toBe("600519.SH");
230
+ expect(res.market).toBe("equity");
231
+ expect(res.timeframe).toBe("1d");
232
+ expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
233
+ }, 30_000);
234
+
235
+ it("2.4 fin_data_markets returns {datahub, markets, categories, endpoints}", async () => {
236
+ const tool = tools.get("fin_data_markets")!;
237
+ const raw = await tool.execute("fmt-4", {});
238
+ const res = parseResult(raw);
239
+ expect(res).toHaveProperty("datahub");
240
+ expect(res).toHaveProperty("markets");
241
+ expect(res).toHaveProperty("categories");
242
+ expect(res.endpoints).toBe(172);
243
+ });
244
+
245
+ it("2.5 error responses return {error: string}", async () => {
246
+ const tool = tools.get("fin_query")!;
247
+ const raw = await tool.execute("fmt-5", { path: "" });
248
+ const res = parseResult(raw);
249
+ expect(typeof res.error).toBe("string");
250
+ expect(res.success).toBeUndefined();
251
+ });
252
+ });
253
+
254
+ /* === Section 3: Config resolution === */
255
+
256
+ describe("3. Config edge cases", () => {
257
+ it("3.1 plugin warns when no API key configured", async () => {
258
+ const tmpDir2 = mkdtempSync(join(tmpdir(), "l3-nokey-"));
259
+ const saved = { ...process.env };
260
+ delete process.env.DATAHUB_API_KEY;
261
+ delete process.env.DATAHUB_PASSWORD;
262
+ delete process.env.OPENFINCLAW_DATAHUB_PASSWORD;
263
+
264
+ const ctx = createGatewayApi(tmpDir2, { datahubApiKey: undefined });
265
+ // Clear the pluginConfig entirely so resolveConfig can't find a key
266
+ (ctx.api as unknown as { pluginConfig: Record<string, unknown> }).pluginConfig = {};
267
+ await findooDatahubPlugin.register(ctx.api);
268
+
269
+ const warnLog = ctx.logs.find(
270
+ (l) => l.level === "warn" && l.msg.includes("no API key"),
271
+ );
272
+ expect(warnLog).toBeDefined();
273
+
274
+ // Restore
275
+ Object.assign(process.env, saved);
276
+ rmSync(tmpDir2, { recursive: true, force: true });
277
+ });
278
+
279
+ it("3.2 plugin still registers all tools even without API key", async () => {
280
+ const tmpDir2 = mkdtempSync(join(tmpdir(), "l3-nokey2-"));
281
+ const ctx = createGatewayApi(tmpDir2, { datahubApiKey: undefined });
282
+ (ctx.api as unknown as { pluginConfig: Record<string, unknown> }).pluginConfig = {};
283
+ await findooDatahubPlugin.register(ctx.api);
284
+ expect(ctx.tools.size).toBe(12);
285
+ expect(ctx.services.size).toBe(2);
286
+ rmSync(tmpDir2, { recursive: true, force: true });
287
+ });
288
+ });
289
+
290
+ /* === Section 4: Cross-extension service consumption === */
291
+
292
+ describe("4. Cross-extension service consumption", () => {
293
+ it("4.1 another extension can resolve fin-data-provider and call getOHLCV", async () => {
294
+ // Simulate another extension calling the service
295
+ const provider = services.get("fin-data-provider") as {
296
+ getOHLCV: (p: {
297
+ symbol: string;
298
+ market: string;
299
+ timeframe: string;
300
+ limit: number;
301
+ }) => Promise<Array<{ timestamp: number; close: number }>>;
302
+ };
303
+
304
+ const data = await provider.getOHLCV({
305
+ symbol: "600519.SH",
306
+ market: "equity",
307
+ timeframe: "1d",
308
+ limit: 10,
309
+ });
310
+ expect(data.length).toBeGreaterThan(0);
311
+ expect(typeof data[0]!.timestamp).toBe("number");
312
+ expect(typeof data[0]!.close).toBe("number");
313
+ }, 30_000);
314
+
315
+ it("4.2 another extension can resolve fin-regime-detector and detect regime", async () => {
316
+ const provider = services.get("fin-data-provider") as {
317
+ getOHLCV: (p: {
318
+ symbol: string;
319
+ market: string;
320
+ timeframe: string;
321
+ limit: number;
322
+ }) => Promise<Array<{ timestamp: number; open: number; high: number; low: number; close: number; volume: number }>>;
323
+ };
324
+ const detector = services.get("fin-regime-detector") as {
325
+ detect: (bars: Array<{ timestamp: number; open: number; high: number; low: number; close: number; volume: number }>) => string;
326
+ };
327
+
328
+ const bars = await provider.getOHLCV({
329
+ symbol: "600519.SH",
330
+ market: "equity",
331
+ timeframe: "1d",
332
+ limit: 300,
333
+ });
334
+ const regime = detector.detect(bars);
335
+ expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(regime);
336
+ }, 30_000);
337
+ });
338
+ },
339
+ );