@openfinclaw/openfinclaw-strategy 2026.3.275 → 2026.3.276

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
@@ -75,22 +75,24 @@ const openfinclawPlugin = {
75
75
  }));
76
76
 
77
77
  // ── Gateway Cron registration ──
78
- // Register cron jobs on gateway_start (writes to ~/.openclaw/cron/jobs.json)
78
+ // Write cron jobs directly to ~/.openclaw/cron/jobs.json during register().
79
+ // This ensures jobs are available immediately on both gateway startup AND
80
+ // hot-reload (plugin install without restart). The CronService picks up
81
+ // file changes on its next tick.
79
82
  if (config.schedulerEnabled) {
80
- api.on("gateway_start", async () => {
81
- try {
82
- const result = await setupOpenfinclawCronJobs(config);
83
+ setupOpenfinclawCronJobs(config)
84
+ .then((result) => {
83
85
  if (result.created > 0) {
84
86
  api.logger.info(
85
87
  `[OpenFinClaw] Cron jobs registered: ${result.created} created, ${result.existing} existing`,
86
88
  );
87
89
  }
88
- } catch (err) {
90
+ })
91
+ .catch((err) => {
89
92
  api.logger.info(
90
93
  `[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
91
94
  );
92
- }
93
- });
95
+ });
94
96
  }
95
97
  },
96
98
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfinclaw/openfinclaw-strategy",
3
- "version": "2026.3.275",
3
+ "version": "2026.3.276",
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
@@ -101,7 +101,9 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
101
101
  (typeof raw?.newsProvider === "string" ? raw.newsProvider : undefined) ??
102
102
  readEnv(["OPENFINCLAW_NEWS_PROVIDER"]) ??
103
103
  "coingecko";
104
- const newsProvider: NewsProviderType = VALID_NEWS_PROVIDERS.has(newsProviderRaw as NewsProviderType)
104
+ const newsProvider: NewsProviderType = VALID_NEWS_PROVIDERS.has(
105
+ newsProviderRaw as NewsProviderType,
106
+ )
105
107
  ? (newsProviderRaw as NewsProviderType)
106
108
  : "coingecko";
107
109
 
@@ -325,7 +325,18 @@ export function insertScanHistory(db: DatabaseSync, entry: ScanHistoryEntry): vo
325
325
  export function updateScanHistory(
326
326
  db: DatabaseSync,
327
327
  id: string,
328
- patch: Partial<Pick<ScanHistoryEntry, "completed_at" | "status" | "strategies_scanned" | "news_found" | "actions_taken" | "summary" | "detail_json">>,
328
+ patch: Partial<
329
+ Pick<
330
+ ScanHistoryEntry,
331
+ | "completed_at"
332
+ | "status"
333
+ | "strategies_scanned"
334
+ | "news_found"
335
+ | "actions_taken"
336
+ | "summary"
337
+ | "detail_json"
338
+ >
339
+ >,
329
340
  ): void {
330
341
  try {
331
342
  const sets: string[] = [];
@@ -350,7 +361,9 @@ export function queryScanHistory(
350
361
  const { limit = 20, offset = 0, scanType } = opts;
351
362
  if (scanType) {
352
363
  return db
353
- .prepare(`SELECT * FROM scan_history WHERE scan_type = ? ORDER BY started_at DESC LIMIT ? OFFSET ?`)
364
+ .prepare(
365
+ `SELECT * FROM scan_history WHERE scan_type = ? ORDER BY started_at DESC LIMIT ? OFFSET ?`,
366
+ )
354
367
  .all(scanType, limit, offset) as ScanHistoryEntry[];
355
368
  }
356
369
  return db
@@ -403,7 +416,9 @@ export function queryPriceAlerts(
403
416
  const { limit = 50, offset = 0, strategyId } = opts;
404
417
  if (strategyId) {
405
418
  return db
406
- .prepare(`SELECT * FROM price_alerts WHERE strategy_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`)
419
+ .prepare(
420
+ `SELECT * FROM price_alerts WHERE strategy_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`,
421
+ )
407
422
  .all(strategyId, limit, offset) as PriceAlertEntry[];
408
423
  }
409
424
  return db
@@ -1,3 +1,4 @@
1
+ import { guessMarket } from "../datahub/client.js";
1
2
  /**
2
3
  * External news providers for strategy symbol monitoring.
3
4
  *
@@ -10,7 +11,6 @@
10
11
  */
11
12
  import type { MarketType } from "../types.js";
12
13
  import type { NewsItem, NewsProvider, SymbolNewsResult } from "./types.js";
13
- import { guessMarket } from "../datahub/client.js";
14
14
 
15
15
  // ── CoinGecko Trending (no key required) ─────────────────────────────────
16
16
 
@@ -101,7 +101,9 @@ export function formatScanReportMarkdown(report: ScanReport): string {
101
101
  for (const sd of entry.symbolData) {
102
102
  if (sd.currentPrice != null) {
103
103
  const change =
104
- sd.priceChange24h != null ? ` (24h ${sd.priceChange24h >= 0 ? "+" : ""}${sd.priceChange24h.toFixed(1)}%)` : "";
104
+ sd.priceChange24h != null
105
+ ? ` (24h ${sd.priceChange24h >= 0 ? "+" : ""}${sd.priceChange24h.toFixed(1)}%)`
106
+ : "";
105
107
  lines.push(`- ${sd.symbol} 当前价格: $${sd.currentPrice.toFixed(2)}${change}`);
106
108
  }
107
109
 
@@ -127,7 +129,9 @@ export function formatScanReportMarkdown(report: ScanReport): string {
127
129
  if (strategiesWithNews.length > 0) {
128
130
  lines.push("### 需要关注的策略");
129
131
  for (const e of strategiesWithNews) {
130
- lines.push(`- ${e.strategyName}: ${e.significantNewsCount} 条相关新闻,建议分析影响并考虑是否需要优化`);
132
+ lines.push(
133
+ `- ${e.strategyName}: ${e.significantNewsCount} 条相关新闻,建议分析影响并考虑是否需要优化`,
134
+ );
131
135
  }
132
136
  } else {
133
137
  lines.push("### 汇总");
@@ -6,6 +6,7 @@ import { randomUUID } from "node:crypto";
6
6
  import type { DatabaseSync } from "node:sqlite";
7
7
  import { Type } from "@sinclair/typebox";
8
8
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
9
+ import { DataHubClient } from "../datahub/client.js";
9
10
  import {
10
11
  insertScanHistory,
11
12
  queryBacktestResults,
@@ -13,7 +14,6 @@ import {
13
14
  queryStrategies,
14
15
  updateScanHistory,
15
16
  } from "../db/repositories.js";
16
- import { DataHubClient } from "../datahub/client.js";
17
17
  import { withLogging } from "../middleware/with-logging.js";
18
18
  import type { UnifiedPluginConfig } from "../types.js";
19
19
  import type { AggregatedNewsProvider } from "./news-provider.js";
@@ -200,56 +200,61 @@ export function registerSchedulerTools(
200
200
  parameters: Type.Object({
201
201
  scanType: Type.Optional(
202
202
  Type.String({
203
- description:
204
- "Filter by type: daily_scan, price_monitor, weekly_report, monthly_report",
203
+ description: "Filter by type: daily_scan, price_monitor, weekly_report, monthly_report",
205
204
  }),
206
205
  ),
207
206
  limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
208
207
  }),
209
- execute: withLogging(getDb, "strategy_scan_history", "scheduler", async (_toolCallId, params) => {
210
- try {
211
- const db = getDb();
212
- const entries = queryScanHistory(db, {
213
- scanType: params.scanType ? String(params.scanType) : undefined,
214
- limit: Number(params.limit) || 10,
215
- });
208
+ execute: withLogging(
209
+ getDb,
210
+ "strategy_scan_history",
211
+ "scheduler",
212
+ async (_toolCallId, params) => {
213
+ try {
214
+ const db = getDb();
215
+ const entries = queryScanHistory(db, {
216
+ scanType: params.scanType ? String(params.scanType) : undefined,
217
+ limit: Number(params.limit) || 10,
218
+ });
216
219
 
217
- if (entries.length === 0) {
218
- return {
219
- content: [
220
- {
221
- type: "text" as const,
222
- text: "暂无扫描记录。调用 strategy_daily_scan 执行首次扫描。",
223
- },
224
- ],
225
- details: { success: true, entries: [] },
226
- };
227
- }
220
+ if (entries.length === 0) {
221
+ return {
222
+ content: [
223
+ {
224
+ type: "text" as const,
225
+ text: "暂无扫描记录。调用 strategy_daily_scan 执行首次扫描。",
226
+ },
227
+ ],
228
+ details: { success: true, entries: [] },
229
+ };
230
+ }
228
231
 
229
- const lines: string[] = [];
230
- lines.push(`## 扫描历史 (最近 ${entries.length} 条)`);
231
- lines.push("");
232
+ const lines: string[] = [];
233
+ lines.push(`## 扫描历史 (最近 ${entries.length} 条)`);
234
+ lines.push("");
232
235
 
233
- for (const e of entries) {
234
- const date = e.started_at.slice(0, 16).replace("T", " ");
235
- const status = e.status === "completed" ? "OK" : e.status === "failed" ? "FAIL" : "...";
236
- lines.push(
237
- `- [${status}] ${date} | ${e.scan_type} | 策略: ${e.strategies_scanned} | 新闻: ${e.news_found}`,
238
- );
239
- if (e.summary) lines.push(` ${e.summary}`);
240
- }
236
+ for (const e of entries) {
237
+ const date = e.started_at.slice(0, 16).replace("T", " ");
238
+ const status =
239
+ e.status === "completed" ? "OK" : e.status === "failed" ? "FAIL" : "...";
240
+ lines.push(
241
+ `- [${status}] ${date} | ${e.scan_type} | 策略: ${e.strategies_scanned} | 新闻: ${e.news_found}`,
242
+ );
243
+ if (e.summary) lines.push(` ${e.summary}`);
244
+ }
241
245
 
242
- return {
243
- content: [{ type: "text" as const, text: lines.join("\n") }],
244
- details: { success: true, entries },
245
- };
246
- } catch (err) {
247
- return json({
248
- success: false,
249
- error: err instanceof Error ? err.message : String(err),
250
- });
251
- }
252
- }),
246
+ return {
247
+ content: [{ type: "text" as const, text: lines.join("\n") }],
248
+ details: { success: true, entries },
249
+ };
250
+ } catch (err) {
251
+ return json({
252
+ success: false,
253
+ error: err instanceof Error ? err.message : String(err),
254
+ });
255
+ }
256
+ },
257
+ ),
253
258
  },
254
259
  { names: ["strategy_scan_history"] },
255
260
  );
@@ -204,122 +204,131 @@ export function registerStrategyTools(
204
204
  Type.String({ description: "Backtest task ID from skill_publish response" }),
205
205
  ),
206
206
  }),
207
- execute: withLogging(getDb, "skill_publish_verify", "strategy", async (_toolCallId, params) => {
208
- try {
209
- const submissionId = String(params.submissionId ?? "").trim() || undefined;
210
- const backtestTaskId = String(params.backtestTaskId ?? "").trim() || undefined;
207
+ execute: withLogging(
208
+ getDb,
209
+ "skill_publish_verify",
210
+ "strategy",
211
+ async (_toolCallId, params) => {
212
+ try {
213
+ const submissionId = String(params.submissionId ?? "").trim() || undefined;
214
+ const backtestTaskId = String(params.backtestTaskId ?? "").trim() || undefined;
211
215
 
212
- if (!submissionId && !backtestTaskId) {
213
- return json({
214
- success: false,
215
- error: "Either submissionId or backtestTaskId is required",
216
- });
217
- }
216
+ if (!submissionId && !backtestTaskId) {
217
+ return json({
218
+ success: false,
219
+ error: "Either submissionId or backtestTaskId is required",
220
+ });
221
+ }
218
222
 
219
- if (!config.apiKey) {
220
- return json({
221
- success: false,
222
- error: NO_API_KEY,
223
- });
224
- }
223
+ if (!config.apiKey) {
224
+ return json({
225
+ success: false,
226
+ error: NO_API_KEY,
227
+ });
228
+ }
225
229
 
226
- const searchParams: Record<string, string> = {};
227
- if (submissionId) searchParams.submissionId = submissionId;
228
- if (backtestTaskId) searchParams.backtestTaskId = backtestTaskId;
230
+ const searchParams: Record<string, string> = {};
231
+ if (submissionId) searchParams.submissionId = submissionId;
232
+ if (backtestTaskId) searchParams.backtestTaskId = backtestTaskId;
229
233
 
230
- const { status, data } = await hubApiRequest(config, "GET", "/skill/publish/verify", {
231
- searchParams,
232
- });
234
+ const { status, data } = await hubApiRequest(config, "GET", "/skill/publish/verify", {
235
+ searchParams,
236
+ });
233
237
 
234
- if (status >= 200 && status < 300) {
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 report = resp.backtestReport as Record<string, unknown> | undefined;
241
- const perf = report?.performance as Record<string, unknown> | undefined;
242
- updateBacktestResult(getDb(), backtestTaskId, {
243
- status: "completed",
244
- total_return: typeof perf?.totalReturn === "number" ? perf.totalReturn : undefined,
245
- sharpe: typeof perf?.sharpe === "number" ? perf.sharpe : undefined,
246
- sortino: typeof perf?.sortino === "number" ? perf.sortino : undefined,
247
- max_drawdown: typeof perf?.maxDrawdown === "number" ? perf.maxDrawdown : undefined,
248
- win_rate: typeof perf?.winRate === "number" ? perf.winRate : undefined,
249
- profit_factor:
250
- typeof perf?.profitFactor === "number" ? perf.profitFactor : undefined,
251
- total_trades: typeof perf?.totalTrades === "number" ? perf.totalTrades : undefined,
252
- final_equity: typeof perf?.finalEquity === "number" ? perf.finalEquity : undefined,
253
- equity_curve: Array.isArray(report?.equity_curve)
254
- ? JSON.stringify(report.equity_curve)
255
- : undefined,
256
- trade_journal: Array.isArray(report?.trade_journal)
257
- ? JSON.stringify(report.trade_journal)
258
- : undefined,
259
- monthly_returns:
260
- report?.monthly_returns && typeof report.monthly_returns === "object"
261
- ? JSON.stringify(report.monthly_returns)
238
+ if (status >= 200 && status < 300) {
239
+ const resp = data as Record<string, unknown>;
240
+
241
+ // Update backtest result metrics and strategy level
242
+ const verifyStrategyId = resp.entryId as string | undefined;
243
+ if (resp.backtestStatus === "completed" && backtestTaskId) {
244
+ const report = resp.backtestReport as Record<string, unknown> | undefined;
245
+ const perf = report?.performance as Record<string, unknown> | undefined;
246
+ updateBacktestResult(getDb(), backtestTaskId, {
247
+ status: "completed",
248
+ total_return:
249
+ typeof perf?.totalReturn === "number" ? perf.totalReturn : undefined,
250
+ sharpe: typeof perf?.sharpe === "number" ? perf.sharpe : undefined,
251
+ sortino: typeof perf?.sortino === "number" ? perf.sortino : undefined,
252
+ max_drawdown:
253
+ typeof perf?.maxDrawdown === "number" ? perf.maxDrawdown : undefined,
254
+ win_rate: typeof perf?.winRate === "number" ? perf.winRate : undefined,
255
+ profit_factor:
256
+ typeof perf?.profitFactor === "number" ? perf.profitFactor : undefined,
257
+ total_trades:
258
+ typeof perf?.totalTrades === "number" ? perf.totalTrades : undefined,
259
+ final_equity:
260
+ typeof perf?.finalEquity === "number" ? perf.finalEquity : undefined,
261
+ equity_curve: Array.isArray(report?.equity_curve)
262
+ ? JSON.stringify(report.equity_curve)
262
263
  : undefined,
263
- tearsheet_html:
264
- typeof report?.tearsheet_html === "string" ? report.tearsheet_html : undefined,
265
- completed_at: new Date().toISOString(),
266
- });
267
- // Backtest completed strategy stays at L1
268
- if (verifyStrategyId) updateStrategyLevel(getDb(), verifyStrategyId, "L1");
269
- } else if (resp.backtestStatus === "failed" && backtestTaskId) {
270
- updateBacktestResult(getDb(), backtestTaskId, {
271
- status: "failed",
272
- completed_at: new Date().toISOString(),
273
- });
274
- // Backtest failedrevert to L0
275
- if (verifyStrategyId) updateStrategyLevel(getDb(), verifyStrategyId, "L0");
276
- }
277
-
278
- const lines: string[] = [];
279
- lines.push("发布验证结果:");
280
- lines.push(`- Slug: ${resp.slug ?? "(未知)"}`);
281
- lines.push(`- Version: ${resp.version ?? "(未知)"}`);
282
- lines.push(`- Backtest Status: ${resp.backtestStatus ?? "(未知)"}`);
264
+ trade_journal: Array.isArray(report?.trade_journal)
265
+ ? JSON.stringify(report.trade_journal)
266
+ : undefined,
267
+ monthly_returns:
268
+ report?.monthly_returns && typeof report.monthly_returns === "object"
269
+ ? JSON.stringify(report.monthly_returns)
270
+ : undefined,
271
+ tearsheet_html:
272
+ typeof report?.tearsheet_html === "string" ? report.tearsheet_html : undefined,
273
+ completed_at: new Date().toISOString(),
274
+ });
275
+ // Backtest completedstrategy stays at L1
276
+ if (verifyStrategyId) updateStrategyLevel(getDb(), verifyStrategyId, "L1");
277
+ } else if (resp.backtestStatus === "failed" && backtestTaskId) {
278
+ updateBacktestResult(getDb(), backtestTaskId, {
279
+ status: "failed",
280
+ completed_at: new Date().toISOString(),
281
+ });
282
+ // Backtest failed revert to L0
283
+ if (verifyStrategyId) updateStrategyLevel(getDb(), verifyStrategyId, "L0");
284
+ }
283
285
 
284
- if (resp.backtestStatus === "completed" && resp.backtestReport) {
285
- const perf = (resp.backtestReport as Record<string, unknown>).performance as
286
- | Record<string, unknown>
287
- | undefined;
288
- if (perf) {
289
- lines.push("");
290
- lines.push("回测报告摘要:");
291
- if (typeof perf.totalReturn === "number")
292
- lines.push(`- 总收益率: ${(perf.totalReturn * 100).toFixed(2)}%`);
293
- if (typeof perf.sharpe === "number")
294
- lines.push(`- 夏普比率: ${perf.sharpe.toFixed(3)}`);
295
- if (typeof perf.maxDrawdown === "number")
296
- lines.push(`- 最大回撤: ${(perf.maxDrawdown * 100).toFixed(2)}%`);
297
- if (typeof perf.winRate === "number")
298
- lines.push(`- 胜率: ${(perf.winRate * 100).toFixed(1)}%`);
286
+ const lines: string[] = [];
287
+ lines.push("发布验证结果:");
288
+ lines.push(`- Slug: ${resp.slug ?? "(未知)"}`);
289
+ lines.push(`- Version: ${resp.version ?? "(未知)"}`);
290
+ lines.push(`- Backtest Status: ${resp.backtestStatus ?? "(未知)"}`);
291
+
292
+ if (resp.backtestStatus === "completed" && resp.backtestReport) {
293
+ const perf = (resp.backtestReport as Record<string, unknown>).performance as
294
+ | Record<string, unknown>
295
+ | undefined;
296
+ if (perf) {
297
+ lines.push("");
298
+ lines.push("回测报告摘要:");
299
+ if (typeof perf.totalReturn === "number")
300
+ lines.push(`- 总收益率: ${(perf.totalReturn * 100).toFixed(2)}%`);
301
+ if (typeof perf.sharpe === "number")
302
+ lines.push(`- 夏普比率: ${perf.sharpe.toFixed(3)}`);
303
+ if (typeof perf.maxDrawdown === "number")
304
+ lines.push(`- 最大回撤: ${(perf.maxDrawdown * 100).toFixed(2)}%`);
305
+ if (typeof perf.winRate === "number")
306
+ lines.push(`- 胜率: ${(perf.winRate * 100).toFixed(1)}%`);
307
+ }
299
308
  }
309
+
310
+ return {
311
+ content: [{ type: "text" as const, text: lines.join("\n") }],
312
+ details: { success: true, ...resp },
313
+ };
300
314
  }
301
315
 
302
- return {
303
- content: [{ type: "text" as const, text: lines.join("\n") }],
304
- details: { success: true, ...resp },
305
- };
316
+ return json({
317
+ success: false,
318
+ status,
319
+ error:
320
+ (data as { code?: string; message?: string })?.message ??
321
+ (data as { detail?: string })?.detail ??
322
+ data,
323
+ });
324
+ } catch (err) {
325
+ return json({
326
+ success: false,
327
+ error: err instanceof Error ? err.message : String(err),
328
+ });
306
329
  }
307
-
308
- return json({
309
- success: false,
310
- status,
311
- error:
312
- (data as { code?: string; message?: string })?.message ??
313
- (data as { detail?: string })?.detail ??
314
- data,
315
- });
316
- } catch (err) {
317
- return json({
318
- success: false,
319
- error: err instanceof Error ? err.message : String(err),
320
- });
321
- }
322
- }),
330
+ },
331
+ ),
323
332
  },
324
333
  { names: ["skill_publish_verify"] },
325
334
  );