@openfinclaw/openfinclaw-strategy 2026.3.14 → 2026.3.26

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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * DataHub market data tools registration.
3
+ * Tools: fin_price, fin_kline, fin_crypto, fin_compare, fin_slim_search
4
+ */
5
+ import { Type } from "@sinclair/typebox";
6
+ import type { OpenClawPluginApi } from "openfinclaw/plugin-sdk";
7
+ import type { UnifiedPluginConfig, MarketType } from "../types.js";
8
+ import { DataHubClient, guessMarket } from "./client.js";
9
+
10
+ /** JSON tool result helper. */
11
+ function json(payload: unknown) {
12
+ return {
13
+ content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
14
+ details: payload,
15
+ };
16
+ }
17
+
18
+ /** Helper to pick params. */
19
+ function pick(params: Record<string, unknown>, ...keys: string[]): Record<string, string> {
20
+ const out: Record<string, string> = {};
21
+ for (const k of keys) {
22
+ if (params[k] != null) out[k] = String(params[k]);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ const NO_KEY =
28
+ "API key not configured. Set apiKey in plugin config or OPENFINCLAW_API_KEY env var.";
29
+
30
+ /**
31
+ * Register DataHub market data tools.
32
+ */
33
+ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPluginConfig): void {
34
+ const datahubClient = config.apiKey
35
+ ? new DataHubClient(config.datahubGatewayUrl, config.apiKey, config.requestTimeoutMs)
36
+ : null;
37
+
38
+ // Register fin-data-provider service for other plugins
39
+ if (datahubClient) {
40
+ api.registerService({
41
+ id: "fin-data-provider",
42
+ start: () => {},
43
+ instance: {
44
+ async getOHLCV(params: {
45
+ symbol: string;
46
+ market?: string;
47
+ timeframe?: string;
48
+ limit?: number;
49
+ }) {
50
+ const market = (params.market as MarketType) ?? guessMarket(params.symbol);
51
+ return datahubClient.getOHLCV({
52
+ symbol: params.symbol,
53
+ market,
54
+ limit: params.limit ?? 300,
55
+ });
56
+ },
57
+ async getTicker(symbol: string, market?: string) {
58
+ const m = (market as MarketType) ?? guessMarket(symbol);
59
+ return datahubClient.getTicker(symbol, m);
60
+ },
61
+ },
62
+ } as Parameters<typeof api.registerService>[0]);
63
+ }
64
+
65
+ // ── fin_price — Price Lookup ──
66
+ api.registerTool(
67
+ {
68
+ name: "fin_price",
69
+ label: "Price Lookup",
70
+ description:
71
+ "Get the current/latest price for any asset — stocks (A/HK/US), crypto, index. " +
72
+ "Returns latest close, volume, and date. The simplest way to answer 'XX 现在什么价格'.",
73
+ parameters: Type.Object({
74
+ symbol: Type.String({
75
+ description:
76
+ "Asset symbol. Crypto: BTC/USDT, ETH/USDT; A-share: 600519.SH; HK: 00700.HK; US: AAPL; Index: 000300.SH",
77
+ }),
78
+ market: Type.Optional(
79
+ Type.Unsafe<"crypto" | "equity">({
80
+ type: "string",
81
+ enum: ["crypto", "equity"],
82
+ description:
83
+ "Market type. Auto-detected if omitted: symbols with .SH/.SZ/.HK or pure letters → equity; contains '/' → crypto.",
84
+ }),
85
+ ),
86
+ }),
87
+ async execute(_id: string, params: Record<string, unknown>) {
88
+ try {
89
+ if (!datahubClient) return json({ error: NO_KEY });
90
+ const symbol = String(params.symbol);
91
+ const market = (params.market as MarketType) ?? guessMarket(symbol);
92
+ const ticker = await datahubClient.getTicker(symbol, market);
93
+ return json({
94
+ symbol: ticker.symbol,
95
+ market: ticker.market,
96
+ price: ticker.last,
97
+ volume24h: ticker.volume24h,
98
+ timestamp: new Date(ticker.timestamp).toISOString(),
99
+ });
100
+ } catch (err) {
101
+ return json({ error: err instanceof Error ? err.message : String(err) });
102
+ }
103
+ },
104
+ },
105
+ { names: ["fin_price"] },
106
+ );
107
+
108
+ // ── fin_kline — K-Line / OHLCV ──
109
+ api.registerTool(
110
+ {
111
+ name: "fin_kline",
112
+ label: "K-Line / OHLCV",
113
+ description:
114
+ "Fetch historical OHLCV (candlestick) data for any asset. " +
115
+ "Use for price history, charting, and trend analysis.",
116
+ parameters: Type.Object({
117
+ symbol: Type.String({
118
+ description: "Asset symbol (BTC/USDT, 600519.SH, AAPL, etc.)",
119
+ }),
120
+ market: Type.Optional(
121
+ Type.Unsafe<"crypto" | "equity">({
122
+ type: "string",
123
+ enum: ["crypto", "equity"],
124
+ description: "Market type (auto-detected if omitted)",
125
+ }),
126
+ ),
127
+ limit: Type.Optional(
128
+ Type.Number({ description: "Number of bars to return (default: 30)" }),
129
+ ),
130
+ }),
131
+ async execute(_id: string, params: Record<string, unknown>) {
132
+ try {
133
+ if (!datahubClient) return json({ error: NO_KEY });
134
+ const symbol = String(params.symbol);
135
+ const market = (params.market as MarketType) ?? guessMarket(symbol);
136
+ const limit = (params.limit as number) ?? 30;
137
+ const ohlcv = await datahubClient.getOHLCV({ symbol, market, limit });
138
+ return json({
139
+ symbol,
140
+ market,
141
+ count: ohlcv.length,
142
+ bars: ohlcv.map((b) => ({
143
+ date: new Date(b.timestamp).toISOString().slice(0, 10),
144
+ open: b.open,
145
+ high: b.high,
146
+ low: b.low,
147
+ close: b.close,
148
+ volume: b.volume,
149
+ })),
150
+ });
151
+ } catch (err) {
152
+ return json({ error: err instanceof Error ? err.message : String(err) });
153
+ }
154
+ },
155
+ },
156
+ { names: ["fin_kline"] },
157
+ );
158
+
159
+ // ── fin_crypto — Crypto & DeFi ──
160
+ api.registerTool(
161
+ {
162
+ name: "fin_crypto",
163
+ label: "Crypto & DeFi",
164
+ description:
165
+ "Crypto market data (ticker/orderbook/trades/funding_rate) via CEX, " +
166
+ "DeFi (protocols/TVL/yields/stablecoins/fees/dex_volumes) via DefiLlama, " +
167
+ "market metrics (coin/market/info/categories/trending/global_stats) via CoinGecko.",
168
+ parameters: Type.Object({
169
+ endpoint: Type.Unsafe<string>({
170
+ type: "string",
171
+ enum: [
172
+ "market/ticker",
173
+ "market/tickers",
174
+ "market/orderbook",
175
+ "market/trades",
176
+ "market/funding_rate",
177
+ "coin/market",
178
+ "coin/historical",
179
+ "coin/info",
180
+ "coin/categories",
181
+ "coin/trending",
182
+ "coin/global_stats",
183
+ "defi/protocols",
184
+ "defi/tvl_historical",
185
+ "defi/protocol_tvl",
186
+ "defi/chains",
187
+ "defi/yields",
188
+ "defi/stablecoins",
189
+ "defi/fees",
190
+ "defi/dex_volumes",
191
+ "defi/bridges",
192
+ "defi/coin_prices",
193
+ "price/historical",
194
+ "search",
195
+ ],
196
+ description: "DataHub crypto endpoint path",
197
+ }),
198
+ symbol: Type.Optional(
199
+ Type.String({ description: "Coin ID, trading pair, or protocol slug" }),
200
+ ),
201
+ start_date: Type.Optional(Type.String({ description: "Start date (YYYY-MM-DD)" })),
202
+ end_date: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })),
203
+ limit: Type.Optional(Type.Number({ description: "Max results (default: 20)" })),
204
+ }),
205
+ async execute(_id: string, params: Record<string, unknown>) {
206
+ try {
207
+ if (!datahubClient) return json({ error: NO_KEY });
208
+ const endpoint = String(params.endpoint ?? "coin/market");
209
+ const qp = pick(params, "symbol", "start_date", "end_date", "limit");
210
+ if (!qp.limit) qp.limit = "20";
211
+ if (qp.symbol) {
212
+ const coinIdEndpoints = ["coin/historical", "coin/info"];
213
+ if (coinIdEndpoints.includes(endpoint)) {
214
+ qp.coin_id = qp.symbol;
215
+ delete qp.symbol;
216
+ } else if (endpoint === "defi/protocol_tvl") {
217
+ qp.protocol = qp.symbol;
218
+ delete qp.symbol;
219
+ } else if (endpoint === "defi/coin_prices") {
220
+ qp.coins = qp.symbol;
221
+ delete qp.symbol;
222
+ }
223
+ }
224
+ const results = await datahubClient.crypto(endpoint, qp);
225
+ return json({
226
+ success: true,
227
+ endpoint: `crypto/${endpoint}`,
228
+ count: results.length,
229
+ results,
230
+ });
231
+ } catch (err) {
232
+ return json({ error: err instanceof Error ? err.message : String(err) });
233
+ }
234
+ },
235
+ },
236
+ { names: ["fin_crypto"] },
237
+ );
238
+
239
+ // ── fin_compare — Price Compare ──
240
+ api.registerTool(
241
+ {
242
+ name: "fin_compare",
243
+ label: "Price Compare",
244
+ description:
245
+ "Compare prices of 2-5 assets side by side. Returns latest price and recent change for each. " +
246
+ "Use for cross-asset comparison questions like 'BTC vs ETH vs 黄金'.",
247
+ parameters: Type.Object({
248
+ symbols: Type.String({
249
+ description: "Comma-separated symbols (2-5). Example: BTC/USDT,ETH/USDT,600519.SH",
250
+ }),
251
+ }),
252
+ async execute(_id: string, params: Record<string, unknown>) {
253
+ try {
254
+ if (!datahubClient) return json({ error: NO_KEY });
255
+ const raw = String(params.symbols);
256
+ const symbols = raw
257
+ .split(",")
258
+ .map((s) => s.trim())
259
+ .filter(Boolean)
260
+ .slice(0, 5);
261
+ if (symbols.length < 2)
262
+ return json({ error: "Need at least 2 symbols, comma-separated" });
263
+
264
+ const results = await Promise.allSettled(
265
+ symbols.map(async (sym) => {
266
+ const market = guessMarket(sym);
267
+ const ticker = await datahubClient!.getTicker(sym, market);
268
+ const bars = await datahubClient!.getOHLCV({ symbol: sym, market, limit: 7 });
269
+ const weekAgo = bars.length > 0 ? bars[0]!.close : ticker.last;
270
+ const weekChange = weekAgo > 0 ? ((ticker.last - weekAgo) / weekAgo) * 100 : 0;
271
+ return {
272
+ symbol: sym,
273
+ market,
274
+ price: ticker.last,
275
+ weekChange: Math.round(weekChange * 100) / 100,
276
+ };
277
+ }),
278
+ );
279
+
280
+ return json({
281
+ comparison: results.map((r, i) =>
282
+ r.status === "fulfilled"
283
+ ? r.value
284
+ : { symbol: symbols[i], error: (r.reason as Error).message },
285
+ ),
286
+ });
287
+ } catch (err) {
288
+ return json({ error: err instanceof Error ? err.message : String(err) });
289
+ }
290
+ },
291
+ },
292
+ { names: ["fin_compare"] },
293
+ );
294
+
295
+ // ── fin_slim_search — Symbol Search ──
296
+ api.registerTool(
297
+ {
298
+ name: "fin_slim_search",
299
+ label: "Symbol Search",
300
+ description:
301
+ "Search for stock/crypto symbols by name or keyword. " +
302
+ "Use when user mentions a company/coin name but not the exact symbol.",
303
+ parameters: Type.Object({
304
+ query: Type.String({ description: "Search keyword (e.g. '茅台', 'bitcoin', 'Tesla')" }),
305
+ market: Type.Optional(
306
+ Type.Unsafe<"crypto" | "equity">({
307
+ type: "string",
308
+ enum: ["crypto", "equity"],
309
+ description: "Limit search to market type",
310
+ }),
311
+ ),
312
+ }),
313
+ async execute(_id: string, params: Record<string, unknown>) {
314
+ try {
315
+ if (!datahubClient) return json({ error: NO_KEY });
316
+ const q = String(params.query);
317
+ const market = params.market as string | undefined;
318
+
319
+ const results: unknown[] = [];
320
+
321
+ if (!market || market === "crypto") {
322
+ try {
323
+ const crypto = await datahubClient!.crypto("search", { query: q, limit: "5" });
324
+ results.push(...crypto.map((r) => ({ ...(r as object), market: "crypto" })));
325
+ } catch {
326
+ /* ignore */
327
+ }
328
+ }
329
+
330
+ if (!market || market === "equity") {
331
+ try {
332
+ const equity = await datahubClient!.equity("search", { query: q, limit: "5" });
333
+ results.push(...equity.map((r) => ({ ...(r as object), market: "equity" })));
334
+ } catch {
335
+ /* ignore */
336
+ }
337
+ }
338
+
339
+ return json({ query: q, count: results.length, results });
340
+ } catch (err) {
341
+ return json({ error: err instanceof Error ? err.message : String(err) });
342
+ }
343
+ },
344
+ },
345
+ { names: ["fin_slim_search"] },
346
+ );
347
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Hub API client for strategy operations.
3
+ * Handles HTTP requests to hub.openfinclaw.ai
4
+ */
5
+ import type { UnifiedPluginConfig } from "../types.js";
6
+
7
+ /**
8
+ * HTTP request helper for Hub API.
9
+ */
10
+ export async function hubApiRequest(
11
+ config: UnifiedPluginConfig,
12
+ method: "GET" | "POST",
13
+ pathSegments: string,
14
+ options?: { body?: Record<string, unknown>; searchParams?: Record<string, string> },
15
+ ): Promise<{ status: number; data: unknown }> {
16
+ const url = new URL(`${config.hubApiUrl}/api/v1${pathSegments}`);
17
+ if (options?.searchParams) {
18
+ for (const [k, v] of Object.entries(options.searchParams)) {
19
+ url.searchParams.set(k, v);
20
+ }
21
+ }
22
+
23
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
24
+ if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
25
+
26
+ const response = await fetch(url.toString(), {
27
+ method,
28
+ headers,
29
+ body: options?.body ? JSON.stringify(options.body) : undefined,
30
+ signal: AbortSignal.timeout(config.requestTimeoutMs),
31
+ });
32
+
33
+ const rawText = await response.text();
34
+ let data: unknown = rawText;
35
+ if (rawText && rawText.trim().startsWith("{")) {
36
+ try {
37
+ data = JSON.parse(rawText);
38
+ } catch {
39
+ data = { raw: rawText };
40
+ }
41
+ }
42
+
43
+ return { status: response.status, data };
44
+ }
@@ -4,6 +4,14 @@
4
4
  */
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
+ import type {
8
+ UnifiedPluginConfig,
9
+ ForkOptions,
10
+ ForkResult,
11
+ HubPublicEntry,
12
+ ForkAndDownloadResponse,
13
+ } from "../types.js";
14
+ import type { ForkMeta } from "../types.js";
7
15
  import {
8
16
  getStrategiesRoot,
9
17
  createDateDir,
@@ -11,15 +19,7 @@ import {
11
19
  writeForkMeta,
12
20
  parseStrategyId,
13
21
  formatDate,
14
- } from "./strategy-storage.js";
15
- import type {
16
- UnifiedPluginConfig,
17
- ForkOptions,
18
- ForkResult,
19
- HubPublicEntry,
20
- ForkAndDownloadResponse,
21
- } from "./types.js";
22
- import type { ForkMeta } from "./types.js";
22
+ } from "./storage.js";
23
23
 
24
24
  const HUB_BASE_URL = "https://hub.openfinclaw.ai";
25
25
 
@@ -91,7 +91,8 @@ export async function forkAndDownloadFromHub(
91
91
  if (!config.apiKey) {
92
92
  return {
93
93
  success: false,
94
- error: "API key is required for fork operation. Set OPENFINCLAW_API_KEY environment variable.",
94
+ error:
95
+ "API key is required for fork operation. Set OPENFINCLAW_API_KEY environment variable.",
95
96
  };
96
97
  }
97
98
 
@@ -1,7 +1,11 @@
1
+ /**
2
+ * Local strategy storage management.
3
+ * Handles reading/writing strategy metadata and listing local strategies.
4
+ */
1
5
  import fs from "node:fs";
2
6
  import { homedir } from "node:os";
3
7
  import path from "node:path";
4
- import type { ForkMeta, CreatedMeta, LocalStrategy, StrategyPerformance } from "./types.js";
8
+ import type { ForkMeta, CreatedMeta, LocalStrategy } from "../types.js";
5
9
 
6
10
  const WORKSPACE_DIRNAME = "workspace";
7
11
  const STRATEGIES_DIRNAME = "strategies";
@@ -30,10 +34,6 @@ export function getStrategiesRoot(): string {
30
34
 
31
35
  /**
32
36
  * Generate a slugified directory name from strategy name.
33
- * - Lowercase
34
- * - Spaces/underscores to hyphens
35
- * - Remove special characters
36
- * - Max 40 characters
37
37
  */
38
38
  export function slugifyName(name: string): string {
39
39
  return name
@@ -73,7 +73,6 @@ export function generateCreatedDirName(name: string): string {
73
73
 
74
74
  /**
75
75
  * Create date directory under strategies root.
76
- * Returns the full path to the date directory.
77
76
  */
78
77
  export function createDateDir(baseDir: string, date?: string): string {
79
78
  const dateStr = date ?? formatDate(new Date());