@openfinclaw/openfinclaw-strategy 2026.3.310 → 2026.4.9

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
@@ -12,7 +12,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
12
12
  import { registerStrategyCli } from "./src/cli.js";
13
13
  import { resolvePluginConfig } from "./src/config.js";
14
14
  import { registerDatahubTools } from "./src/datahub/tools.js";
15
- import { getDb } from "./src/db/db.js";
15
+ import { closeDb, 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";
@@ -82,6 +82,11 @@ export default definePluginEntry({
82
82
  prependSystemContext: `${OPENFINCLAW_AGENT_GUIDANCE}\n\n${tournamentPrompt}`,
83
83
  }));
84
84
 
85
+ // Graceful shutdown: close SQLite connection
86
+ process.once("beforeExit", () => {
87
+ closeDb();
88
+ });
89
+
85
90
  // ── Gateway Cron registration ──
86
91
  // Write cron jobs directly to ~/.openclaw/cron/jobs.json during register().
87
92
  // This ensures jobs are available immediately on both gateway startup AND
@@ -90,9 +95,9 @@ export default definePluginEntry({
90
95
  if (config.schedulerEnabled) {
91
96
  setupOpenfinclawCronJobs(config)
92
97
  .then((result) => {
93
- if (result.created > 0) {
98
+ if (result.created > 0 || result.migrated > 0) {
94
99
  api.logger.info(
95
- `[OpenFinClaw] Cron jobs registered: ${result.created} created, ${result.existing} existing`,
100
+ `[OpenFinClaw] Cron jobs: ${result.created} created, ${result.migrated} migrated, ${result.existing} existing`,
96
101
  );
97
102
  }
98
103
  })
@@ -23,7 +23,7 @@
23
23
  "datahubGatewayUrl": {
24
24
  "type": "string",
25
25
  "description": "DataHub Gateway URL for market data",
26
- "default": "http://43.134.61.136:9080"
26
+ "default": "https://datahub.openfinclaw.ai"
27
27
  },
28
28
  "requestTimeoutMs": {
29
29
  "type": "number",
@@ -69,6 +69,16 @@
69
69
  "default": "Asia/Shanghai",
70
70
  "description": "Timezone for scheduled tasks"
71
71
  },
72
+ "cronSessionTarget": {
73
+ "type": "string",
74
+ "default": "isolated",
75
+ "description": "Cron job session mode: 'isolated' (own session, won't block main chat) or 'main' (legacy, runs in main conversation)"
76
+ },
77
+ "cronDeliveryMode": {
78
+ "type": "string",
79
+ "default": "announce",
80
+ "description": "Cron result delivery: 'announce' (push to user's last active channel) or 'none' (silent)"
81
+ },
72
82
  "newsApiKey": {
73
83
  "type": "string",
74
84
  "description": "Optional API key for Finnhub or NewsAPI news provider",
@@ -101,7 +111,7 @@
101
111
  },
102
112
  "datahubGatewayUrl": {
103
113
  "label": "DataHub Gateway URL",
104
- "placeholder": "http://43.134.61.136:9080"
114
+ "placeholder": "https://datahub.openfinclaw.ai"
105
115
  }
106
116
  }
107
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfinclaw/openfinclaw-strategy",
3
- "version": "2026.3.310",
3
+ "version": "2026.4.9",
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/config.ts CHANGED
@@ -6,7 +6,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
6
6
  import type { NewsProviderType, UnifiedPluginConfig } from "./types.js";
7
7
 
8
8
  const DEFAULT_HUB_API_URL = "https://hub.openfinclaw.ai";
9
- const DEFAULT_DATAHUB_GATEWAY_URL = "http://43.134.61.136:9080";
9
+ const DEFAULT_DATAHUB_GATEWAY_URL = "https://datahub.openfinclaw.ai";
10
10
  const DEFAULT_TIMEOUT_MS = 60_000;
11
11
 
12
12
  const DEFAULT_SCAN_CRON = "0 8 * * *";
@@ -92,6 +92,29 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
92
92
  readEnv(["OPENFINCLAW_SCAN_TZ"]) ??
93
93
  DEFAULT_SCAN_TZ;
94
94
 
95
+ const cronAgentIdRaw =
96
+ (typeof raw?.cronAgentId === "string" ? raw.cronAgentId : undefined) ??
97
+ readEnv(["OPENFINCLAW_CRON_AGENT_ID"]);
98
+ const cronAgentId = cronAgentIdRaw?.trim() || undefined;
99
+
100
+ const VALID_SESSION_TARGETS = new Set(["isolated", "main"]);
101
+ const cronSessionTargetRaw =
102
+ (typeof raw?.cronSessionTarget === "string" ? raw.cronSessionTarget : undefined) ??
103
+ readEnv(["OPENFINCLAW_CRON_SESSION_TARGET"]) ??
104
+ "isolated";
105
+ const cronSessionTarget = VALID_SESSION_TARGETS.has(cronSessionTargetRaw)
106
+ ? cronSessionTargetRaw
107
+ : "isolated";
108
+
109
+ const VALID_DELIVERY_MODES = new Set(["announce", "none"]);
110
+ const cronDeliveryModeRaw =
111
+ (typeof raw?.cronDeliveryMode === "string" ? raw.cronDeliveryMode : undefined) ??
112
+ readEnv(["OPENFINCLAW_CRON_DELIVERY_MODE"]) ??
113
+ "announce";
114
+ const cronDeliveryMode = VALID_DELIVERY_MODES.has(cronDeliveryModeRaw)
115
+ ? cronDeliveryModeRaw
116
+ : "announce";
117
+
95
118
  // ── News config ──
96
119
  const newsApiKey =
97
120
  (typeof raw?.newsApiKey === "string" ? raw.newsApiKey : undefined) ??
@@ -124,6 +147,9 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
124
147
  weeklyReportCronExpr,
125
148
  monthlyReportCronExpr,
126
149
  scanTimezone,
150
+ cronAgentId,
151
+ cronSessionTarget,
152
+ cronDeliveryMode,
127
153
  newsApiKey: newsApiKey && newsApiKey.length > 0 ? newsApiKey : undefined,
128
154
  newsProvider,
129
155
  priceAlertThreshold,
package/src/db/db.ts CHANGED
@@ -12,7 +12,7 @@ import { mkdirSync, unlinkSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { DatabaseSync } from "node:sqlite";
15
- import { ensureSchema } from "./schema.js";
15
+ import { cleanupOldRows, ensureSchema } from "./schema.js";
16
16
 
17
17
  /** globalThis key — survives module hot-reloads within the same process. */
18
18
  const GLOBAL_DB_KEY = "__openfinclaw_db__" as const;
@@ -52,6 +52,8 @@ export function getDb(): DatabaseSync {
52
52
  ensureSchema(db);
53
53
  }
54
54
  (globalThis as Record<string, unknown>)[GLOBAL_DB_KEY] = db;
55
+ // Best-effort cleanup of old rows on first access each process lifecycle
56
+ cleanupOldRows(db);
55
57
  return db;
56
58
  }
57
59
 
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Data access helpers for OpenFinClaw plugin SQLite tables.
3
3
  */
4
- import type { DatabaseSync } from "node:sqlite";
4
+ import type { DatabaseSync, SQLInputValue } from "node:sqlite";
5
+
6
+ /** Cast unknown[] to SQLInputValue[] for db.prepare().run() spread. */
7
+ function sqlParams(values: unknown[]): SQLInputValue[] {
8
+ return values as SQLInputValue[];
9
+ }
5
10
 
6
11
  // ── Row types ─────────────────────────────────────────────────────────────
7
12
 
@@ -262,7 +267,7 @@ export function updateBacktestResult(
262
267
  }
263
268
  if (sets.length === 0) return;
264
269
  values.push(id);
265
- db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...values);
270
+ db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams(values));
266
271
  } catch {
267
272
  // Logging must never crash the calling tool
268
273
  }
@@ -347,7 +352,7 @@ export function updateScanHistory(
347
352
  }
348
353
  if (sets.length === 0) return;
349
354
  values.push(id);
350
- db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...values);
355
+ db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams(values));
351
356
  } catch {
352
357
  // Logging must never crash the calling tool
353
358
  }
@@ -378,7 +383,7 @@ export function queryScanHistory(
378
383
  params.push(limit, offset);
379
384
  return db
380
385
  .prepare(`SELECT * FROM scan_history ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`)
381
- .all(...params) as ScanHistoryEntry[];
386
+ .all(...sqlParams(params)) as unknown as ScanHistoryEntry[];
382
387
  }
383
388
 
384
389
  /**
@@ -462,7 +467,7 @@ export function queryPriceAlerts(
462
467
  params.push(limit, offset);
463
468
  return db
464
469
  .prepare(`SELECT * FROM price_alerts ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
465
- .all(...params) as PriceAlertEntry[];
470
+ .all(...sqlParams(params)) as unknown as PriceAlertEntry[];
466
471
  }
467
472
 
468
473
  /** Count price_alerts rows since an inclusive time bound (ISO 8601 created_at). */
@@ -488,19 +493,19 @@ export function acknowledgePriceAlert(db: DatabaseSync, id: string): void {
488
493
  export function queryActivityLog(db: DatabaseSync, limit = 50, offset = 0): ActivityLogEntry[] {
489
494
  return db
490
495
  .prepare(`SELECT * FROM agent_activity_log ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
491
- .all(limit, offset) as ActivityLogEntry[];
496
+ .all(limit, offset) as unknown as ActivityLogEntry[];
492
497
  }
493
498
 
494
499
  /** Query agent_events, newest first. */
495
500
  export function queryAgentEvents(db: DatabaseSync, limit = 50, offset = 0): AgentEventEntry[] {
496
501
  return db
497
502
  .prepare(`SELECT * FROM agent_events ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
498
- .all(limit, offset) as AgentEventEntry[];
503
+ .all(limit, offset) as unknown as AgentEventEntry[];
499
504
  }
500
505
 
501
506
  /** Query all strategies, newest first. */
502
507
  export function queryStrategies(db: DatabaseSync): StrategyRow[] {
503
- return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as StrategyRow[];
508
+ return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as unknown as StrategyRow[];
504
509
  }
505
510
 
506
511
  /** Query backtest results, optionally filtered by strategy_id. */
@@ -508,9 +513,9 @@ export function queryBacktestResults(db: DatabaseSync, strategyId?: string): Bac
508
513
  if (strategyId) {
509
514
  return db
510
515
  .prepare(`SELECT * FROM backtest_results WHERE strategy_id = ? ORDER BY created_at DESC`)
511
- .all(strategyId) as BacktestRow[];
516
+ .all(strategyId) as unknown as BacktestRow[];
512
517
  }
513
518
  return db
514
519
  .prepare(`SELECT * FROM backtest_results ORDER BY created_at DESC`)
515
- .all() as BacktestRow[];
520
+ .all() as unknown as BacktestRow[];
516
521
  }
package/src/db/schema.ts CHANGED
@@ -141,6 +141,23 @@ export function ensureSchema(db: DatabaseSync): void {
141
141
  // ── migrations (add columns to existing databases) ──────────────────────
142
142
  }
143
143
 
144
+ /**
145
+ * Purge old rows from accumulating tables.
146
+ * Safe to call periodically (e.g. on plugin register or via cron).
147
+ * Keeps 90 days of activity logs, scan history, and acknowledged price alerts.
148
+ */
149
+ export function cleanupOldRows(db: DatabaseSync): void {
150
+ const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
151
+ try {
152
+ db.exec(`DELETE FROM agent_activity_log WHERE timestamp < '${cutoff}'`);
153
+ db.exec(`DELETE FROM scan_history WHERE started_at < '${cutoff}'`);
154
+ db.exec(`DELETE FROM price_alerts WHERE acknowledged = 1 AND created_at < '${cutoff}'`);
155
+ db.exec(`DELETE FROM agent_events WHERE timestamp < '${cutoff}'`);
156
+ } catch {
157
+ // Cleanup is best-effort — never crash the plugin
158
+ }
159
+ }
160
+
144
161
  /** Add a column if it doesn't already exist (safe for repeated calls). */
145
162
  function ensureColumn(db: DatabaseSync, table: string, column: string, definition: string): void {
146
163
  const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
@@ -8,7 +8,9 @@
8
8
  * The running CronService watches the store file and picks up new jobs
9
9
  * on its next tick.
10
10
  *
11
- * Idempotent: safe to call multiple times (skips existing jobs by name).
11
+ * Idempotent: creates missing jobs and migrates legacy "main" session jobs
12
+ * to "isolated" so cron tasks run in their own session without blocking the
13
+ * user's active conversation.
12
14
  *
13
15
  * Jobs:
14
16
  * - openfinclaw:daily-scan (0 8 * * *) 每日策略扫描
@@ -23,16 +25,21 @@ import type { UnifiedPluginConfig } from "../types.js";
23
25
 
24
26
  // ── Cron store types (mirrors src/cron/types.ts subset) ──────────────────
25
27
 
28
+ type CronPayload =
29
+ | { kind: "systemEvent"; text: string }
30
+ | { kind: "agentTurn"; message: string };
31
+
26
32
  /** Minimal stored cron job shape. */
27
33
  interface StoredCronJob {
28
34
  id: string;
29
35
  name: string;
36
+ agentId?: string;
30
37
  enabled: boolean;
31
38
  schedule: { kind: "cron"; expr: string; tz?: string };
32
- payload: { kind: "systemEvent"; text: string };
39
+ payload: CronPayload;
33
40
  sessionTarget: string;
34
41
  wakeMode: string;
35
- delivery: { mode: string };
42
+ delivery: { mode: string; channel?: string; to?: string };
36
43
  createdAtMs: number;
37
44
  updatedAtMs: number;
38
45
  state: Record<string, unknown>;
@@ -43,6 +50,14 @@ interface CronStoreFile {
43
50
  jobs: StoredCronJob[];
44
51
  }
45
52
 
53
+ /** Names managed by this plugin — used for migration detection. */
54
+ const OPENFINCLAW_JOB_NAMES = new Set([
55
+ "openfinclaw:daily-scan",
56
+ "openfinclaw:price-monitor",
57
+ "openfinclaw:weekly-report",
58
+ "openfinclaw:monthly-report",
59
+ ]);
60
+
46
61
  // ── File I/O ──────────────────────────────────────────────────────────────
47
62
 
48
63
  /** Resolve default cron store path: ~/.openclaw/cron/jobs.json */
@@ -82,15 +97,26 @@ async function saveStore(storePath: string, store: CronStoreFile): Promise<void>
82
97
  function buildCronJobDefs(config: UnifiedPluginConfig): Array<{
83
98
  name: string;
84
99
  schedule: { kind: "cron"; expr: string; tz?: string };
85
- payload: { kind: "systemEvent"; text: string };
100
+ payload: CronPayload;
101
+ sessionTarget: string;
102
+ delivery: { mode: string };
86
103
  }> {
104
+ const sessionTarget = config.cronSessionTarget;
105
+ const delivery = { mode: config.cronDeliveryMode };
106
+ const useAgentTurn = sessionTarget !== "main";
107
+
108
+ function makePayload(text: string): CronPayload {
109
+ return useAgentTurn ? { kind: "agentTurn", message: text } : { kind: "systemEvent", text };
110
+ }
111
+
87
112
  return [
88
113
  {
89
114
  name: "openfinclaw:daily-scan",
90
115
  schedule: { kind: "cron", expr: config.scanCronExpr, tz: config.scanTimezone },
91
- payload: {
92
- kind: "systemEvent",
93
- text: [
116
+ sessionTarget,
117
+ delivery,
118
+ payload: makePayload(
119
+ [
94
120
  "[openfinclaw-strategy 每日扫描]",
95
121
  "1. 调用 strategy_daily_scan 获取策略扫描报告",
96
122
  "2. 分析每条新闻对策略的影响(利好/利空/中性)",
@@ -98,56 +124,94 @@ function buildCronJobDefs(config: UnifiedPluginConfig): Array<{
98
124
  "4. 用 skill_publish_verify 确认回测完成",
99
125
  "5. 将分析报告和建议操作发送给用户",
100
126
  ].join("\n"),
101
- },
127
+ ),
102
128
  },
103
129
  {
104
130
  name: "openfinclaw:price-monitor",
105
131
  schedule: { kind: "cron", expr: config.monitorCronExpr, tz: config.scanTimezone },
106
- payload: {
107
- kind: "systemEvent",
108
- text: [
132
+ sessionTarget,
133
+ delivery,
134
+ payload: makePayload(
135
+ [
109
136
  "[openfinclaw-strategy 价格监控]",
110
137
  "调用 strategy_price_monitor 检查所有策略标的的价格异动。",
111
138
  "如有告警(涨跌幅超阈值),分析可能的原因并通知用户。",
112
139
  ].join("\n"),
113
- },
140
+ ),
114
141
  },
115
142
  {
116
143
  name: "openfinclaw:weekly-report",
117
144
  schedule: { kind: "cron", expr: config.weeklyReportCronExpr, tz: config.scanTimezone },
118
- payload: {
119
- kind: "systemEvent",
120
- text: [
145
+ sessionTarget,
146
+ delivery,
147
+ payload: makePayload(
148
+ [
121
149
  "[openfinclaw-strategy 周报]",
122
150
  '调用 strategy_periodic_report(period="weekly") 生成策略绩效周报。',
123
151
  "汇总本周回测结果、价格告警和扫描记录,发送给用户。",
124
152
  ].join("\n"),
125
- },
153
+ ),
126
154
  },
127
155
  {
128
156
  name: "openfinclaw:monthly-report",
129
157
  schedule: { kind: "cron", expr: config.monthlyReportCronExpr, tz: config.scanTimezone },
130
- payload: {
131
- kind: "systemEvent",
132
- text: [
158
+ sessionTarget,
159
+ delivery,
160
+ payload: makePayload(
161
+ [
133
162
  "[openfinclaw-strategy 月报]",
134
163
  '调用 strategy_periodic_report(period="monthly") 生成策略绩效月报。',
135
164
  "汇总本月回测结果、价格告警和扫描记录,发送给用户。",
136
165
  ].join("\n"),
137
- },
166
+ ),
138
167
  },
139
168
  ];
140
169
  }
141
170
 
171
+ // ── Migration ─────────────────────────────────────────────────────────────
172
+
173
+ /**
174
+ * Migrate legacy openfinclaw jobs that still use sessionTarget "main" +
175
+ * systemEvent payload to the new isolated agentTurn pattern.
176
+ * Returns the number of jobs migrated.
177
+ */
178
+ function migrateLegacyJobs(
179
+ store: CronStoreFile,
180
+ defs: ReturnType<typeof buildCronJobDefs>,
181
+ ): number {
182
+ const defsByName = new Map(defs.map((d) => [d.name, d]));
183
+ let migrated = 0;
184
+
185
+ for (const job of store.jobs) {
186
+ if (!OPENFINCLAW_JOB_NAMES.has(job.name)) continue;
187
+ if (job.sessionTarget !== "main") continue;
188
+
189
+ const def = defsByName.get(job.name);
190
+ if (!def) continue;
191
+
192
+ job.payload = def.payload;
193
+ job.sessionTarget = def.sessionTarget;
194
+ job.delivery = def.delivery;
195
+ job.updatedAtMs = Date.now();
196
+ migrated++;
197
+ }
198
+
199
+ return migrated;
200
+ }
201
+
142
202
  // ── Public API ────────────────────────────────────────────────────────────
143
203
 
144
204
  /**
145
- * Idempotently register openfinclaw cron jobs by writing to the
205
+ * Register and migrate openfinclaw cron jobs by writing to the
146
206
  * Gateway cron store file. Safe to call at plugin startup.
207
+ *
208
+ * - Creates missing jobs with the current config (isolated session by default).
209
+ * - Migrates existing legacy "main" session jobs to "isolated" + "agentTurn"
210
+ * so cron tasks no longer block the user's active conversation.
147
211
  */
148
212
  export async function setupOpenfinclawCronJobs(
149
213
  config: UnifiedPluginConfig,
150
- ): Promise<{ ok: boolean; created: number; existing: number }> {
214
+ ): Promise<{ ok: boolean; created: number; existing: number; migrated: number }> {
151
215
  const storePath = defaultStorePath();
152
216
  const store = await loadStore(storePath);
153
217
  const existingNames = new Set(store.jobs.map((j) => j.name));
@@ -160,12 +224,13 @@ export async function setupOpenfinclawCronJobs(
160
224
  store.jobs.push({
161
225
  id: randomUUID(),
162
226
  name: def.name,
227
+ agentId: config.cronAgentId,
163
228
  enabled: true,
164
229
  schedule: def.schedule,
165
230
  payload: def.payload,
166
- sessionTarget: "main",
231
+ sessionTarget: def.sessionTarget,
167
232
  wakeMode: "now",
168
- delivery: { mode: "none" },
233
+ delivery: def.delivery,
169
234
  createdAtMs: now,
170
235
  updatedAtMs: now,
171
236
  state: {},
@@ -173,9 +238,11 @@ export async function setupOpenfinclawCronJobs(
173
238
  created++;
174
239
  }
175
240
 
176
- if (created > 0) {
241
+ const migrated = migrateLegacyJobs(store, defs);
242
+
243
+ if (created > 0 || migrated > 0) {
177
244
  await saveStore(storePath, store);
178
245
  }
179
246
 
180
- return { ok: true, created, existing: existingNames.size };
247
+ return { ok: true, created, existing: existingNames.size, migrated };
181
248
  }
package/src/types.ts CHANGED
@@ -552,6 +552,12 @@ export interface UnifiedPluginConfig {
552
552
  monthlyReportCronExpr: string;
553
553
  /** 调度时区 */
554
554
  scanTimezone: string;
555
+ /** 定时任务绑定的专用 agent id;未设置时沿用默认 agent */
556
+ cronAgentId: string | undefined;
557
+ /** Cron session 模式:"isolated"(独立 session,不阻塞主对话)或 "main"(旧行为) */
558
+ cronSessionTarget: string;
559
+ /** Cron 投递模式:"announce"(推送到用户最近活跃频道)或 "none" */
560
+ cronDeliveryMode: string;
555
561
 
556
562
  // ── 新闻源配置 ────────────────────────────────────────────
557
563
  /** 外部新闻 API Key(Finnhub 或 NewsAPI) */