@openfinclaw/findoo-datahub-plugin 2026.3.10 → 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.
- package/index.ts +27 -9
- package/openclaw.plugin.json +11 -18
- package/package.json +1 -1
- package/skills/derivatives/skill.md +4 -4
- package/skills/risk-monitor/skill.md +11 -11
- package/test/e2e/l3-gateway-bootstrap.live.test.ts +246 -231
- package/test/e2e/l4-skill-tool-chain.live.test.ts +362 -366
- package/test/e2e/l5-browser/data-freshness.live.test.ts +379 -0
- package/test/e2e/l5-browser/market-data-chat.live.test.ts +259 -0
- package/test/e2e/l5-browser/skills-registry.test.ts +282 -0
|
@@ -87,379 +87,375 @@ function assertSuccess(res: Record<string, unknown>, minCount = 0) {
|
|
|
87
87
|
|
|
88
88
|
/* ---------- tests ---------- */
|
|
89
89
|
|
|
90
|
-
describe.skipIf(SKIP)(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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();
|
|
102
173
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
assertSuccess(res, 1);
|
|
142
|
-
fundamentals = res;
|
|
143
|
-
}, 30_000);
|
|
144
|
-
|
|
145
|
-
it("A.3 fin_ta(rsi) — technical overlay", async () => {
|
|
146
|
-
const res = parse(
|
|
147
|
-
await tools.get("fin_ta")!.execute("a3", {
|
|
148
|
-
symbol: "600519.SH",
|
|
149
|
-
indicator: "rsi",
|
|
150
|
-
period: 14,
|
|
151
|
-
limit: 30,
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
154
|
-
assertSuccess(res, 1);
|
|
155
|
-
expect(res.endpoint).toBe("ta/rsi");
|
|
156
|
-
}, 30_000);
|
|
157
|
-
|
|
158
|
-
it("A.4 fin_data_regime — trend context", async () => {
|
|
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 {
|
|
159
212
|
const res = parse(
|
|
160
|
-
await tools.get("fin_data_regime")!.execute("
|
|
161
|
-
symbol: "
|
|
213
|
+
await tools.get("fin_data_regime")!.execute("b3", {
|
|
214
|
+
symbol: "HSI",
|
|
162
215
|
market: "equity",
|
|
163
216
|
timeframe: "1d",
|
|
164
217
|
}),
|
|
165
218
|
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
regime = res;
|
|
169
|
-
}, 30_000);
|
|
170
|
-
|
|
171
|
-
it("A.5 chain produces complete analysis inputs", () => {
|
|
172
|
-
// Verify all 4 steps produced data that an LLM could synthesize
|
|
173
|
-
expect(priceData.count as number).toBeGreaterThan(0);
|
|
174
|
-
expect(fundamentals.count as number).toBeGreaterThan(0);
|
|
175
|
-
expect(regime.regime).toBeDefined();
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ═══════════════════════════════════════════════════════════════
|
|
180
|
-
// Scenario B: fin-hk-hsi-pulse — 恒指估值脉搏
|
|
181
|
-
// Skill pattern: index valuation → HIBOR rate → regime
|
|
182
|
-
// ═══════════════════════════════════════════════════════════════
|
|
183
|
-
|
|
184
|
-
describe("B: fin-hk-hsi-pulse — HSI valuation pulse", () => {
|
|
185
|
-
let valuation: Record<string, unknown>;
|
|
186
|
-
let hibor: Record<string, unknown>;
|
|
187
|
-
let regime: Record<string, unknown>;
|
|
188
|
-
|
|
189
|
-
it("B.1 fin_index(daily_basic) — PE/PB percentile data", async () => {
|
|
190
|
-
const res = parse(
|
|
191
|
-
await tools.get("fin_index")!.execute("b1", {
|
|
192
|
-
symbol: "HSI",
|
|
193
|
-
endpoint: "daily_basic",
|
|
194
|
-
limit: 100,
|
|
195
|
-
}),
|
|
196
|
-
);
|
|
197
|
-
assertSuccess(res);
|
|
198
|
-
valuation = res;
|
|
199
|
-
}, 30_000);
|
|
200
|
-
|
|
201
|
-
it("B.2 fin_macro(hibor) — risk-free rate for ERP calc", async () => {
|
|
202
|
-
const res = parse(
|
|
203
|
-
await tools.get("fin_macro")!.execute("b2", {
|
|
204
|
-
endpoint: "hibor",
|
|
205
|
-
limit: 10,
|
|
206
|
-
}),
|
|
207
|
-
);
|
|
208
|
-
assertSuccess(res, 1);
|
|
209
|
-
hibor = res;
|
|
210
|
-
}, 30_000);
|
|
211
|
-
|
|
212
|
-
it("B.3 fin_data_regime(HSI) — trend overlay", async () => {
|
|
213
|
-
// HSI may not have OHLCV via the standard equity path, but we test the flow
|
|
214
|
-
try {
|
|
215
|
-
const res = parse(
|
|
216
|
-
await tools.get("fin_data_regime")!.execute("b3", {
|
|
217
|
-
symbol: "HSI",
|
|
218
|
-
market: "equity",
|
|
219
|
-
timeframe: "1d",
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
if (!res.error) {
|
|
223
|
-
expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
|
|
224
|
-
}
|
|
225
|
-
regime = res;
|
|
226
|
-
} catch {
|
|
227
|
-
// HSI index may not have OHLCV data in the same format
|
|
228
|
-
regime = { regime: "unavailable" };
|
|
219
|
+
if (!res.error) {
|
|
220
|
+
expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
|
|
229
221
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
237
233
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
253
|
-
|
|
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) {
|
|
254
375
|
assertSuccess(res);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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) {
|
|
265
387
|
assertSuccess(res);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
it("E.3 fin_macro(treasury_us) — US 10Y for global anchor", async () => {
|
|
342
|
-
const res = parse(
|
|
343
|
-
await tools.get("fin_macro")!.execute("e3", {
|
|
344
|
-
endpoint: "treasury_us",
|
|
345
|
-
limit: 10,
|
|
346
|
-
}),
|
|
347
|
-
);
|
|
348
|
-
assertSuccess(res, 1);
|
|
349
|
-
}, 30_000);
|
|
350
|
-
|
|
351
|
-
it("E.4 fin_derivatives(futures/historical) — commodity context", async () => {
|
|
352
|
-
const res = parse(
|
|
353
|
-
await tools.get("fin_derivatives")!.execute("e4", {
|
|
354
|
-
symbol: "RB2501.SHF",
|
|
355
|
-
endpoint: "futures/historical",
|
|
356
|
-
limit: 10,
|
|
357
|
-
}),
|
|
358
|
-
);
|
|
359
|
-
assertSuccess(res, 1);
|
|
360
|
-
}, 30_000);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// ═══════════════════════════════════════════════════════════════
|
|
364
|
-
// Scenario F: fin-a-quant-board — 涨停板量化 + 题材跟踪
|
|
365
|
-
// Skill pattern: limit_list → top_list (龙虎榜) → ths_index (题材)
|
|
366
|
-
// ═══════════════════════════════════════════════════════════════
|
|
367
|
-
|
|
368
|
-
describe("F: fin-a-quant-board — Limit-up board + theme tracking", () => {
|
|
369
|
-
it("F.1 fin_market(market/limit_list) — limit-up/down stats", async () => {
|
|
370
|
-
const res = parse(
|
|
371
|
-
await tools.get("fin_market")!.execute("f1", {
|
|
372
|
-
endpoint: "market/limit_list",
|
|
373
|
-
trade_date: "2026-02-27",
|
|
374
|
-
}),
|
|
375
|
-
);
|
|
376
|
-
// May return error for non-trading days
|
|
377
|
-
if (!res.error) {
|
|
378
|
-
assertSuccess(res);
|
|
379
|
-
}
|
|
380
|
-
}, 30_000);
|
|
381
|
-
|
|
382
|
-
it("F.2 fin_market(market/top_list) — dragon-tiger list", async () => {
|
|
383
|
-
const res = parse(
|
|
384
|
-
await tools.get("fin_market")!.execute("f2", {
|
|
385
|
-
endpoint: "market/top_list",
|
|
386
|
-
trade_date: "2026-02-27",
|
|
387
|
-
}),
|
|
388
|
-
);
|
|
389
|
-
if (!res.error) {
|
|
390
|
-
assertSuccess(res);
|
|
391
|
-
}
|
|
392
|
-
}, 30_000);
|
|
393
|
-
|
|
394
|
-
it("F.3 fin_index(thematic/ths_index) — theme index list", async () => {
|
|
395
|
-
const res = parse(
|
|
396
|
-
await tools.get("fin_index")!.execute("f3", {
|
|
397
|
-
endpoint: "thematic/ths_index",
|
|
398
|
-
limit: 20,
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
assertSuccess(res, 1);
|
|
402
|
-
}, 30_000);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// ═══════════════════════════════════════════════════════════════
|
|
406
|
-
// Scenario G: Error resilience in multi-step chains
|
|
407
|
-
// ═══════════════════════════════════════════════════════════════
|
|
408
|
-
|
|
409
|
-
describe("G: Error resilience", () => {
|
|
410
|
-
it("G.1 tool error doesn't crash — chain can continue", async () => {
|
|
411
|
-
// Step 1: Intentionally fail
|
|
412
|
-
const bad = parse(
|
|
413
|
-
await tools.get("fin_stock")!.execute("g1-bad", {
|
|
414
|
-
symbol: "NONEXISTENT_999",
|
|
415
|
-
endpoint: "price/historical",
|
|
416
|
-
limit: 5,
|
|
417
|
-
}),
|
|
418
|
-
);
|
|
419
|
-
// Should gracefully return (error or empty results)
|
|
420
|
-
const hasSomeResponse = bad.error !== undefined || bad.success !== undefined;
|
|
421
|
-
expect(hasSomeResponse).toBe(true);
|
|
422
|
-
|
|
423
|
-
// Step 2: Next tool in chain still works
|
|
424
|
-
const good = parse(
|
|
425
|
-
await tools.get("fin_macro")!.execute("g1-good", {
|
|
426
|
-
endpoint: "cpi",
|
|
427
|
-
limit: 3,
|
|
428
|
-
}),
|
|
429
|
-
);
|
|
430
|
-
assertSuccess(good, 1);
|
|
431
|
-
}, 30_000);
|
|
432
|
-
|
|
433
|
-
it("G.2 parallel tool calls produce independent results", async () => {
|
|
434
|
-
// Simulate LLM calling 3 tools in parallel (common pattern)
|
|
435
|
-
const [r1, r2, r3] = await Promise.all([
|
|
436
|
-
tools.get("fin_stock")!.execute("g2-1", {
|
|
437
|
-
symbol: "600519.SH",
|
|
438
|
-
endpoint: "price/historical",
|
|
439
|
-
limit: 5,
|
|
440
|
-
}),
|
|
441
|
-
tools.get("fin_macro")!.execute("g2-2", {
|
|
442
|
-
endpoint: "cpi",
|
|
443
|
-
limit: 5,
|
|
444
|
-
}),
|
|
445
|
-
tools.get("fin_crypto")!.execute("g2-3", {
|
|
446
|
-
endpoint: "coin/market",
|
|
447
|
-
limit: 5,
|
|
448
|
-
}),
|
|
449
|
-
]);
|
|
450
|
-
|
|
451
|
-
const p1 = parse(r1);
|
|
452
|
-
const p2 = parse(r2);
|
|
453
|
-
const p3 = parse(r3);
|
|
454
|
-
|
|
455
|
-
assertSuccess(p1, 1);
|
|
456
|
-
assertSuccess(p2, 1);
|
|
457
|
-
assertSuccess(p3, 1);
|
|
458
|
-
|
|
459
|
-
// Results are independent
|
|
460
|
-
expect(p1.endpoint).not.toBe(p2.endpoint);
|
|
461
|
-
expect(p2.endpoint).not.toBe(p3.endpoint);
|
|
462
|
-
}, 30_000);
|
|
463
|
-
});
|
|
464
|
-
},
|
|
465
|
-
);
|
|
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
|
+
});
|