@openfinclaw/openfinclaw-strategy 2026.3.273 → 2026.3.275

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/SKILL.md CHANGED
@@ -7,6 +7,7 @@ metadata:
7
7
  requires:
8
8
  extensions: ["openfinclaw-strategy"]
9
9
  ---
10
+
10
11
  # OpenFinClaw
11
12
 
12
13
  统一金融工具平台,一个 API Key 即可使用所有功能:
@@ -121,42 +122,42 @@ export OPENFINCLAW_API_KEY=YOUR_API_KEY
121
122
 
122
123
  ### 行情数据工具
123
124
 
124
- | 工具名 | 用途 | 需要 API Key |
125
- | ------------------- | --------------------------- | ------------ |
126
- | `fin_price` | 价格查询(股票/加密/指数) | **是** |
127
- | `fin_kline` | K线/OHLCV 数据 | **是** |
128
- | `fin_crypto` | 加密市场数据(21个端点) | **是** |
129
- | `fin_compare` | 多资产价格对比(2-5个资产) | **是** |
130
- | `fin_slim_search` | 代码/名称搜索 | **是** |
125
+ | 工具名 | 用途 | 需要 API Key |
126
+ | ----------------- | --------------------------- | ------------ |
127
+ | `fin_price` | 价格查询(股票/加密/指数) | **是** |
128
+ | `fin_kline` | K线/OHLCV 数据 | **是** |
129
+ | `fin_crypto` | 加密市场数据(21个端点) | **是** |
130
+ | `fin_compare` | 多资产价格对比(2-5个资产) | **是** |
131
+ | `fin_slim_search` | 代码/名称搜索 | **是** |
131
132
 
132
133
  ### 策略工具
133
134
 
134
- | 工具名 | 用途 | 需要 API Key |
135
- | ------------------------ | -------------------------------------- | ------------ |
135
+ | 工具名 | 用途 | 需要 API Key |
136
+ | ---------------------- | -------------------------------------- | ------------ |
136
137
  | `skill_leaderboard` | 查询排行榜(综合/收益/风控/人气/新星) | 否 |
137
138
  | `skill_get_info` | 获取 Hub 策略公开详情 | 否 |
138
139
  | `skill_validate` | 本地验证策略包格式(FEP v2.0) | 否 |
139
140
  | `skill_list_local` | 列出本地已下载的策略 | 否 |
140
- | `skill_fork` | 从 Hub 下载公开策略到本地 | **是** |
141
- | `skill_publish` | 发布策略 ZIP 到 Hub,自动触发回测 | **是** |
142
- | `skill_publish_verify` | 查询发布状态和回测报告 | **是** |
141
+ | `skill_fork` | 从 Hub 下载公开策略到本地 | **是** |
142
+ | `skill_publish` | 发布策略 ZIP 到 Hub,自动触发回测 | **是** |
143
+ | `skill_publish_verify` | 查询发布状态和回测报告 | **是** |
143
144
 
144
145
  ### Skills(指导文档)
145
146
 
146
147
  #### 行情数据 Skills
147
148
 
148
- | Skill | 触发场景 | 说明 |
149
- | --------------- | ------------------ | ---------------- |
149
+ | Skill | 触发场景 | 说明 |
150
+ | ------------- | ------------------ | ---------------- |
150
151
  | `price-check` | 快速查价、XX多少钱 | 最简单的价格查询 |
151
152
 
152
153
  #### 策略 Skills
153
154
 
154
- | Skill | 触发场景 | 说明 |
155
- | -------------------- | ------------------------ | -------------------------------- |
156
- | `strategy-builder` | 创建新策略、生成策略代码 | 自然语言 → FEP v2.0 策略包 |
155
+ | Skill | 触发场景 | 说明 |
156
+ | ------------------ | ------------------------ | ----------------------------- |
157
+ | `strategy-builder` | 创建新策略、生成策略代码 | 自然语言 → FEP v2.0 策略包 |
157
158
  | `skill-publish` | 发布策略到服务器 | 验证 → 打包 → 发布 → 查询回测 |
158
- | `strategy-fork` | 下载/克隆 Hub 策略 | Fork → 本地编辑 → 发布新版本 |
159
- | `strategy-pack` | 创建回测策略包 | 生成 fep.yaml + strategy.py |
159
+ | `strategy-fork` | 下载/克隆 Hub 策略 | Fork → 本地编辑 → 发布新版本 |
160
+ | `strategy-pack` | 创建回测策略包 | 生成 fep.yaml + strategy.py |
160
161
 
161
162
  ### 典型工作流
162
163
 
@@ -186,8 +187,8 @@ openclaw strategy leaderboard popular --offset 20 --limit 20
186
187
 
187
188
  **榜单类型**:
188
189
 
189
- | 榜单类型 | 说明 | 排序依据 |
190
- | ------------- | -------------- | ---------------- |
190
+ | 榜单类型 | 说明 | 排序依据 |
191
+ | ----------- | -------------- | ---------------- |
191
192
  | `composite` | 综合榜(默认) | FCS 综合分 |
192
193
  | `returns` | 收益榜 | 收益率 |
193
194
  | `risk` | 风控榜 | 风控分 |
@@ -252,8 +253,8 @@ openclaw strategy show 550e8400-e29b-41d4-a716-446655440001 --remote
252
253
 
253
254
  看板包含 4 个标签页:
254
255
 
255
- | 标签页 | 数据来源 | 说明 |
256
- | -------- | ---------------------- | ------------------------------ |
256
+ | 标签页 | 数据来源 | 说明 |
257
+ | -------- | -------------------- | ------------------------------ |
257
258
  | 活动日志 | `agent_activity_log` | 所有 12 个工具的每次执行记录 |
258
259
  | 事件流 | `agent_events` | Fork、发布、回测完成等关键事件 |
259
260
  | 策略列表 | `strategies` | 本地策略生命周期状态 |
@@ -261,8 +262,8 @@ openclaw strategy show 550e8400-e29b-41d4-a716-446655440001 --remote
261
262
 
262
263
  **REST API**(供前端或其他工具调用):
263
264
 
264
- | 端点 | 说明 |
265
- | ----------------------------- | ------------------------ |
265
+ | 端点 | 说明 |
266
+ | --------------------------- | ------------------------ |
266
267
  | `GET /api/activity-log` | 工具调用日志(分页) |
267
268
  | `GET /api/agent-events` | 事件流(分页) |
268
269
  | `GET /api/strategies` | 策略列表 |
@@ -283,19 +284,19 @@ openclaw strategy show 550e8400-e29b-41d4-a716-446655440001 --remote
283
284
 
284
285
  当用户提到以下内容时,应引导阅读对应的 Skill:
285
286
 
286
- | 触发关键词 | Skill | 说明 |
287
- | ------------------------------ | -------------------- | -------------------- |
288
- | XX多少钱、什么价格、查价 | `price-check` | 最简单的价格查询 |
287
+ | 触发关键词 | Skill | 说明 |
288
+ | ------------------------------ | ------------------ | ------------------- |
289
+ | XX多少钱、什么价格、查价 | `price-check` | 最简单的价格查询 |
289
290
  | 创建策略、写策略、生成策略包 | `strategy-builder` | 自然语言 → FEP v2.0 |
290
- | 发布策略、上传策略、提交策略 | `skill-publish` | 验证 → 打包 → 发布 |
291
- | Fork 策略、下载策略、克隆策略 | `strategy-fork` | 从 Hub Fork 策略 |
292
- | 策略包格式、FEP 规范、打包回测 | `strategy-pack` | FEP v2.0 规范详解 |
291
+ | 发布策略、上传策略、提交策略 | `skill-publish` | 验证 → 打包 → 发布 |
292
+ | Fork 策略、下载策略、克隆策略 | `strategy-fork` | 从 Hub Fork 策略 |
293
+ | 策略包格式、FEP 规范、打包回测 | `strategy-pack` | FEP v2.0 规范详解 |
293
294
 
294
295
  ## 配置选项
295
296
 
296
- | 配置项 | 环境变量 | 说明 | 默认值 |
297
- | --------------------- | ------------------------- | ---------------- | ------------------------------ |
298
- | `apiKey` | `OPENFINCLAW_API_KEY` | 统一 API Key | 必填 |
297
+ | 配置项 | 环境变量 | 说明 | 默认值 |
298
+ | ------------------- | ----------------------- | ---------------- | ---------------------------- |
299
+ | `apiKey` | `OPENFINCLAW_API_KEY` | 统一 API Key | 必填 |
299
300
  | `hubApiUrl` | `HUB_API_URL` | Hub 服务地址 | `https://hub.openfinclaw.ai` |
300
301
  | `datahubGatewayUrl` | `DATAHUB_GATEWAY_URL` | DataHub 网关地址 | `http://43.134.61.136:9080` |
301
302
  | `requestTimeoutMs` | `REQUEST_TIMEOUT_MS` | 请求超时(毫秒) | `60000` |
package/index.ts CHANGED
@@ -16,6 +16,9 @@ import { getDb } from "./src/db/db.js";
16
16
  import { createOpenFinclawGatewayProxy } from "./src/http/gateway-proxy.js";
17
17
  import { startHttpServer } from "./src/http/server.js";
18
18
  import { OPENFINCLAW_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
19
+ import { setupOpenfinclawCronJobs } from "./src/scheduler/cron-setup.js";
20
+ import { AggregatedNewsProvider, createNewsProviders } from "./src/scheduler/news-provider.js";
21
+ import { registerSchedulerTools } from "./src/scheduler/tools.js";
19
22
  import { registerStrategyTools } from "./src/strategy/tools.js";
20
23
 
21
24
  const openfinclawPlugin = {
@@ -28,14 +31,17 @@ const openfinclawPlugin = {
28
31
  register(api: OpenClawPluginApi) {
29
32
  const config = resolvePluginConfig(api);
30
33
 
31
- // Initialise SQLite database (creates tables on first run)
32
- const db = getDb();
33
-
34
34
  // Register DataHub market data tools (fin_price, fin_kline, fin_crypto, fin_compare, fin_slim_search)
35
- registerDatahubTools(api, config, db);
35
+ // DB init is lazy — getDb is called at tool execution time, not here.
36
+ registerDatahubTools(api, config, getDb);
36
37
 
37
38
  // Register strategy tools (skill_publish, skill_validate, skill_fork, skill_leaderboard, etc.)
38
- registerStrategyTools(api, config, db);
39
+ registerStrategyTools(api, config, getDb);
40
+
41
+ // Register scheduler tools (strategy_daily_scan, strategy_scan_history)
42
+ const newsProviders = createNewsProviders(config);
43
+ const newsProvider = new AggregatedNewsProvider(newsProviders);
44
+ registerSchedulerTools(api, config, getDb, newsProvider);
39
45
 
40
46
  // Register CLI commands
41
47
  api.registerCli(
@@ -44,7 +50,7 @@ const openfinclawPlugin = {
44
50
  program,
45
51
  config,
46
52
  logger: api.logger,
47
- db,
53
+ getDb,
48
54
  }),
49
55
  { commands: ["strategy"] },
50
56
  );
@@ -67,6 +73,25 @@ const openfinclawPlugin = {
67
73
  api.on("before_prompt_build", async () => ({
68
74
  prependSystemContext: OPENFINCLAW_AGENT_GUIDANCE,
69
75
  }));
76
+
77
+ // ── Gateway Cron registration ──
78
+ // Register cron jobs on gateway_start (writes to ~/.openclaw/cron/jobs.json)
79
+ if (config.schedulerEnabled) {
80
+ api.on("gateway_start", async () => {
81
+ try {
82
+ const result = await setupOpenfinclawCronJobs(config);
83
+ if (result.created > 0) {
84
+ api.logger.info(
85
+ `[OpenFinClaw] Cron jobs registered: ${result.created} created, ${result.existing} existing`,
86
+ );
87
+ }
88
+ } catch (err) {
89
+ api.logger.info(
90
+ `[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
91
+ );
92
+ }
93
+ });
94
+ }
70
95
  },
71
96
  };
72
97
 
@@ -38,6 +38,53 @@
38
38
  "minimum": 1024,
39
39
  "maximum": 65535,
40
40
  "description": "Dashboard HTTP server port (loopback only)"
41
+ },
42
+ "schedulerEnabled": {
43
+ "type": "boolean",
44
+ "default": true,
45
+ "description": "Enable scheduled tasks (daily scan, price monitor, reports)"
46
+ },
47
+ "scanCronExpr": {
48
+ "type": "string",
49
+ "default": "0 8 * * *",
50
+ "description": "Cron expression for daily strategy scan"
51
+ },
52
+ "monitorCronExpr": {
53
+ "type": "string",
54
+ "default": "*/30 * * * *",
55
+ "description": "Cron expression for price monitoring"
56
+ },
57
+ "weeklyReportCronExpr": {
58
+ "type": "string",
59
+ "default": "0 20 * * 0",
60
+ "description": "Cron expression for weekly report"
61
+ },
62
+ "monthlyReportCronExpr": {
63
+ "type": "string",
64
+ "default": "0 20 1 * *",
65
+ "description": "Cron expression for monthly report"
66
+ },
67
+ "scanTimezone": {
68
+ "type": "string",
69
+ "default": "Asia/Shanghai",
70
+ "description": "Timezone for scheduled tasks"
71
+ },
72
+ "newsApiKey": {
73
+ "type": "string",
74
+ "description": "Optional API key for Finnhub or NewsAPI news provider",
75
+ "sensitive": true
76
+ },
77
+ "newsProvider": {
78
+ "type": "string",
79
+ "default": "coingecko",
80
+ "description": "News provider: coingecko (free, no key), finnhub, or newsapi"
81
+ },
82
+ "priceAlertThreshold": {
83
+ "type": "number",
84
+ "default": 5,
85
+ "minimum": 0.1,
86
+ "maximum": 100,
87
+ "description": "Price change alert threshold in percent"
41
88
  }
42
89
  }
43
90
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfinclaw/openfinclaw-strategy",
3
- "version": "2026.3.273",
3
+ "version": "2026.3.275",
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",
package/src/cli.ts CHANGED
@@ -17,13 +17,13 @@ type Logger = {
17
17
 
18
18
  /** Log a CLI command execution to agent_activity_log. */
19
19
  function logCli(
20
- db: DatabaseSync,
20
+ getDb: () => DatabaseSync,
21
21
  action: string,
22
22
  params: Record<string, unknown>,
23
23
  startMs: number,
24
24
  error?: string,
25
25
  ): void {
26
- insertActivityLog(db, {
26
+ insertActivityLog(getDb(), {
27
27
  id: randomUUID(),
28
28
  timestamp: new Date().toISOString(),
29
29
  category: "strategy",
@@ -39,9 +39,9 @@ export function registerStrategyCli(params: {
39
39
  program: Command;
40
40
  config: UnifiedPluginConfig;
41
41
  logger: Logger;
42
- db: DatabaseSync;
42
+ getDb: () => DatabaseSync;
43
43
  }) {
44
- const { program, config, db } = params;
44
+ const { program, config, getDb } = params;
45
45
 
46
46
  const root = program
47
47
  .command("strategy")
@@ -72,7 +72,7 @@ export function registerStrategyCli(params: {
72
72
 
73
73
  if (!response.ok) {
74
74
  logCli(
75
- db,
75
+ getDb,
76
76
  "leaderboard",
77
77
  { boardType, limit, offset },
78
78
  startMs,
@@ -101,7 +101,7 @@ export function registerStrategyCli(params: {
101
101
  const perf = s.performance || {};
102
102
  const returnStr =
103
103
  typeof perf.returnSincePublish === "number"
104
- ? `收益: ${(perf.returnSincePublish * 100).toFixed(1)}%`
104
+ ? `收益: ${perf.returnSincePublish.toFixed(1)}%`
105
105
  : "收益: --";
106
106
  const sharpeStr =
107
107
  typeof perf.sharpeRatio === "number"
@@ -109,7 +109,7 @@ export function registerStrategyCli(params: {
109
109
  : "夏普: --";
110
110
  const ddStr =
111
111
  typeof perf.maxDrawdown === "number"
112
- ? `回撤: ${(perf.maxDrawdown * 100).toFixed(1)}%`
112
+ ? `回撤: ${perf.maxDrawdown.toFixed(1)}%`
113
113
  : "回撤: --";
114
114
  const author = s.author?.displayName || "未知";
115
115
 
@@ -124,9 +124,9 @@ export function registerStrategyCli(params: {
124
124
  console.log("");
125
125
  console.log("使用 openclaw strategy show <id> --remote 查看详情");
126
126
  console.log("使用 openclaw strategy fork <id> 下载策略(需要 API Key)");
127
- logCli(db, "leaderboard", { boardType, limit, offset }, startMs);
127
+ logCli(getDb, "leaderboard", { boardType, limit, offset }, startMs);
128
128
  } catch (err) {
129
- logCli(db, "leaderboard", { boardType, limit, offset }, startMs, String(err));
129
+ logCli(getDb, "leaderboard", { boardType, limit, offset }, startMs, String(err));
130
130
  console.error(`✗ 请求失败: ${err instanceof Error ? err.message : String(err)}`);
131
131
  process.exitCode = 1;
132
132
  }
@@ -149,7 +149,7 @@ export function registerStrategyCli(params: {
149
149
  });
150
150
 
151
151
  if (result.success) {
152
- logCli(db, "fork", { strategyId }, startMs);
152
+ logCli(getDb, "fork", { strategyId }, startMs);
153
153
  console.log("✓ 策略 Fork 成功!");
154
154
  console.log("");
155
155
  console.log(` 名称: ${result.sourceName}`);
@@ -160,7 +160,7 @@ export function registerStrategyCli(params: {
160
160
  console.log(` 验证: openfinclaw strategy validate ${result.localPath}`);
161
161
  console.log(` 发布: openfinclaw strategy publish ${result.localPath}`);
162
162
  } else {
163
- logCli(db, "fork", { strategyId }, startMs, result.error ?? "unknown");
163
+ logCli(getDb, "fork", { strategyId }, startMs, result.error ?? "unknown");
164
164
  console.error(`✗ Fork 失败: ${result.error}`);
165
165
  process.exitCode = 1;
166
166
  }
@@ -202,7 +202,7 @@ export function registerStrategyCli(params: {
202
202
  s.displayName.length > 20 ? s.displayName.slice(0, 17) + "..." : s.displayName;
203
203
  console.log(` ${name.padEnd(40)} ${displayName.padEnd(20)} ${typeLabel}`);
204
204
  }
205
- logCli(db, "list", { count: strategies.length }, startMs);
205
+ logCli(getDb, "list", { count: strategies.length }, startMs);
206
206
  });
207
207
 
208
208
  // ── strategy show ──
@@ -216,7 +216,7 @@ export function registerStrategyCli(params: {
216
216
  const local = await findLocalStrategy(nameOrId);
217
217
 
218
218
  if (!local && !options.remote) {
219
- logCli(db, "show", { nameOrId }, startMs, "not found");
219
+ logCli(getDb, "show", { nameOrId }, startMs, "not found");
220
220
  console.error(`✗ 本地策略未找到: ${nameOrId}`);
221
221
  console.error(" 使用 --remote 从 Hub 获取信息");
222
222
  process.exitCode = 1;
@@ -227,7 +227,7 @@ export function registerStrategyCli(params: {
227
227
  const infoResult = await fetchStrategyInfo(config, local.sourceId);
228
228
  if (infoResult.success && infoResult.data) {
229
229
  const info = infoResult.data;
230
- logCli(db, "show", { nameOrId, remote: true }, startMs);
230
+ logCli(getDb, "show", { nameOrId, remote: true }, startMs);
231
231
  if (options.json) {
232
232
  console.log(JSON.stringify({ local, hub: info }, null, 2));
233
233
  return;
@@ -238,7 +238,7 @@ export function registerStrategyCli(params: {
238
238
  }
239
239
 
240
240
  if (local) {
241
- logCli(db, "show", { nameOrId }, startMs);
241
+ logCli(getDb, "show", { nameOrId }, startMs);
242
242
  if (options.json) {
243
243
  console.log(JSON.stringify(local, null, 2));
244
244
  return;
@@ -247,7 +247,7 @@ export function registerStrategyCli(params: {
247
247
  return;
248
248
  }
249
249
 
250
- logCli(db, "show", { nameOrId }, startMs, "not found");
250
+ logCli(getDb, "show", { nameOrId }, startMs, "not found");
251
251
  console.error(`✗ 策略未找到: ${nameOrId}`);
252
252
  process.exitCode = 1;
253
253
  });
@@ -262,7 +262,7 @@ export function registerStrategyCli(params: {
262
262
  const startMs = Date.now();
263
263
  const local = await findLocalStrategy(nameOrId);
264
264
  if (!local) {
265
- logCli(db, "remove", { nameOrId }, startMs, "not found");
265
+ logCli(getDb, "remove", { nameOrId }, startMs, "not found");
266
266
  console.error(`✗ 策略未找到: ${nameOrId}`);
267
267
  process.exitCode = 1;
268
268
  return;
@@ -278,10 +278,10 @@ export function registerStrategyCli(params: {
278
278
 
279
279
  const result = await removeLocalStrategy(nameOrId);
280
280
  if (result.success) {
281
- logCli(db, "remove", { nameOrId }, startMs);
281
+ logCli(getDb, "remove", { nameOrId }, startMs);
282
282
  console.log("✓ 策略已删除");
283
283
  } else {
284
- logCli(db, "remove", { nameOrId }, startMs, result.error ?? "unknown");
284
+ logCli(getDb, "remove", { nameOrId }, startMs, result.error ?? "unknown");
285
285
  console.error(`✗ 删除失败: ${result.error}`);
286
286
  process.exitCode = 1;
287
287
  }
package/src/config.ts CHANGED
@@ -3,12 +3,20 @@
3
3
  * Supports single API key for both Hub and DataHub services.
4
4
  */
5
5
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
6
- import type { UnifiedPluginConfig } from "./types.js";
6
+ import type { NewsProviderType, UnifiedPluginConfig } from "./types.js";
7
7
 
8
8
  const DEFAULT_HUB_API_URL = "https://hub.openfinclaw.ai";
9
9
  const DEFAULT_DATAHUB_GATEWAY_URL = "http://43.134.61.136:9080";
10
10
  const DEFAULT_TIMEOUT_MS = 60_000;
11
11
 
12
+ const DEFAULT_SCAN_CRON = "0 8 * * *";
13
+ const DEFAULT_MONITOR_CRON = "*/30 * * * *";
14
+ const DEFAULT_WEEKLY_CRON = "0 20 * * 0";
15
+ const DEFAULT_MONTHLY_CRON = "0 20 1 * *";
16
+ const DEFAULT_SCAN_TZ = "Asia/Shanghai";
17
+ const DEFAULT_ALERT_THRESHOLD = 5;
18
+ const VALID_NEWS_PROVIDERS = new Set<NewsProviderType>(["coingecko", "finnhub", "newsapi"]);
19
+
12
20
  function readEnv(keys: string[]): string | undefined {
13
21
  for (const key of keys) {
14
22
  const value = process.env[key]?.trim();
@@ -55,11 +63,67 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
55
63
  ? Math.floor(httpPortNum)
56
64
  : 18792;
57
65
 
66
+ // ── Scheduler config ──
67
+ const schedulerEnabled =
68
+ (raw?.schedulerEnabled ?? readEnv(["OPENFINCLAW_SCHEDULER_ENABLED"])) !== "false";
69
+
70
+ const scanCronExpr =
71
+ (typeof raw?.scanCronExpr === "string" ? raw.scanCronExpr : undefined) ??
72
+ readEnv(["OPENFINCLAW_SCAN_CRON"]) ??
73
+ DEFAULT_SCAN_CRON;
74
+
75
+ const monitorCronExpr =
76
+ (typeof raw?.monitorCronExpr === "string" ? raw.monitorCronExpr : undefined) ??
77
+ readEnv(["OPENFINCLAW_MONITOR_CRON"]) ??
78
+ DEFAULT_MONITOR_CRON;
79
+
80
+ const weeklyReportCronExpr =
81
+ (typeof raw?.weeklyReportCronExpr === "string" ? raw.weeklyReportCronExpr : undefined) ??
82
+ readEnv(["OPENFINCLAW_WEEKLY_CRON"]) ??
83
+ DEFAULT_WEEKLY_CRON;
84
+
85
+ const monthlyReportCronExpr =
86
+ (typeof raw?.monthlyReportCronExpr === "string" ? raw.monthlyReportCronExpr : undefined) ??
87
+ readEnv(["OPENFINCLAW_MONTHLY_CRON"]) ??
88
+ DEFAULT_MONTHLY_CRON;
89
+
90
+ const scanTimezone =
91
+ (typeof raw?.scanTimezone === "string" ? raw.scanTimezone : undefined) ??
92
+ readEnv(["OPENFINCLAW_SCAN_TZ"]) ??
93
+ DEFAULT_SCAN_TZ;
94
+
95
+ // ── News config ──
96
+ const newsApiKey =
97
+ (typeof raw?.newsApiKey === "string" ? raw.newsApiKey : undefined) ??
98
+ readEnv(["OPENFINCLAW_NEWS_API_KEY"]);
99
+
100
+ const newsProviderRaw =
101
+ (typeof raw?.newsProvider === "string" ? raw.newsProvider : undefined) ??
102
+ readEnv(["OPENFINCLAW_NEWS_PROVIDER"]) ??
103
+ "coingecko";
104
+ const newsProvider: NewsProviderType = VALID_NEWS_PROVIDERS.has(newsProviderRaw as NewsProviderType)
105
+ ? (newsProviderRaw as NewsProviderType)
106
+ : "coingecko";
107
+
108
+ // ── Alert config ──
109
+ const alertThresholdRaw = raw?.priceAlertThreshold ?? readEnv(["OPENFINCLAW_ALERT_THRESHOLD"]);
110
+ const priceAlertThreshold =
111
+ Number(alertThresholdRaw) > 0 ? Number(alertThresholdRaw) : DEFAULT_ALERT_THRESHOLD;
112
+
58
113
  return {
59
114
  apiKey: apiKey && apiKey.length > 0 ? apiKey : undefined,
60
115
  hubApiUrl: hubApiUrl.replace(/\/$/, ""),
61
116
  datahubGatewayUrl: datahubGatewayUrl.replace(/\/+$/, ""),
62
117
  requestTimeoutMs,
63
118
  httpPort,
119
+ schedulerEnabled,
120
+ scanCronExpr,
121
+ monitorCronExpr,
122
+ weeklyReportCronExpr,
123
+ monthlyReportCronExpr,
124
+ scanTimezone,
125
+ newsApiKey: newsApiKey && newsApiKey.length > 0 ? newsApiKey : undefined,
126
+ newsProvider,
127
+ priceAlertThreshold,
64
128
  };
65
129
  }
@@ -1,8 +1,8 @@
1
- import type { DatabaseSync } from "node:sqlite";
2
1
  /**
3
2
  * DataHub market data tools registration.
4
3
  * Tools: fin_price, fin_kline, fin_crypto, fin_compare, fin_slim_search
5
4
  */
5
+ import type { DatabaseSync } from "node:sqlite";
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
8
8
  import { withLogging } from "../middleware/with-logging.js";
@@ -31,12 +31,12 @@ const NO_KEY =
31
31
 
32
32
  /**
33
33
  * Register DataHub market data tools.
34
- * @param db - SQLite database for activity logging.
34
+ * @param getDb - Lazy database getter (called at execution time, not registration).
35
35
  */
36
36
  export function registerDatahubTools(
37
37
  api: OpenClawPluginApi,
38
38
  config: UnifiedPluginConfig,
39
- db: DatabaseSync,
39
+ getDb: () => DatabaseSync,
40
40
  ): void {
41
41
  const datahubClient = config.apiKey
42
42
  ? new DataHubClient(config.datahubGatewayUrl, config.apiKey, config.requestTimeoutMs)
@@ -91,7 +91,7 @@ export function registerDatahubTools(
91
91
  }),
92
92
  ),
93
93
  }),
94
- execute: withLogging(db, "fin_price", "market-data", async (_id, params) => {
94
+ execute: withLogging(getDb, "fin_price", "market-data", async (_id, params) => {
95
95
  try {
96
96
  if (!datahubClient) return json({ error: NO_KEY });
97
97
  const symbol = String(params.symbol);
@@ -135,7 +135,7 @@ export function registerDatahubTools(
135
135
  Type.Number({ description: "Number of bars to return (default: 30)" }),
136
136
  ),
137
137
  }),
138
- execute: withLogging(db, "fin_kline", "market-data", async (_id, params) => {
138
+ execute: withLogging(getDb, "fin_kline", "market-data", async (_id, params) => {
139
139
  try {
140
140
  if (!datahubClient) return json({ error: NO_KEY });
141
141
  const symbol = String(params.symbol);
@@ -209,7 +209,7 @@ export function registerDatahubTools(
209
209
  end_date: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })),
210
210
  limit: Type.Optional(Type.Number({ description: "Max results (default: 20)" })),
211
211
  }),
212
- execute: withLogging(db, "fin_crypto", "market-data", async (_id, params) => {
212
+ execute: withLogging(getDb, "fin_crypto", "market-data", async (_id, params) => {
213
213
  try {
214
214
  if (!datahubClient) return json({ error: NO_KEY });
215
215
  const endpoint = String(params.endpoint ?? "coin/market");
@@ -256,7 +256,7 @@ export function registerDatahubTools(
256
256
  description: "Comma-separated symbols (2-5). Example: BTC/USDT,ETH/USDT,600519.SH",
257
257
  }),
258
258
  }),
259
- execute: withLogging(db, "fin_compare", "market-data", async (_id, params) => {
259
+ execute: withLogging(getDb, "fin_compare", "market-data", async (_id, params) => {
260
260
  try {
261
261
  if (!datahubClient) return json({ error: NO_KEY });
262
262
  const raw = String(params.symbols);
@@ -279,7 +279,7 @@ export function registerDatahubTools(
279
279
  symbol: sym,
280
280
  market,
281
281
  price: ticker.last,
282
- weekChange: Math.round(weekChange * 100) / 100,
282
+ weekChange: parseFloat(weekChange.toFixed(2)),
283
283
  };
284
284
  }),
285
285
  );
@@ -317,7 +317,7 @@ export function registerDatahubTools(
317
317
  }),
318
318
  ),
319
319
  }),
320
- execute: withLogging(db, "fin_slim_search", "market-data", async (_id, params) => {
320
+ execute: withLogging(getDb, "fin_slim_search", "market-data", async (_id, params) => {
321
321
  try {
322
322
  if (!datahubClient) return json({ error: NO_KEY });
323
323
  const q = String(params.query);
package/src/db/db.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { mkdirSync, unlinkSync } from "node:fs";
2
2
  /**
3
3
  * SQLite database singleton for OpenFinClaw plugin.
4
4
  * Database path: ~/.openfinclaw/workspace/openfinclaw-plugin.db
@@ -9,15 +9,11 @@ import { mkdirSync } from "node:fs";
9
9
  * a second DatabaseSync connection while the old HTTP server still
10
10
  * holds a reference to the first one via the old module scope.
11
11
  */
12
- import { createRequire } from "node:module";
13
12
  import { homedir } from "node:os";
14
13
  import { join } from "node:path";
15
- import type { DatabaseSync } from "node:sqlite";
14
+ import { DatabaseSync } from "node:sqlite";
16
15
  import { ensureSchema } from "./schema.js";
17
16
 
18
- // Use createRequire to load the built-in node:sqlite in an ESM context.
19
- const _require = createRequire(import.meta.url);
20
-
21
17
  /** globalThis key — survives module hot-reloads within the same process. */
22
18
  const GLOBAL_DB_KEY = "__openfinclaw_db__" as const;
23
19
 
@@ -40,10 +36,21 @@ export function getDb(): DatabaseSync {
40
36
  if (existing) return existing;
41
37
 
42
38
  // node:sqlite is available in Node 22+
43
- const { DatabaseSync } = _require("node:sqlite") as typeof import("node:sqlite");
44
39
  const dbPath = resolveDbPath();
45
- const db = new DatabaseSync(dbPath);
46
- ensureSchema(db);
40
+ let db: DatabaseSync;
41
+ try {
42
+ db = new DatabaseSync(dbPath);
43
+ ensureSchema(db);
44
+ } catch (err) {
45
+ // If the file is corrupted ("file is not a database"), delete and retry once.
46
+ try {
47
+ unlinkSync(dbPath);
48
+ } catch {
49
+ // File may already be gone — ignore.
50
+ }
51
+ db = new DatabaseSync(dbPath);
52
+ ensureSchema(db);
53
+ }
47
54
  (globalThis as Record<string, unknown>)[GLOBAL_DB_KEY] = db;
48
55
  return db;
49
56
  }