@openfinclaw/openfinclaw-strategy 2026.3.26 → 2026.3.27

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 CHANGED
@@ -4,12 +4,16 @@ import type { Command } from "commander";
4
4
  * Features:
5
5
  * - Strategy tools: publish, validate, fork, leaderboard
6
6
  * - Market data tools: price, K-line, crypto data, compare, search
7
+ * - SQLite persistence: all tool executions logged; domain tables for strategies and backtests
8
+ * - Dashboard: embedded HTTP server at http://127.0.0.1:<httpPort> (default 18792)
7
9
  * Supports FEP v2.0 protocol for strategy packages.
8
10
  */
9
11
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
10
12
  import { registerStrategyCli } from "./src/cli.js";
11
13
  import { resolvePluginConfig } from "./src/config.js";
12
14
  import { registerDatahubTools } from "./src/datahub/tools.js";
15
+ import { getDb } from "./src/db/db.js";
16
+ import { startHttpServer } from "./src/http/server.js";
13
17
  import { registerStrategyTools } from "./src/strategy/tools.js";
14
18
 
15
19
  const openfinclawPlugin = {
@@ -22,11 +26,14 @@ const openfinclawPlugin = {
22
26
  register(api: OpenClawPluginApi) {
23
27
  const config = resolvePluginConfig(api);
24
28
 
29
+ // Initialise SQLite database (creates tables on first run)
30
+ const db = getDb();
31
+
25
32
  // Register DataHub market data tools (fin_price, fin_kline, fin_crypto, fin_compare, fin_slim_search)
26
- registerDatahubTools(api, config);
33
+ registerDatahubTools(api, config, db);
27
34
 
28
35
  // Register strategy tools (skill_publish, skill_validate, skill_fork, skill_leaderboard, etc.)
29
- registerStrategyTools(api, config);
36
+ registerStrategyTools(api, config, db);
30
37
 
31
38
  // Register CLI commands
32
39
  api.registerCli(
@@ -38,6 +45,9 @@ const openfinclawPlugin = {
38
45
  }),
39
46
  { commands: ["strategy"] },
40
47
  );
48
+
49
+ // Start embedded dashboard HTTP server (loopback only, configurable port)
50
+ startHttpServer(db, config.httpPort, api.logger);
41
51
  },
42
52
  };
43
53
 
@@ -30,6 +30,13 @@
30
30
  "minimum": 5000,
31
31
  "maximum": 300000,
32
32
  "description": "HTTP request timeout in milliseconds"
33
+ },
34
+ "httpPort": {
35
+ "type": "number",
36
+ "default": 18792,
37
+ "minimum": 1024,
38
+ "maximum": 65535,
39
+ "description": "Dashboard HTTP server port (loopback only)"
33
40
  }
34
41
  }
35
42
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfinclaw/openfinclaw-strategy",
3
- "version": "2026.3.26",
3
+ "version": "2026.3.27",
4
4
  "description": "OpenFinClaw - Unified financial tools: market data (price/K-line/crypto/compare/search), strategy publishing, fork, and validation. Single API key for Hub and DataHub.",
5
5
  "keywords": [
6
6
  "backtest",
@@ -34,7 +34,7 @@
34
34
  "registry": "https://registry.npmjs.org"
35
35
  },
36
36
  "dependencies": {
37
- "@sinclair/typebox": "^0.34.0",
37
+ "@sinclair/typebox": "0.34.48",
38
38
  "adm-zip": "^0.5.16"
39
39
  },
40
40
  "devDependencies": {
package/src/config.ts CHANGED
@@ -48,10 +48,18 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
48
48
  ? Math.floor(Number(timeoutRaw))
49
49
  : DEFAULT_TIMEOUT_MS;
50
50
 
51
+ const httpPortRaw = raw?.httpPort ?? readEnv(["OPENFINCLAW_HTTP_PORT"]);
52
+ const httpPortNum = Number(httpPortRaw);
53
+ const httpPort =
54
+ Number.isFinite(httpPortNum) && httpPortNum >= 1024 && httpPortNum <= 65535
55
+ ? Math.floor(httpPortNum)
56
+ : 18792;
57
+
51
58
  return {
52
59
  apiKey: apiKey && apiKey.length > 0 ? apiKey : undefined,
53
60
  hubApiUrl: hubApiUrl.replace(/\/$/, ""),
54
61
  datahubGatewayUrl: datahubGatewayUrl.replace(/\/+$/, ""),
55
62
  requestTimeoutMs,
63
+ httpPort,
56
64
  };
57
65
  }
@@ -1,9 +1,11 @@
1
+ import type { DatabaseSync } from "node:sqlite";
1
2
  /**
2
3
  * DataHub market data tools registration.
3
4
  * Tools: fin_price, fin_kline, fin_crypto, fin_compare, fin_slim_search
4
5
  */
5
6
  import { Type } from "@sinclair/typebox";
6
- import type { OpenClawPluginApi } from "openfinclaw/plugin-sdk";
7
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
8
+ import { withLogging } from "../middleware/with-logging.js";
7
9
  import type { UnifiedPluginConfig, MarketType } from "../types.js";
8
10
  import { DataHubClient, guessMarket } from "./client.js";
9
11
 
@@ -29,8 +31,13 @@ const NO_KEY =
29
31
 
30
32
  /**
31
33
  * Register DataHub market data tools.
34
+ * @param db - SQLite database for activity logging.
32
35
  */
33
- export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPluginConfig): void {
36
+ export function registerDatahubTools(
37
+ api: OpenClawPluginApi,
38
+ config: UnifiedPluginConfig,
39
+ db: DatabaseSync,
40
+ ): void {
34
41
  const datahubClient = config.apiKey
35
42
  ? new DataHubClient(config.datahubGatewayUrl, config.apiKey, config.requestTimeoutMs)
36
43
  : null;
@@ -84,7 +91,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
84
91
  }),
85
92
  ),
86
93
  }),
87
- async execute(_id: string, params: Record<string, unknown>) {
94
+ execute: withLogging(db, "fin_price", "market-data", async (_id, params) => {
88
95
  try {
89
96
  if (!datahubClient) return json({ error: NO_KEY });
90
97
  const symbol = String(params.symbol);
@@ -100,7 +107,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
100
107
  } catch (err) {
101
108
  return json({ error: err instanceof Error ? err.message : String(err) });
102
109
  }
103
- },
110
+ }),
104
111
  },
105
112
  { names: ["fin_price"] },
106
113
  );
@@ -128,7 +135,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
128
135
  Type.Number({ description: "Number of bars to return (default: 30)" }),
129
136
  ),
130
137
  }),
131
- async execute(_id: string, params: Record<string, unknown>) {
138
+ execute: withLogging(db, "fin_kline", "market-data", async (_id, params) => {
132
139
  try {
133
140
  if (!datahubClient) return json({ error: NO_KEY });
134
141
  const symbol = String(params.symbol);
@@ -151,7 +158,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
151
158
  } catch (err) {
152
159
  return json({ error: err instanceof Error ? err.message : String(err) });
153
160
  }
154
- },
161
+ }),
155
162
  },
156
163
  { names: ["fin_kline"] },
157
164
  );
@@ -202,7 +209,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
202
209
  end_date: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })),
203
210
  limit: Type.Optional(Type.Number({ description: "Max results (default: 20)" })),
204
211
  }),
205
- async execute(_id: string, params: Record<string, unknown>) {
212
+ execute: withLogging(db, "fin_crypto", "market-data", async (_id, params) => {
206
213
  try {
207
214
  if (!datahubClient) return json({ error: NO_KEY });
208
215
  const endpoint = String(params.endpoint ?? "coin/market");
@@ -231,7 +238,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
231
238
  } catch (err) {
232
239
  return json({ error: err instanceof Error ? err.message : String(err) });
233
240
  }
234
- },
241
+ }),
235
242
  },
236
243
  { names: ["fin_crypto"] },
237
244
  );
@@ -249,7 +256,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
249
256
  description: "Comma-separated symbols (2-5). Example: BTC/USDT,ETH/USDT,600519.SH",
250
257
  }),
251
258
  }),
252
- async execute(_id: string, params: Record<string, unknown>) {
259
+ execute: withLogging(db, "fin_compare", "market-data", async (_id, params) => {
253
260
  try {
254
261
  if (!datahubClient) return json({ error: NO_KEY });
255
262
  const raw = String(params.symbols);
@@ -287,7 +294,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
287
294
  } catch (err) {
288
295
  return json({ error: err instanceof Error ? err.message : String(err) });
289
296
  }
290
- },
297
+ }),
291
298
  },
292
299
  { names: ["fin_compare"] },
293
300
  );
@@ -310,7 +317,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
310
317
  }),
311
318
  ),
312
319
  }),
313
- async execute(_id: string, params: Record<string, unknown>) {
320
+ execute: withLogging(db, "fin_slim_search", "market-data", async (_id, params) => {
314
321
  try {
315
322
  if (!datahubClient) return json({ error: NO_KEY });
316
323
  const q = String(params.query);
@@ -340,7 +347,7 @@ export function registerDatahubTools(api: OpenClawPluginApi, config: UnifiedPlug
340
347
  } catch (err) {
341
348
  return json({ error: err instanceof Error ? err.message : String(err) });
342
349
  }
343
- },
350
+ }),
344
351
  },
345
352
  { names: ["fin_slim_search"] },
346
353
  );
package/src/db/db.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { mkdirSync } from "node:fs";
2
+ /**
3
+ * SQLite database singleton for OpenFinClaw plugin.
4
+ * Database path: ~/.openfinclaw/workspace/openfinclaw-plugin.db
5
+ */
6
+ import { createRequire } from "node:module";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import type { DatabaseSync } from "node:sqlite";
10
+ import { ensureSchema } from "./schema.js";
11
+
12
+ // Use createRequire to load the built-in node:sqlite in an ESM context.
13
+ const _require = createRequire(import.meta.url);
14
+
15
+ let _db: DatabaseSync | null = null;
16
+
17
+ /** Resolve the database file path under ~/.openfinclaw/workspace/. */
18
+ function resolveDbPath(): string {
19
+ const base = join(homedir(), ".openfinclaw", "workspace");
20
+ mkdirSync(base, { recursive: true });
21
+ return join(base, "openfinclaw-plugin.db");
22
+ }
23
+
24
+ /**
25
+ * Get (or lazily initialise) the SQLite database singleton.
26
+ * Calls ensureSchema on first access to create missing tables.
27
+ */
28
+ export function getDb(): DatabaseSync {
29
+ if (_db) return _db;
30
+ // node:sqlite is available in Node 22+
31
+ const { DatabaseSync } = _require("node:sqlite") as typeof import("node:sqlite");
32
+ const dbPath = resolveDbPath();
33
+ _db = new DatabaseSync(dbPath);
34
+ ensureSchema(_db);
35
+ return _db;
36
+ }
37
+
38
+ /** Close the database (used in tests / graceful shutdown). */
39
+ export function closeDb(): void {
40
+ if (_db) {
41
+ _db.close();
42
+ _db = null;
43
+ }
44
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Data access helpers for OpenFinClaw plugin SQLite tables.
3
+ */
4
+ import type { DatabaseSync } from "node:sqlite";
5
+
6
+ // ── Row types ─────────────────────────────────────────────────────────────
7
+
8
+ export interface ActivityLogEntry {
9
+ id: string;
10
+ timestamp: string;
11
+ category: string;
12
+ action: string;
13
+ strategy_id?: string | null;
14
+ detail?: string | null;
15
+ metadata_json?: string | null;
16
+ }
17
+
18
+ export interface AgentEventEntry {
19
+ id: string;
20
+ type: string;
21
+ title: string;
22
+ detail?: string | null;
23
+ timestamp: string;
24
+ status?: string | null;
25
+ action_params_json?: string | null;
26
+ narration?: string | null;
27
+ feed_type?: string | null;
28
+ chips_json?: string | null;
29
+ sparkline_json?: string | null;
30
+ category?: string | null;
31
+ severity?: string | null;
32
+ strategy_id?: string | null;
33
+ reasoning?: string | null;
34
+ }
35
+
36
+ /**
37
+ * Strategy lifecycle levels:
38
+ * - L0: 创建 / Fork(尚未回测)
39
+ * - L1: 回测中 / 回测完成
40
+ * - L2: 模拟盘运行中(预留)
41
+ * - L3: 实盘运行中(预留)
42
+ */
43
+ export type StrategyLevel = "L0" | "L1" | "L2" | "L3";
44
+
45
+ export interface StrategyRow {
46
+ id: string;
47
+ name: string;
48
+ template_id?: string | null;
49
+ /** 策略生命周期阶段:L0 创建 → L1 回测 → L2 模拟盘 → L3 实盘 */
50
+ level?: StrategyLevel | null;
51
+ status: string;
52
+ symbols?: string | null;
53
+ timeframes?: string | null;
54
+ markets?: string | null;
55
+ exchange_id?: string | null;
56
+ parameters?: string | null;
57
+ definition?: string | null;
58
+ version: number;
59
+ created_at: string;
60
+ updated_at: string;
61
+ promoted_at?: string | null;
62
+ last_backtest_id?: string | null;
63
+ last_paper_session_id?: string | null;
64
+ tags?: string | null;
65
+ }
66
+
67
+ export interface BacktestRow {
68
+ id: string;
69
+ strategy_id: string;
70
+ remote_task_id?: string | null;
71
+ status: string;
72
+ total_return?: number | null;
73
+ sharpe?: number | null;
74
+ sortino?: number | null;
75
+ max_drawdown?: number | null;
76
+ win_rate?: number | null;
77
+ profit_factor?: number | null;
78
+ total_trades?: number | null;
79
+ final_equity?: number | null;
80
+ initial_capital?: number | null;
81
+ equity_curve?: string | null;
82
+ trade_journal?: string | null;
83
+ monthly_returns?: string | null;
84
+ tearsheet_html?: string | null;
85
+ submitted_at?: string | null;
86
+ completed_at?: string | null;
87
+ created_at: string;
88
+ }
89
+
90
+ // ── Writes ────────────────────────────────────────────────────────────────
91
+
92
+ /** Insert one row into agent_activity_log. Silently swallows errors to never interrupt tools. */
93
+ export function insertActivityLog(db: DatabaseSync, entry: ActivityLogEntry): void {
94
+ try {
95
+ db.prepare(`
96
+ INSERT INTO agent_activity_log
97
+ (id, timestamp, category, action, strategy_id, detail, metadata_json)
98
+ VALUES (?, ?, ?, ?, ?, ?, ?)
99
+ `).run(
100
+ entry.id,
101
+ entry.timestamp,
102
+ entry.category,
103
+ entry.action,
104
+ entry.strategy_id ?? null,
105
+ entry.detail ?? null,
106
+ entry.metadata_json ?? null,
107
+ );
108
+ } catch {
109
+ // Logging must never crash the calling tool
110
+ }
111
+ }
112
+
113
+ /** Insert one row into agent_events. Silently swallows errors. */
114
+ export function insertAgentEvent(db: DatabaseSync, event: AgentEventEntry): void {
115
+ try {
116
+ db.prepare(`
117
+ INSERT INTO agent_events
118
+ (id, type, title, detail, timestamp, status, action_params_json,
119
+ narration, feed_type, chips_json, sparkline_json, category, severity,
120
+ strategy_id, reasoning)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
122
+ `).run(
123
+ event.id,
124
+ event.type,
125
+ event.title,
126
+ event.detail ?? null,
127
+ event.timestamp,
128
+ event.status ?? null,
129
+ event.action_params_json ?? null,
130
+ event.narration ?? null,
131
+ event.feed_type ?? null,
132
+ event.chips_json ?? null,
133
+ event.sparkline_json ?? null,
134
+ event.category ?? null,
135
+ event.severity ?? null,
136
+ event.strategy_id ?? null,
137
+ event.reasoning ?? null,
138
+ );
139
+ } catch {
140
+ // Logging must never crash the calling tool
141
+ }
142
+ }
143
+
144
+ /** Insert or replace a strategy row. */
145
+ export function upsertStrategy(db: DatabaseSync, row: StrategyRow): void {
146
+ try {
147
+ db.prepare(`
148
+ INSERT INTO strategies
149
+ (id, name, template_id, level, status, symbols, timeframes, markets,
150
+ exchange_id, parameters, definition, version, created_at, updated_at,
151
+ promoted_at, last_backtest_id, last_paper_session_id, tags)
152
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
153
+ ON CONFLICT(id) DO UPDATE SET
154
+ name = excluded.name,
155
+ template_id = excluded.template_id,
156
+ level = excluded.level,
157
+ status = excluded.status,
158
+ symbols = excluded.symbols,
159
+ timeframes = excluded.timeframes,
160
+ markets = excluded.markets,
161
+ exchange_id = excluded.exchange_id,
162
+ parameters = excluded.parameters,
163
+ definition = excluded.definition,
164
+ version = excluded.version,
165
+ updated_at = excluded.updated_at,
166
+ promoted_at = excluded.promoted_at,
167
+ last_backtest_id = excluded.last_backtest_id,
168
+ last_paper_session_id = excluded.last_paper_session_id,
169
+ tags = excluded.tags
170
+ `).run(
171
+ row.id,
172
+ row.name,
173
+ row.template_id ?? null,
174
+ row.level ?? null,
175
+ row.status,
176
+ row.symbols ?? null,
177
+ row.timeframes ?? null,
178
+ row.markets ?? null,
179
+ row.exchange_id ?? null,
180
+ row.parameters ?? null,
181
+ row.definition ?? null,
182
+ row.version,
183
+ row.created_at,
184
+ row.updated_at,
185
+ row.promoted_at ?? null,
186
+ row.last_backtest_id ?? null,
187
+ row.last_paper_session_id ?? null,
188
+ row.tags ?? null,
189
+ );
190
+ } catch {
191
+ // Logging must never crash the calling tool
192
+ }
193
+ }
194
+
195
+ /** Insert a backtest result row. */
196
+ export function insertBacktestResult(db: DatabaseSync, row: BacktestRow): void {
197
+ try {
198
+ db.prepare(`
199
+ INSERT INTO backtest_results
200
+ (id, strategy_id, remote_task_id, status, total_return, sharpe, sortino,
201
+ max_drawdown, win_rate, profit_factor, total_trades, final_equity,
202
+ initial_capital, equity_curve, trade_journal, monthly_returns,
203
+ tearsheet_html, submitted_at, completed_at, created_at)
204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
205
+ `).run(
206
+ row.id,
207
+ row.strategy_id,
208
+ row.remote_task_id ?? null,
209
+ row.status,
210
+ row.total_return ?? null,
211
+ row.sharpe ?? null,
212
+ row.sortino ?? null,
213
+ row.max_drawdown ?? null,
214
+ row.win_rate ?? null,
215
+ row.profit_factor ?? null,
216
+ row.total_trades ?? null,
217
+ row.final_equity ?? null,
218
+ row.initial_capital ?? null,
219
+ row.equity_curve ?? null,
220
+ row.trade_journal ?? null,
221
+ row.monthly_returns ?? null,
222
+ row.tearsheet_html ?? null,
223
+ row.submitted_at ?? null,
224
+ row.completed_at ?? null,
225
+ row.created_at,
226
+ );
227
+ } catch {
228
+ // Logging must never crash the calling tool
229
+ }
230
+ }
231
+
232
+ /** Update backtest_results metrics after publish_verify completes. */
233
+ export function updateBacktestResult(
234
+ db: DatabaseSync,
235
+ id: string,
236
+ patch: Partial<
237
+ Pick<
238
+ BacktestRow,
239
+ | "status"
240
+ | "total_return"
241
+ | "sharpe"
242
+ | "sortino"
243
+ | "max_drawdown"
244
+ | "win_rate"
245
+ | "profit_factor"
246
+ | "total_trades"
247
+ | "final_equity"
248
+ | "equity_curve"
249
+ | "trade_journal"
250
+ | "monthly_returns"
251
+ | "tearsheet_html"
252
+ | "completed_at"
253
+ >
254
+ >,
255
+ ): void {
256
+ try {
257
+ const sets: string[] = [];
258
+ const values: unknown[] = [];
259
+ for (const [k, v] of Object.entries(patch)) {
260
+ sets.push(`${k} = ?`);
261
+ values.push(v ?? null);
262
+ }
263
+ if (sets.length === 0) return;
264
+ values.push(id);
265
+ db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...values);
266
+ } catch {
267
+ // Logging must never crash the calling tool
268
+ }
269
+ }
270
+
271
+ /** Update strategy level by id. */
272
+ export function updateStrategyLevel(db: DatabaseSync, id: string, level: StrategyLevel): void {
273
+ try {
274
+ db.prepare(`UPDATE strategies SET level = ?, updated_at = ? WHERE id = ?`).run(
275
+ level,
276
+ new Date().toISOString(),
277
+ id,
278
+ );
279
+ } catch {
280
+ // Logging must never crash the calling tool
281
+ }
282
+ }
283
+
284
+ // ── Reads ─────────────────────────────────────────────────────────────────
285
+
286
+ /** Query agent_activity_log, newest first. */
287
+ export function queryActivityLog(db: DatabaseSync, limit = 50, offset = 0): ActivityLogEntry[] {
288
+ return db
289
+ .prepare(`SELECT * FROM agent_activity_log ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
290
+ .all(limit, offset) as ActivityLogEntry[];
291
+ }
292
+
293
+ /** Query agent_events, newest first. */
294
+ export function queryAgentEvents(db: DatabaseSync, limit = 50, offset = 0): AgentEventEntry[] {
295
+ return db
296
+ .prepare(`SELECT * FROM agent_events ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
297
+ .all(limit, offset) as AgentEventEntry[];
298
+ }
299
+
300
+ /** Query all strategies, newest first. */
301
+ export function queryStrategies(db: DatabaseSync): StrategyRow[] {
302
+ return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as StrategyRow[];
303
+ }
304
+
305
+ /** Query backtest results, optionally filtered by strategy_id. */
306
+ export function queryBacktestResults(db: DatabaseSync, strategyId?: string): BacktestRow[] {
307
+ if (strategyId) {
308
+ return db
309
+ .prepare(`SELECT * FROM backtest_results WHERE strategy_id = ? ORDER BY created_at DESC`)
310
+ .all(strategyId) as BacktestRow[];
311
+ }
312
+ return db
313
+ .prepare(`SELECT * FROM backtest_results ORDER BY created_at DESC`)
314
+ .all() as BacktestRow[];
315
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * SQLite schema for OpenFinClaw plugin (MVP 4 tables).
3
+ * Based on ER diagram v0.3 — openfinclaw-opc-fund-plugin.
4
+ */
5
+ import type { DatabaseSync } from "node:sqlite";
6
+
7
+ /**
8
+ * Create (or migrate) all MVP tables.
9
+ * Safe to call multiple times — uses CREATE TABLE IF NOT EXISTS.
10
+ */
11
+ export function ensureSchema(db: DatabaseSync): void {
12
+ // ── strategies ────────────────────────────────────────────────────────────
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS strategies (
15
+ id TEXT PRIMARY KEY,
16
+ name TEXT NOT NULL,
17
+ template_id TEXT,
18
+ level TEXT DEFAULT 'L0',
19
+ status TEXT NOT NULL DEFAULT 'draft',
20
+ symbols TEXT,
21
+ timeframes TEXT,
22
+ markets TEXT,
23
+ exchange_id TEXT,
24
+ parameters TEXT,
25
+ definition TEXT,
26
+ version INTEGER NOT NULL DEFAULT 1,
27
+ created_at TEXT NOT NULL,
28
+ updated_at TEXT NOT NULL,
29
+ promoted_at TEXT,
30
+ last_backtest_id TEXT,
31
+ last_paper_session_id TEXT,
32
+ tags TEXT
33
+ );
34
+ `);
35
+
36
+ // ── backtest_results ──────────────────────────────────────────────────────
37
+ db.exec(`
38
+ CREATE TABLE IF NOT EXISTS backtest_results (
39
+ id TEXT PRIMARY KEY,
40
+ strategy_id TEXT NOT NULL,
41
+ remote_task_id TEXT,
42
+ status TEXT NOT NULL DEFAULT 'pending',
43
+ total_return REAL,
44
+ sharpe REAL,
45
+ sortino REAL,
46
+ max_drawdown REAL,
47
+ win_rate REAL,
48
+ profit_factor REAL,
49
+ total_trades INTEGER,
50
+ final_equity REAL,
51
+ initial_capital REAL,
52
+ equity_curve TEXT,
53
+ trade_journal TEXT,
54
+ monthly_returns TEXT,
55
+ tearsheet_html TEXT,
56
+ submitted_at TEXT,
57
+ completed_at TEXT,
58
+ created_at TEXT NOT NULL,
59
+ FOREIGN KEY (strategy_id) REFERENCES strategies(id)
60
+ );
61
+ `);
62
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_br_strategy_id ON backtest_results(strategy_id);`);
63
+
64
+ // ── agent_activity_log ────────────────────────────────────────────────────
65
+ db.exec(`
66
+ CREATE TABLE IF NOT EXISTS agent_activity_log (
67
+ id TEXT PRIMARY KEY,
68
+ timestamp TEXT NOT NULL,
69
+ category TEXT NOT NULL,
70
+ action TEXT NOT NULL,
71
+ strategy_id TEXT,
72
+ detail TEXT,
73
+ metadata_json TEXT
74
+ );
75
+ `);
76
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_aal_timestamp ON agent_activity_log(timestamp);`);
77
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_aal_category ON agent_activity_log(category);`);
78
+
79
+ // ── agent_events ──────────────────────────────────────────────────────────
80
+ db.exec(`
81
+ CREATE TABLE IF NOT EXISTS agent_events (
82
+ id TEXT PRIMARY KEY,
83
+ type TEXT NOT NULL,
84
+ title TEXT NOT NULL,
85
+ detail TEXT,
86
+ timestamp TEXT NOT NULL,
87
+ status TEXT,
88
+ action_params_json TEXT,
89
+ narration TEXT,
90
+ feed_type TEXT,
91
+ chips_json TEXT,
92
+ sparkline_json TEXT,
93
+ category TEXT,
94
+ severity TEXT,
95
+ strategy_id TEXT,
96
+ reasoning TEXT
97
+ );
98
+ `);
99
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_ae_timestamp ON agent_events(timestamp);`);
100
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_ae_strategy_id ON agent_events(strategy_id);`);
101
+
102
+ // ── migrations (add columns to existing databases) ──────────────────────
103
+ }
104
+
105
+ /** Add a column if it doesn't already exist (safe for repeated calls). */
106
+ function ensureColumn(db: DatabaseSync, table: string, column: string, definition: string): void {
107
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
108
+ if (rows.some((r) => r.name === column)) return;
109
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
110
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * REST route handlers for the OpenFinClaw dashboard HTTP server.
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ import type { IncomingMessage, ServerResponse } from "node:http";
6
+ import { join } from "node:path";
7
+ import type { DatabaseSync } from "node:sqlite";
8
+ import { fileURLToPath } from "node:url";
9
+ import {
10
+ queryActivityLog,
11
+ queryAgentEvents,
12
+ queryStrategies,
13
+ queryBacktestResults,
14
+ } from "../db/repositories.js";
15
+
16
+ // Resolve path to web/index.html relative to this file's location
17
+ const WEB_DIR = join(fileURLToPath(import.meta.url), "..", "..", "..", "web");
18
+
19
+ function getIndexHtml(): string {
20
+ return readFileSync(join(WEB_DIR, "index.html"), "utf8");
21
+ }
22
+
23
+ function parseQueryParam(url: string, name: string, fallback: number): number {
24
+ try {
25
+ const u = new URL(url, "http://localhost");
26
+ const v = u.searchParams.get(name);
27
+ const n = v != null ? parseInt(v, 10) : NaN;
28
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
29
+ } catch {
30
+ return fallback;
31
+ }
32
+ }
33
+
34
+ function parseStringParam(url: string, name: string): string | undefined {
35
+ try {
36
+ const u = new URL(url, "http://localhost");
37
+ return u.searchParams.get(name) ?? undefined;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ function sendJson(res: ServerResponse, data: unknown): void {
44
+ const body = JSON.stringify(data);
45
+ res.writeHead(200, {
46
+ "Content-Type": "application/json",
47
+ "Content-Length": Buffer.byteLength(body),
48
+ "Access-Control-Allow-Origin": "*",
49
+ });
50
+ res.end(body);
51
+ }
52
+
53
+ function sendHtml(res: ServerResponse, html: string): void {
54
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
55
+ res.end(html);
56
+ }
57
+
58
+ function send404(res: ServerResponse): void {
59
+ res.writeHead(404, { "Content-Type": "text/plain" });
60
+ res.end("Not found");
61
+ }
62
+
63
+ /**
64
+ * Dispatch an incoming HTTP request to the appropriate handler.
65
+ */
66
+ export function handleRoute(db: DatabaseSync, req: IncomingMessage, res: ServerResponse): void {
67
+ const url = req.url ?? "/";
68
+ const pathname = url.split("?")[0];
69
+
70
+ // Dashboard HTML
71
+ if (pathname === "/" || pathname === "/index.html") {
72
+ try {
73
+ sendHtml(res, getIndexHtml());
74
+ } catch {
75
+ res.writeHead(500);
76
+ res.end("Failed to load dashboard HTML");
77
+ }
78
+ return;
79
+ }
80
+
81
+ if (pathname === "/api/activity-log") {
82
+ const limit = parseQueryParam(url, "limit", 50);
83
+ const offset = parseQueryParam(url, "offset", 0);
84
+ sendJson(res, queryActivityLog(db, limit, offset));
85
+ return;
86
+ }
87
+
88
+ if (pathname === "/api/agent-events") {
89
+ const limit = parseQueryParam(url, "limit", 50);
90
+ const offset = parseQueryParam(url, "offset", 0);
91
+ sendJson(res, queryAgentEvents(db, limit, offset));
92
+ return;
93
+ }
94
+
95
+ if (pathname === "/api/strategies") {
96
+ sendJson(res, queryStrategies(db));
97
+ return;
98
+ }
99
+
100
+ if (pathname === "/api/backtest-results") {
101
+ const strategyId = parseStringParam(url, "strategy_id");
102
+ sendJson(res, queryBacktestResults(db, strategyId));
103
+ return;
104
+ }
105
+
106
+ send404(res);
107
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Embedded HTTP server for the OpenFinClaw dashboard.
3
+ * Binds to 127.0.0.1 only (loopback) for local access.
4
+ */
5
+ import { createServer } from "node:http";
6
+ import type { DatabaseSync } from "node:sqlite";
7
+ import { handleRoute } from "./routes.js";
8
+
9
+ export interface DashboardLogger {
10
+ info(msg: string): void;
11
+ }
12
+
13
+ /**
14
+ * Start the dashboard HTTP server.
15
+ * Errors during individual requests are caught and returned as 500s.
16
+ */
17
+ export function startHttpServer(db: DatabaseSync, port: number, logger: DashboardLogger): void {
18
+ const server = createServer((req, res) => {
19
+ try {
20
+ handleRoute(db, req, res);
21
+ } catch (err) {
22
+ if (!res.headersSent) {
23
+ res.writeHead(500, { "Content-Type": "text/plain" });
24
+ }
25
+ res.end(`Internal Server Error: ${String(err)}`);
26
+ }
27
+ });
28
+
29
+ server.on("error", (err) => {
30
+ logger.info(`[OpenFinClaw] Dashboard server error: ${String(err)}`);
31
+ });
32
+
33
+ server.listen(port, "127.0.0.1", () => {
34
+ logger.info(`[OpenFinClaw] Dashboard available at http://127.0.0.1:${port}`);
35
+ });
36
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Higher-order function that wraps a tool execute() to automatically log
3
+ * every invocation (success or failure) into agent_activity_log.
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ import type { DatabaseSync } from "node:sqlite";
7
+ import { insertActivityLog } from "../db/repositories.js";
8
+
9
+ /**
10
+ * Generic tool execute function signature.
11
+ * Uses `(...args: never[])` rest params so the wrapper is assignable to any
12
+ * concrete execute overload the SDK defines (extra optional params like
13
+ * `signal` and `onUpdate` are passed through transparently).
14
+ */
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ type AnyExecuteFn = (
17
+ toolCallId: string,
18
+ params: Record<string, unknown>,
19
+ ...rest: any[]
20
+ ) => Promise<any>;
21
+
22
+ /**
23
+ * Wrap a tool execute function with automatic activity logging.
24
+ *
25
+ * @param db - The SQLite database instance.
26
+ * @param toolName - Name used as the `action` column value.
27
+ * @param category - Broad category, e.g. "market-data" or "strategy".
28
+ * @param fn - The original execute function to wrap.
29
+ * @param opts - Optional: extract strategy_id from params for context.
30
+ */
31
+ export function withLogging<T extends AnyExecuteFn>(
32
+ db: DatabaseSync,
33
+ toolName: string,
34
+ category: string,
35
+ fn: T,
36
+ opts?: { strategyIdParam?: string },
37
+ ): T {
38
+ const wrapped: AnyExecuteFn = async (toolCallId, params, ...rest) => {
39
+ const logId = randomUUID();
40
+ const startMs = Date.now();
41
+ const strategyId =
42
+ opts?.strategyIdParam != null
43
+ ? ((params[opts.strategyIdParam] as string | undefined) ?? null)
44
+ : null;
45
+
46
+ try {
47
+ const result = await fn(toolCallId, params, ...rest);
48
+ insertActivityLog(db, {
49
+ id: logId,
50
+ timestamp: new Date().toISOString(),
51
+ category,
52
+ action: toolName,
53
+ strategy_id: strategyId,
54
+ detail: `OK (${Date.now() - startMs}ms)`,
55
+ metadata_json: JSON.stringify({ toolCallId, params }),
56
+ });
57
+ return result;
58
+ } catch (err) {
59
+ insertActivityLog(db, {
60
+ id: logId,
61
+ timestamp: new Date().toISOString(),
62
+ category,
63
+ action: toolName,
64
+ strategy_id: strategyId,
65
+ detail: `ERROR (${Date.now() - startMs}ms): ${String(err)}`,
66
+ metadata_json: JSON.stringify({ toolCallId, params }),
67
+ });
68
+ throw err;
69
+ }
70
+ };
71
+ return wrapped as T;
72
+ }
@@ -1,10 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  /**
2
3
  * Strategy tools registration.
3
4
  * Tools: skill_publish, skill_publish_verify, skill_validate, skill_leaderboard, skill_fork, skill_list_local, skill_get_info
4
5
  */
5
6
  import { readFile } from "node:fs/promises";
7
+ import type { DatabaseSync } from "node:sqlite";
6
8
  import { Type } from "@sinclair/typebox";
7
9
  import type { OpenClawPluginApi } from "openfinclaw/plugin-sdk";
10
+ import {
11
+ insertAgentEvent,
12
+ insertBacktestResult,
13
+ updateBacktestResult,
14
+ updateStrategyLevel,
15
+ upsertStrategy,
16
+ } from "../db/repositories.js";
17
+ import { withLogging } from "../middleware/with-logging.js";
8
18
  import type { UnifiedPluginConfig, BoardType, LeaderboardResponse } from "../types.js";
9
19
  import { hubApiRequest } from "./client.js";
10
20
  import { forkStrategy, fetchStrategyInfo } from "./fork.js";
@@ -24,8 +34,13 @@ const NO_API_KEY =
24
34
 
25
35
  /**
26
36
  * Register strategy tools.
37
+ * @param db - SQLite database for activity logging and domain table writes.
27
38
  */
28
- export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPluginConfig): void {
39
+ export function registerStrategyTools(
40
+ api: OpenClawPluginApi,
41
+ config: UnifiedPluginConfig,
42
+ db: DatabaseSync,
43
+ ): void {
29
44
  // ── skill_publish ──
30
45
  api.registerTool(
31
46
  {
@@ -45,7 +60,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
45
60
  }),
46
61
  ),
47
62
  }),
48
- async execute(_toolCallId: string, params: Record<string, unknown>) {
63
+ execute: withLogging(db, "skill_publish", "strategy", async (_toolCallId, params) => {
49
64
  try {
50
65
  const filePath = String(params.filePath ?? "").trim();
51
66
  if (!filePath) return json({ success: false, error: "filePath is required" });
@@ -87,6 +102,57 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
87
102
  creditsEarned?: { action?: string; amount?: number; message?: string };
88
103
  };
89
104
 
105
+ // Persist strategy + pending backtest result
106
+ const now = new Date().toISOString();
107
+ const strategyId = resp.entryId ?? resp.slug ?? randomUUID();
108
+ const hubVersion = Number(resp.version) || 1;
109
+ upsertStrategy(db, {
110
+ id: strategyId,
111
+ name: resp.slug ?? filePath,
112
+ status: "published",
113
+ level: resp.backtestTaskId ? "L1" : "L0",
114
+ version: hubVersion,
115
+ created_at: now,
116
+ updated_at: now,
117
+ promoted_at: now,
118
+ });
119
+
120
+ if (resp.backtestTaskId) {
121
+ const backtestId = randomUUID();
122
+ insertBacktestResult(db, {
123
+ id: backtestId,
124
+ strategy_id: strategyId,
125
+ remote_task_id: resp.backtestTaskId,
126
+ status: "pending",
127
+ submitted_at: now,
128
+ created_at: now,
129
+ });
130
+ upsertStrategy(db, {
131
+ id: strategyId,
132
+ name: resp.slug ?? filePath,
133
+ status: "published",
134
+ level: "L1",
135
+ version: hubVersion,
136
+ created_at: now,
137
+ updated_at: now,
138
+ promoted_at: now,
139
+ last_backtest_id: backtestId,
140
+ });
141
+ }
142
+
143
+ insertAgentEvent(db, {
144
+ id: randomUUID(),
145
+ type: "publish",
146
+ title: `策略发布: ${resp.slug ?? "(未知)"}`,
147
+ detail: `Entry ID: ${resp.entryId ?? "—"} | Backtest: ${resp.backtestTaskId ?? "—"}`,
148
+ timestamp: now,
149
+ status: "info",
150
+ category: "strategy",
151
+ severity: "low",
152
+ strategy_id: strategyId,
153
+ narration: `策略 ${resp.slug ?? filePath} 已成功发布到 Hub`,
154
+ });
155
+
90
156
  const lines: string[] = [];
91
157
  lines.push("Skill 发布成功!");
92
158
  lines.push("");
@@ -118,7 +184,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
118
184
  error: err instanceof Error ? err.message : String(err),
119
185
  });
120
186
  }
121
- },
187
+ }),
122
188
  },
123
189
  { names: ["skill_publish"] },
124
190
  );
@@ -138,7 +204,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
138
204
  Type.String({ description: "Backtest task ID from skill_publish response" }),
139
205
  ),
140
206
  }),
141
- async execute(_toolCallId: string, params: Record<string, unknown>) {
207
+ execute: withLogging(db, "skill_publish_verify", "strategy", async (_toolCallId, params) => {
142
208
  try {
143
209
  const submissionId = String(params.submissionId ?? "").trim() || undefined;
144
210
  const backtestTaskId = String(params.backtestTaskId ?? "").trim() || undefined;
@@ -167,6 +233,31 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
167
233
 
168
234
  if (status >= 200 && status < 300) {
169
235
  const resp = data as Record<string, unknown>;
236
+
237
+ // Update backtest result metrics and strategy level
238
+ const verifyStrategyId = resp.entryId as string | undefined;
239
+ if (resp.backtestStatus === "completed" && backtestTaskId) {
240
+ const perf = (resp.backtestReport as Record<string, unknown> | undefined)
241
+ ?.performance as Record<string, unknown> | undefined;
242
+ updateBacktestResult(db, backtestTaskId, {
243
+ status: "completed",
244
+ total_return: typeof perf?.totalReturn === "number" ? perf.totalReturn : undefined,
245
+ sharpe: typeof perf?.sharpe === "number" ? perf.sharpe : undefined,
246
+ max_drawdown: typeof perf?.maxDrawdown === "number" ? perf.maxDrawdown : undefined,
247
+ win_rate: typeof perf?.winRate === "number" ? perf.winRate : undefined,
248
+ completed_at: new Date().toISOString(),
249
+ });
250
+ // Backtest completed → strategy stays at L1
251
+ if (verifyStrategyId) updateStrategyLevel(db, verifyStrategyId, "L1");
252
+ } else if (resp.backtestStatus === "failed" && backtestTaskId) {
253
+ updateBacktestResult(db, backtestTaskId, {
254
+ status: "failed",
255
+ completed_at: new Date().toISOString(),
256
+ });
257
+ // Backtest failed → revert to L0
258
+ if (verifyStrategyId) updateStrategyLevel(db, verifyStrategyId, "L0");
259
+ }
260
+
170
261
  const lines: string[] = [];
171
262
  lines.push("发布验证结果:");
172
263
  lines.push(`- Slug: ${resp.slug ?? "(未知)"}`);
@@ -211,7 +302,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
211
302
  error: err instanceof Error ? err.message : String(err),
212
303
  });
213
304
  }
214
- },
305
+ }),
215
306
  },
216
307
  { names: ["skill_publish_verify"] },
217
308
  );
@@ -228,7 +319,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
228
319
  description: "Path to strategy package directory (must contain fep.yaml)",
229
320
  }),
230
321
  }),
231
- async execute(_toolCallId: string, params: Record<string, unknown>) {
322
+ execute: withLogging(db, "skill_validate", "strategy", async (_toolCallId, params) => {
232
323
  try {
233
324
  const dirPath = String(params.dirPath ?? "").trim();
234
325
  if (!dirPath)
@@ -248,7 +339,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
248
339
  errors: [err instanceof Error ? err.message : String(err)],
249
340
  });
250
341
  }
251
- },
342
+ }),
252
343
  },
253
344
  { names: ["skill_validate"] },
254
345
  );
@@ -272,7 +363,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
272
363
  ),
273
364
  offset: Type.Optional(Type.Number({ description: "Offset for pagination" })),
274
365
  }),
275
- async execute(_toolCallId: string, params: Record<string, unknown>) {
366
+ execute: withLogging(db, "skill_leaderboard", "strategy", async (_toolCallId, params) => {
276
367
  try {
277
368
  const boardType = (params.boardType as BoardType) || "composite";
278
369
  const limit = Math.min(Math.max(Number(params.limit) || 20, 1), 100);
@@ -352,7 +443,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
352
443
  error: err instanceof Error ? err.message : String(err),
353
444
  });
354
445
  }
355
- },
446
+ }),
356
447
  },
357
448
  { names: ["skill_leaderboard"] },
358
449
  );
@@ -371,46 +462,79 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
371
462
  name: Type.Optional(Type.String({ description: "Name for the forked strategy" })),
372
463
  targetDir: Type.Optional(Type.String({ description: "Custom target directory" })),
373
464
  }),
374
- async execute(_toolCallId: string, params: Record<string, unknown>) {
375
- try {
376
- const strategyId = String(params.strategyId ?? "").trim();
377
- if (!strategyId) return json({ success: false, error: "strategyId is required" });
465
+ execute: withLogging(
466
+ db,
467
+ "skill_fork",
468
+ "strategy",
469
+ async (_toolCallId, params) => {
470
+ try {
471
+ const strategyId = String(params.strategyId ?? "").trim();
472
+ if (!strategyId) return json({ success: false, error: "strategyId is required" });
473
+
474
+ if (!config.apiKey) {
475
+ return json({
476
+ success: false,
477
+ error: "API key is required for fork operation.",
478
+ });
479
+ }
378
480
 
379
- if (!config.apiKey) {
380
- return json({
381
- success: false,
382
- error: "API key is required for fork operation.",
481
+ const result = await forkStrategy(config, strategyId, {
482
+ name: params.name ? String(params.name) : undefined,
483
+ targetDir: params.targetDir ? String(params.targetDir) : undefined,
383
484
  });
384
- }
385
485
 
386
- const result = await forkStrategy(config, strategyId, {
387
- name: params.name ? String(params.name) : undefined,
388
- targetDir: params.targetDir ? String(params.targetDir) : undefined,
389
- });
486
+ if (result.success) {
487
+ // Persist forked strategy to DB
488
+ const now = new Date().toISOString();
489
+ const localId = result.forkEntryId ?? strategyId;
490
+ upsertStrategy(db, {
491
+ id: localId,
492
+ name: result.sourceName ?? (params.name ? String(params.name) : strategyId),
493
+ template_id: result.sourceId,
494
+ status: "forked",
495
+ level: "L0",
496
+ version: Number(result.sourceVersion) || 1,
497
+ created_at: now,
498
+ updated_at: now,
499
+ });
500
+
501
+ insertAgentEvent(db, {
502
+ id: randomUUID(),
503
+ type: "fork",
504
+ title: `Fork 策略: ${result.sourceName ?? strategyId}`,
505
+ detail: `本地路径: ${result.localPath}`,
506
+ timestamp: now,
507
+ status: "info",
508
+ category: "strategy",
509
+ severity: "low",
510
+ strategy_id: localId,
511
+ narration: `已将策略 ${result.sourceName ?? strategyId} Fork 到本地`,
512
+ });
513
+
514
+ const lines: string[] = [];
515
+ lines.push("策略 Fork 成功!");
516
+ lines.push(`- 原策略: ${result.sourceName} (${result.sourceId})`);
517
+ lines.push(`- 本地路径: ${result.localPath}`);
518
+ lines.push("");
519
+ lines.push("下一步:");
520
+ lines.push(`- 编辑策略: code ${result.localPath}/scripts/strategy.py`);
390
521
 
391
- if (result.success) {
392
- const lines: string[] = [];
393
- lines.push("策略 Fork 成功!");
394
- lines.push(`- 原策略: ${result.sourceName} (${result.sourceId})`);
395
- lines.push(`- 本地路径: ${result.localPath}`);
396
- lines.push("");
397
- lines.push("下一步:");
398
- lines.push(`- 编辑策略: code ${result.localPath}/scripts/strategy.py`);
522
+ return {
523
+ content: [{ type: "text" as const, text: lines.join("\n") }],
524
+ details: result,
525
+ };
526
+ }
399
527
 
400
- return {
401
- content: [{ type: "text" as const, text: lines.join("\n") }],
402
- details: result,
403
- };
528
+ return json({ success: false, error: result.error ?? "Failed to fork strategy" });
529
+ } catch (err) {
530
+ return json({
531
+ success: false,
532
+ error: err instanceof Error ? err.message : String(err),
533
+ });
404
534
  }
405
-
406
- return json({ success: false, error: result.error ?? "Failed to fork strategy" });
407
- } catch (err) {
408
- return json({
409
- success: false,
410
- error: err instanceof Error ? err.message : String(err),
411
- });
412
- }
413
- },
535
+ },
536
+ { strategyIdParam: "strategyId" },
537
+ ),
414
538
  },
415
539
  { names: ["skill_fork"] },
416
540
  );
@@ -422,7 +546,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
422
546
  label: "List local strategies",
423
547
  description: "List all strategies downloaded or created locally, organized by date.",
424
548
  parameters: Type.Object({}),
425
- async execute() {
549
+ execute: withLogging(db, "skill_list_local", "strategy", async () => {
426
550
  try {
427
551
  const strategies = await listLocalStrategies();
428
552
 
@@ -461,7 +585,7 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
461
585
  error: err instanceof Error ? err.message : String(err),
462
586
  });
463
587
  }
464
- },
588
+ }),
465
589
  },
466
590
  { names: ["skill_list_local"] },
467
591
  );
@@ -476,45 +600,54 @@ export function registerStrategyTools(api: OpenClawPluginApi, config: UnifiedPlu
476
600
  parameters: Type.Object({
477
601
  strategyId: Type.String({ description: "Strategy ID from Hub (UUID or Hub URL)" }),
478
602
  }),
479
- async execute(_toolCallId: string, params: Record<string, unknown>) {
480
- try {
481
- const strategyId = String(params.strategyId ?? "").trim();
482
- if (!strategyId) return json({ success: false, error: "strategyId is required" });
483
-
484
- const result = await fetchStrategyInfo(config, strategyId);
485
-
486
- if (result.success && result.data) {
487
- const info = result.data;
488
- const lines: string[] = [];
489
- lines.push("策略信息:");
490
- lines.push(`- ID: ${info.id}`);
491
- lines.push(`- 名称: ${info.name}`);
492
- if (info.author?.displayName) lines.push(`- 作者: ${info.author.displayName}`);
493
- if (info.backtestResult) {
603
+ execute: withLogging(
604
+ db,
605
+ "skill_get_info",
606
+ "strategy",
607
+ async (_toolCallId, params) => {
608
+ try {
609
+ const strategyId = String(params.strategyId ?? "").trim();
610
+ if (!strategyId) return json({ success: false, error: "strategyId is required" });
611
+
612
+ const result = await fetchStrategyInfo(config, strategyId);
613
+
614
+ if (result.success && result.data) {
615
+ const info = result.data;
616
+ const lines: string[] = [];
617
+ lines.push("策略信息:");
618
+ lines.push(`- ID: ${info.id}`);
619
+ lines.push(`- 名称: ${info.name}`);
620
+ if (info.author?.displayName) lines.push(`- 作者: ${info.author.displayName}`);
621
+ if (info.backtestResult) {
622
+ lines.push("");
623
+ lines.push("绩效指标:");
624
+ if (typeof info.backtestResult.totalReturn === "number")
625
+ lines.push(`- 总收益率: ${(info.backtestResult.totalReturn * 100).toFixed(2)}%`);
626
+ if (typeof info.backtestResult.sharpe === "number")
627
+ lines.push(`- 夏普比率: ${info.backtestResult.sharpe.toFixed(3)}`);
628
+ }
494
629
  lines.push("");
495
- lines.push("绩效指标:");
496
- if (typeof info.backtestResult.totalReturn === "number")
497
- lines.push(`- 总收益率: ${(info.backtestResult.totalReturn * 100).toFixed(2)}%`);
498
- if (typeof info.backtestResult.sharpe === "number")
499
- lines.push(`- 夏普比率: ${info.backtestResult.sharpe.toFixed(3)}`);
630
+ lines.push(`Hub URL: https://hub.openfinclaw.ai/strategy/${info.id}`);
631
+
632
+ return {
633
+ content: [{ type: "text" as const, text: lines.join("\n") }],
634
+ details: { success: true, ...info },
635
+ };
500
636
  }
501
- lines.push("");
502
- lines.push(`Hub URL: https://hub.openfinclaw.ai/strategy/${info.id}`);
503
637
 
504
- return {
505
- content: [{ type: "text" as const, text: lines.join("\n") }],
506
- details: { success: true, ...info },
507
- };
638
+ return json({
639
+ success: false,
640
+ error: result.error ?? "Failed to fetch strategy info",
641
+ });
642
+ } catch (err) {
643
+ return json({
644
+ success: false,
645
+ error: err instanceof Error ? err.message : String(err),
646
+ });
508
647
  }
509
-
510
- return json({ success: false, error: result.error ?? "Failed to fetch strategy info" });
511
- } catch (err) {
512
- return json({
513
- success: false,
514
- error: err instanceof Error ? err.message : String(err),
515
- });
516
- }
517
- },
648
+ },
649
+ { strategyIdParam: "strategyId" },
650
+ ),
518
651
  },
519
652
  { names: ["skill_get_info"] },
520
653
  );
package/src/types.ts CHANGED
@@ -533,4 +533,6 @@ export interface UnifiedPluginConfig {
533
533
  datahubGatewayUrl: string;
534
534
  /** 请求超时(毫秒) */
535
535
  requestTimeoutMs: number;
536
+ /** Dashboard HTTP server port (default: 18791) */
537
+ httpPort: number;
536
538
  }