@openfinclaw/openfinclaw-strategy 2026.3.275 → 2026.3.310

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.test.ts CHANGED
@@ -77,7 +77,7 @@ describe("openfinclaw plugin", () => {
77
77
  expect.objectContaining({
78
78
  path: "/plugins/openfinclaw",
79
79
  match: "prefix",
80
- auth: "gateway",
80
+ auth: "plugin",
81
81
  }),
82
82
  );
83
83
  });
@@ -86,10 +86,12 @@ describe("openfinclaw plugin", () => {
86
86
  const { api, tools } = createFakeApi({});
87
87
  plugin.register(api);
88
88
 
89
- expect(tools.size).toBeGreaterThanOrEqual(7);
89
+ expect(tools.size).toBeGreaterThanOrEqual(9);
90
90
  expect(tools.has("skill_publish")).toBe(true);
91
91
  expect(tools.has("skill_publish_verify")).toBe(true);
92
92
  expect(tools.has("skill_validate")).toBe(true);
93
+ expect(tools.has("strategy_price_monitor")).toBe(true);
94
+ expect(tools.has("strategy_periodic_report")).toBe(true);
93
95
  });
94
96
 
95
97
  it("skill_publish_verify returns success when API returns 200 with submissionId", async () => {
package/index.ts CHANGED
@@ -8,7 +8,7 @@ import type { Command } from "commander";
8
8
  * - Dashboard: embedded HTTP server at http://127.0.0.1:<httpPort> (default 18792)
9
9
  * Supports FEP v2.0 protocol for strategy packages.
10
10
  */
11
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
11
+ 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";
@@ -20,15 +20,18 @@ import { setupOpenfinclawCronJobs } from "./src/scheduler/cron-setup.js";
20
20
  import { AggregatedNewsProvider, createNewsProviders } from "./src/scheduler/news-provider.js";
21
21
  import { registerSchedulerTools } from "./src/scheduler/tools.js";
22
22
  import { registerStrategyTools } from "./src/strategy/tools.js";
23
+ import { setupTournamentCronJob } from "./src/tournament/cron-setup.js";
24
+ import { TournamentDb } from "./src/tournament/db.js";
25
+ import { buildOrchestratorPrompt } from "./src/tournament/prompts.js";
26
+ import { registerTournamentTools } from "./src/tournament/tools.js";
23
27
 
24
- const openfinclawPlugin = {
28
+ export default definePluginEntry({
25
29
  id: "openfinclaw-strategy",
26
30
  name: "OpenFinClaw",
27
31
  description:
28
32
  "Unified financial tools: strategy publishing/fork/validation, market data (price/K-line/crypto/compare/search). Single API key for Hub and DataHub.",
29
- kind: "financial" as const,
30
33
 
31
- register(api: OpenClawPluginApi) {
34
+ register(api) {
32
35
  const config = resolvePluginConfig(api);
33
36
 
34
37
  // Register DataHub market data tools (fin_price, fin_kline, fin_crypto, fin_compare, fin_slim_search)
@@ -38,7 +41,7 @@ const openfinclawPlugin = {
38
41
  // Register strategy tools (skill_publish, skill_validate, skill_fork, skill_leaderboard, etc.)
39
42
  registerStrategyTools(api, config, getDb);
40
43
 
41
- // Register scheduler tools (strategy_daily_scan, strategy_scan_history)
44
+ // Register scheduler tools (strategy_daily_scan, strategy_price_monitor, strategy_scan_history, strategy_periodic_report)
42
45
  const newsProviders = createNewsProviders(config);
43
46
  const newsProvider = new AggregatedNewsProvider(newsProviders);
44
47
  registerSchedulerTools(api, config, getDb, newsProvider);
@@ -69,30 +72,48 @@ const openfinclawPlugin = {
69
72
  handler: createOpenFinclawGatewayProxy({ port: config.httpPort, logger: api.logger }),
70
73
  });
71
74
 
75
+ // Register tournament tools (tournament_pick, tournament_leaderboard, tournament_result)
76
+ const getTournamentDb = () => new TournamentDb(getDb());
77
+ registerTournamentTools(api.registerTool.bind(api), getTournamentDb);
78
+
72
79
  // Inject agent system prompt: prioritise tool calls so data lands in SQLite
80
+ const tournamentPrompt = buildOrchestratorPrompt();
73
81
  api.on("before_prompt_build", async () => ({
74
- prependSystemContext: OPENFINCLAW_AGENT_GUIDANCE,
82
+ prependSystemContext: `${OPENFINCLAW_AGENT_GUIDANCE}\n\n${tournamentPrompt}`,
75
83
  }));
76
84
 
77
85
  // ── Gateway Cron registration ──
78
- // Register cron jobs on gateway_start (writes to ~/.openclaw/cron/jobs.json)
86
+ // Write cron jobs directly to ~/.openclaw/cron/jobs.json during register().
87
+ // This ensures jobs are available immediately on both gateway startup AND
88
+ // hot-reload (plugin install without restart). The CronService picks up
89
+ // file changes on its next tick.
79
90
  if (config.schedulerEnabled) {
80
- api.on("gateway_start", async () => {
81
- try {
82
- const result = await setupOpenfinclawCronJobs(config);
91
+ setupOpenfinclawCronJobs(config)
92
+ .then((result) => {
83
93
  if (result.created > 0) {
84
94
  api.logger.info(
85
95
  `[OpenFinClaw] Cron jobs registered: ${result.created} created, ${result.existing} existing`,
86
96
  );
87
97
  }
88
- } catch (err) {
98
+ })
99
+ .catch((err) => {
89
100
  api.logger.info(
90
101
  `[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
91
102
  );
92
- }
93
- });
103
+ });
104
+
105
+ // Register tournament cron job
106
+ setupTournamentCronJob()
107
+ .then((result) => {
108
+ if (result.created) {
109
+ api.logger.info("[OpenFinClaw] Tournament cron job registered");
110
+ }
111
+ })
112
+ .catch((err) => {
113
+ api.logger.info(
114
+ `[OpenFinClaw] Tournament cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
115
+ );
116
+ });
94
117
  }
95
118
  },
96
- };
97
-
98
- export default openfinclawPlugin;
119
+ });
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.310",
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[] = [];
@@ -342,20 +353,51 @@ export function updateScanHistory(
342
353
  }
343
354
  }
344
355
 
345
- /** Query scan history, newest first. Optionally filter by scan_type. */
356
+ /** Query scan history, newest first. Optionally filter by scan_type and started_at lower bound (ISO 8601). */
346
357
  export function queryScanHistory(
347
358
  db: DatabaseSync,
348
- opts: { scanType?: string; limit?: number; offset?: number } = {},
359
+ opts: {
360
+ scanType?: string;
361
+ startedAfter?: string;
362
+ limit?: number;
363
+ offset?: number;
364
+ } = {},
349
365
  ): ScanHistoryEntry[] {
350
- const { limit = 20, offset = 0, scanType } = opts;
366
+ const { limit = 20, offset = 0, scanType, startedAfter } = opts;
367
+ const conditions: string[] = [];
368
+ const params: unknown[] = [];
351
369
  if (scanType) {
352
- return db
353
- .prepare(`SELECT * FROM scan_history WHERE scan_type = ? ORDER BY started_at DESC LIMIT ? OFFSET ?`)
354
- .all(scanType, limit, offset) as ScanHistoryEntry[];
370
+ conditions.push("scan_type = ?");
371
+ params.push(scanType);
355
372
  }
373
+ if (startedAfter) {
374
+ conditions.push("started_at >= ?");
375
+ params.push(startedAfter);
376
+ }
377
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
378
+ params.push(limit, offset);
356
379
  return db
357
- .prepare(`SELECT * FROM scan_history ORDER BY started_at DESC LIMIT ? OFFSET ?`)
358
- .all(limit, offset) as ScanHistoryEntry[];
380
+ .prepare(`SELECT * FROM scan_history ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`)
381
+ .all(...params) as ScanHistoryEntry[];
382
+ }
383
+
384
+ /**
385
+ * Count scan_history rows grouped by scan_type since an inclusive time bound (ISO 8601 started_at).
386
+ */
387
+ export function countScanHistoryByTypeSince(
388
+ db: DatabaseSync,
389
+ startedAfter: string,
390
+ ): Map<string, number> {
391
+ const rows = db
392
+ .prepare(
393
+ `SELECT scan_type, COUNT(*) AS cnt FROM scan_history WHERE started_at >= ? GROUP BY scan_type`,
394
+ )
395
+ .all(startedAfter) as Array<{ scan_type: string; cnt: number }>;
396
+ const map = new Map<string, number>();
397
+ for (const r of rows) {
398
+ map.set(r.scan_type, Number(r.cnt));
399
+ }
400
+ return map;
359
401
  }
360
402
 
361
403
  // ── Price Alerts ──────────────────────────────────────────────────────────
@@ -395,20 +437,40 @@ export function insertPriceAlert(db: DatabaseSync, alert: PriceAlertEntry): void
395
437
  }
396
438
  }
397
439
 
398
- /** Query price alerts, newest first. */
440
+ /** Query price alerts, newest first. Optionally filter by strategy and created_at lower bound (ISO 8601). */
399
441
  export function queryPriceAlerts(
400
442
  db: DatabaseSync,
401
- opts: { strategyId?: string; limit?: number; offset?: number } = {},
443
+ opts: {
444
+ strategyId?: string;
445
+ createdAfter?: string;
446
+ limit?: number;
447
+ offset?: number;
448
+ } = {},
402
449
  ): PriceAlertEntry[] {
403
- const { limit = 50, offset = 0, strategyId } = opts;
450
+ const { limit = 50, offset = 0, strategyId, createdAfter } = opts;
451
+ const conditions: string[] = [];
452
+ const params: unknown[] = [];
404
453
  if (strategyId) {
405
- return db
406
- .prepare(`SELECT * FROM price_alerts WHERE strategy_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`)
407
- .all(strategyId, limit, offset) as PriceAlertEntry[];
454
+ conditions.push("strategy_id = ?");
455
+ params.push(strategyId);
456
+ }
457
+ if (createdAfter) {
458
+ conditions.push("created_at >= ?");
459
+ params.push(createdAfter);
408
460
  }
461
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
462
+ params.push(limit, offset);
409
463
  return db
410
- .prepare(`SELECT * FROM price_alerts ORDER BY created_at DESC LIMIT ? OFFSET ?`)
411
- .all(limit, offset) as PriceAlertEntry[];
464
+ .prepare(`SELECT * FROM price_alerts ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
465
+ .all(...params) as PriceAlertEntry[];
466
+ }
467
+
468
+ /** Count price_alerts rows since an inclusive time bound (ISO 8601 created_at). */
469
+ export function countPriceAlertsSince(db: DatabaseSync, createdAfter: string): number {
470
+ const row = db
471
+ .prepare(`SELECT COUNT(*) AS cnt FROM price_alerts WHERE created_at >= ?`)
472
+ .get(createdAfter) as { cnt: number } | undefined;
473
+ return Number(row?.cnt ?? 0);
412
474
  }
413
475
 
414
476
  /** Mark a price alert as acknowledged. */
package/src/db/schema.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * SQLite schema for OpenFinClaw plugin (MVP 4 tables).
3
- * Based on ER diagram v0.3 — openfinclaw-opc-fund-plugin.
3
+ * Based on ER diagram v0.3.
4
4
  */
5
5
  import type { DatabaseSync } from "node:sqlite";
6
+ import { ensureTournamentSchema } from "../tournament/db.js";
6
7
 
7
8
  /**
8
9
  * Create (or migrate) all MVP tables.
@@ -134,6 +135,9 @@ export function ensureSchema(db: DatabaseSync): void {
134
135
  db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_created ON price_alerts(created_at);`);
135
136
  db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_strategy ON price_alerts(strategy_id);`);
136
137
 
138
+ // ── tournament tables ──────────────────────────────────────────────────
139
+ ensureTournamentSchema(db);
140
+
137
141
  // ── migrations (add columns to existing databases) ──────────────────────
138
142
  }
139
143
 
@@ -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
 
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Markdown builder for `strategy_periodic_report` (weekly / monthly rolling windows).
3
+ */
4
+
5
+ /** One row for the performance ranking table. */
6
+ export interface StrategyRankRow {
7
+ strategyId: string;
8
+ strategyName: string;
9
+ totalReturn: number | null;
10
+ sharpe: number | null;
11
+ maxDrawdown: number | null;
12
+ }
13
+
14
+ /** Inputs collected by the tool before formatting. */
15
+ export interface PeriodicReportPayload {
16
+ period: "weekly" | "monthly";
17
+ windowStartIso: string;
18
+ windowEndIso: string;
19
+ scanCountsByType: Map<string, number>;
20
+ priceAlertCount: number;
21
+ rankRows: StrategyRankRow[];
22
+ }
23
+
24
+ /**
25
+ * Format a periodic performance report as Markdown for the agent.
26
+ *
27
+ * @param payload - Aggregated stats and ranking rows for the reporting window.
28
+ * @returns Markdown string (zh headings, decimal returns as percent like daily scan).
29
+ */
30
+ export function formatPeriodicReportMarkdown(payload: PeriodicReportPayload): string {
31
+ const periodLabel = payload.period === "weekly" ? "周报" : "月报";
32
+ const start = payload.windowStartIso.slice(0, 10);
33
+ const end = payload.windowEndIso.slice(0, 10);
34
+ const lines: string[] = [];
35
+
36
+ lines.push(`## 策略绩效${periodLabel} — ${start} ~ ${end}`);
37
+ lines.push("");
38
+
39
+ lines.push("### 绩效排名");
40
+ lines.push("");
41
+ lines.push("| 排名 | 策略 | 总收益 | 夏普 | 最大回撤 |");
42
+ lines.push("|------|------|--------|------|----------|");
43
+
44
+ const sorted = [...payload.rankRows].sort((a, b) => {
45
+ const ar = a.totalReturn ?? Number.NEGATIVE_INFINITY;
46
+ const br = b.totalReturn ?? Number.NEGATIVE_INFINITY;
47
+ return br - ar;
48
+ });
49
+
50
+ sorted.forEach((row, i) => {
51
+ const ret = row.totalReturn == null ? "—" : `${(row.totalReturn * 100).toFixed(2)}%`;
52
+ const sharpe = row.sharpe == null ? "—" : row.sharpe.toFixed(2);
53
+ const dd = row.maxDrawdown == null ? "—" : `${(row.maxDrawdown * 100).toFixed(2)}%`;
54
+ lines.push(`| ${i + 1} | ${row.strategyName} | ${ret} | ${sharpe} | ${dd} |`);
55
+ });
56
+
57
+ lines.push("");
58
+ lines.push("### 活动统计");
59
+ const keys = [...payload.scanCountsByType.keys()].sort();
60
+ if (keys.length === 0) {
61
+ lines.push("- (窗口内无扫描记录)");
62
+ } else {
63
+ for (const k of keys) {
64
+ lines.push(`- ${k}: ${payload.scanCountsByType.get(k)} 次`);
65
+ }
66
+ }
67
+ lines.push(`- 价格告警: ${payload.priceAlertCount} 条`);
68
+ lines.push("");
69
+
70
+ return lines.join("\n");
71
+ }
@@ -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("### 汇总");