@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 +4 -2
- package/index.ts +37 -16
- package/package.json +1 -1
- package/src/config.ts +3 -1
- package/src/db/repositories.ts +79 -17
- package/src/db/schema.ts +5 -1
- package/src/scheduler/news-provider.ts +1 -1
- package/src/scheduler/periodic-report-builder.ts +71 -0
- package/src/scheduler/scan-report-builder.ts +6 -2
- package/src/scheduler/tools.ts +362 -42
- package/src/strategy/tools.ts +114 -105
- package/src/tournament/cron-setup.ts +102 -0
- package/src/tournament/db.test.ts +222 -0
- package/src/tournament/db.ts +286 -0
- package/src/tournament/orchestrator.test.ts +232 -0
- package/src/tournament/orchestrator.ts +238 -0
- package/src/tournament/prompts.ts +65 -0
- package/src/tournament/tools.test.ts +221 -0
- package/src/tournament/tools.ts +192 -0
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: "
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
}
|
|
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.
|
|
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(
|
|
104
|
+
const newsProvider: NewsProviderType = VALID_NEWS_PROVIDERS.has(
|
|
105
|
+
newsProviderRaw as NewsProviderType,
|
|
106
|
+
)
|
|
105
107
|
? (newsProviderRaw as NewsProviderType)
|
|
106
108
|
: "coingecko";
|
|
107
109
|
|
package/src/db/repositories.ts
CHANGED
|
@@ -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<
|
|
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: {
|
|
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
|
-
|
|
353
|
-
|
|
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(
|
|
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: {
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
132
|
+
lines.push(
|
|
133
|
+
`- ${e.strategyName}: ${e.significantNewsCount} 条相关新闻,建议分析影响并考虑是否需要优化`,
|
|
134
|
+
);
|
|
131
135
|
}
|
|
132
136
|
} else {
|
|
133
137
|
lines.push("### 汇总");
|