@openfinclaw/openfinclaw-strategy 2026.3.274 → 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 +33 -6
- package/openclaw.plugin.json +47 -0
- package/package.json +1 -1
- package/src/cli.ts +17 -17
- package/src/config.ts +67 -1
- package/src/datahub/tools.ts +9 -9
- package/src/db/db.ts +16 -9
- package/src/db/repositories.ts +154 -0
- package/src/db/schema.ts +35 -0
- package/src/http/routes.ts +19 -1
- package/src/http/server.ts +16 -12
- package/src/middleware/with-logging.ts +4 -4
- package/src/prompt-guidance.ts +14 -0
- package/src/scheduler/cron-setup.ts +181 -0
- package/src/scheduler/news-provider.ts +295 -0
- package/src/scheduler/scan-report-builder.ts +162 -0
- package/src/scheduler/tools.ts +261 -0
- package/src/scheduler/types.ts +89 -0
- package/src/strategy/tools.ts +129 -120
- package/src/types.ts +27 -0
- package/web/index.html +240 -0
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
|
-
|
|
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,
|
|
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
|
-
|
|
53
|
+
getDb,
|
|
48
54
|
}),
|
|
49
55
|
{ commands: ["strategy"] },
|
|
50
56
|
);
|
|
@@ -67,6 +73,27 @@ const openfinclawPlugin = {
|
|
|
67
73
|
api.on("before_prompt_build", async () => ({
|
|
68
74
|
prependSystemContext: OPENFINCLAW_AGENT_GUIDANCE,
|
|
69
75
|
}));
|
|
76
|
+
|
|
77
|
+
// ── Gateway Cron registration ──
|
|
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.
|
|
82
|
+
if (config.schedulerEnabled) {
|
|
83
|
+
setupOpenfinclawCronJobs(config)
|
|
84
|
+
.then((result) => {
|
|
85
|
+
if (result.created > 0) {
|
|
86
|
+
api.logger.info(
|
|
87
|
+
`[OpenFinClaw] Cron jobs registered: ${result.created} created, ${result.existing} existing`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
api.logger.info(
|
|
93
|
+
`[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
70
97
|
},
|
|
71
98
|
};
|
|
72
99
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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/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
|
-
|
|
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(
|
|
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
|
-
|
|
42
|
+
getDb: () => DatabaseSync;
|
|
43
43
|
}) {
|
|
44
|
-
const { program, config,
|
|
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
|
-
|
|
75
|
+
getDb,
|
|
76
76
|
"leaderboard",
|
|
77
77
|
{ boardType, limit, offset },
|
|
78
78
|
startMs,
|
|
@@ -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(
|
|
127
|
+
logCli(getDb, "leaderboard", { boardType, limit, offset }, startMs);
|
|
128
128
|
} catch (err) {
|
|
129
|
-
logCli(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
281
|
+
logCli(getDb, "remove", { nameOrId }, startMs);
|
|
282
282
|
console.log("✓ 策略已删除");
|
|
283
283
|
} else {
|
|
284
|
-
logCli(
|
|
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,69 @@ 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(
|
|
105
|
+
newsProviderRaw as NewsProviderType,
|
|
106
|
+
)
|
|
107
|
+
? (newsProviderRaw as NewsProviderType)
|
|
108
|
+
: "coingecko";
|
|
109
|
+
|
|
110
|
+
// ── Alert config ──
|
|
111
|
+
const alertThresholdRaw = raw?.priceAlertThreshold ?? readEnv(["OPENFINCLAW_ALERT_THRESHOLD"]);
|
|
112
|
+
const priceAlertThreshold =
|
|
113
|
+
Number(alertThresholdRaw) > 0 ? Number(alertThresholdRaw) : DEFAULT_ALERT_THRESHOLD;
|
|
114
|
+
|
|
58
115
|
return {
|
|
59
116
|
apiKey: apiKey && apiKey.length > 0 ? apiKey : undefined,
|
|
60
117
|
hubApiUrl: hubApiUrl.replace(/\/$/, ""),
|
|
61
118
|
datahubGatewayUrl: datahubGatewayUrl.replace(/\/+$/, ""),
|
|
62
119
|
requestTimeoutMs,
|
|
63
120
|
httpPort,
|
|
121
|
+
schedulerEnabled,
|
|
122
|
+
scanCronExpr,
|
|
123
|
+
monitorCronExpr,
|
|
124
|
+
weeklyReportCronExpr,
|
|
125
|
+
monthlyReportCronExpr,
|
|
126
|
+
scanTimezone,
|
|
127
|
+
newsApiKey: newsApiKey && newsApiKey.length > 0 ? newsApiKey : undefined,
|
|
128
|
+
newsProvider,
|
|
129
|
+
priceAlertThreshold,
|
|
64
130
|
};
|
|
65
131
|
}
|
package/src/datahub/tools.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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
|
}
|
package/src/db/repositories.ts
CHANGED
|
@@ -281,6 +281,160 @@ export function updateStrategyLevel(db: DatabaseSync, id: string, level: Strateg
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// ── Scan History ──────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
export interface ScanHistoryEntry {
|
|
287
|
+
id: string;
|
|
288
|
+
scan_type: string;
|
|
289
|
+
started_at: string;
|
|
290
|
+
completed_at?: string | null;
|
|
291
|
+
status: string;
|
|
292
|
+
strategies_scanned: number;
|
|
293
|
+
news_found: number;
|
|
294
|
+
actions_taken: number;
|
|
295
|
+
summary?: string | null;
|
|
296
|
+
detail_json?: string | null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Insert a scan history entry. */
|
|
300
|
+
export function insertScanHistory(db: DatabaseSync, entry: ScanHistoryEntry): void {
|
|
301
|
+
try {
|
|
302
|
+
db.prepare(`
|
|
303
|
+
INSERT INTO scan_history
|
|
304
|
+
(id, scan_type, started_at, completed_at, status,
|
|
305
|
+
strategies_scanned, news_found, actions_taken, summary, detail_json)
|
|
306
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
307
|
+
`).run(
|
|
308
|
+
entry.id,
|
|
309
|
+
entry.scan_type,
|
|
310
|
+
entry.started_at,
|
|
311
|
+
entry.completed_at ?? null,
|
|
312
|
+
entry.status,
|
|
313
|
+
entry.strategies_scanned,
|
|
314
|
+
entry.news_found,
|
|
315
|
+
entry.actions_taken,
|
|
316
|
+
entry.summary ?? null,
|
|
317
|
+
entry.detail_json ?? null,
|
|
318
|
+
);
|
|
319
|
+
} catch {
|
|
320
|
+
// Logging must never crash the calling tool
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Update scan history entry after completion. */
|
|
325
|
+
export function updateScanHistory(
|
|
326
|
+
db: DatabaseSync,
|
|
327
|
+
id: string,
|
|
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
|
+
>,
|
|
340
|
+
): void {
|
|
341
|
+
try {
|
|
342
|
+
const sets: string[] = [];
|
|
343
|
+
const values: unknown[] = [];
|
|
344
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
345
|
+
sets.push(`${k} = ?`);
|
|
346
|
+
values.push(v ?? null);
|
|
347
|
+
}
|
|
348
|
+
if (sets.length === 0) return;
|
|
349
|
+
values.push(id);
|
|
350
|
+
db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
351
|
+
} catch {
|
|
352
|
+
// Logging must never crash the calling tool
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Query scan history, newest first. Optionally filter by scan_type. */
|
|
357
|
+
export function queryScanHistory(
|
|
358
|
+
db: DatabaseSync,
|
|
359
|
+
opts: { scanType?: string; limit?: number; offset?: number } = {},
|
|
360
|
+
): ScanHistoryEntry[] {
|
|
361
|
+
const { limit = 20, offset = 0, scanType } = opts;
|
|
362
|
+
if (scanType) {
|
|
363
|
+
return db
|
|
364
|
+
.prepare(
|
|
365
|
+
`SELECT * FROM scan_history WHERE scan_type = ? ORDER BY started_at DESC LIMIT ? OFFSET ?`,
|
|
366
|
+
)
|
|
367
|
+
.all(scanType, limit, offset) as ScanHistoryEntry[];
|
|
368
|
+
}
|
|
369
|
+
return db
|
|
370
|
+
.prepare(`SELECT * FROM scan_history ORDER BY started_at DESC LIMIT ? OFFSET ?`)
|
|
371
|
+
.all(limit, offset) as ScanHistoryEntry[];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Price Alerts ──────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
export interface PriceAlertEntry {
|
|
377
|
+
id: string;
|
|
378
|
+
strategy_id: string;
|
|
379
|
+
symbol: string;
|
|
380
|
+
alert_type: string;
|
|
381
|
+
trigger_value?: number | null;
|
|
382
|
+
threshold?: number | null;
|
|
383
|
+
message?: string | null;
|
|
384
|
+
created_at: string;
|
|
385
|
+
acknowledged: number;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Insert a price alert. */
|
|
389
|
+
export function insertPriceAlert(db: DatabaseSync, alert: PriceAlertEntry): void {
|
|
390
|
+
try {
|
|
391
|
+
db.prepare(`
|
|
392
|
+
INSERT INTO price_alerts
|
|
393
|
+
(id, strategy_id, symbol, alert_type, trigger_value, threshold, message, created_at, acknowledged)
|
|
394
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
395
|
+
`).run(
|
|
396
|
+
alert.id,
|
|
397
|
+
alert.strategy_id,
|
|
398
|
+
alert.symbol,
|
|
399
|
+
alert.alert_type,
|
|
400
|
+
alert.trigger_value ?? null,
|
|
401
|
+
alert.threshold ?? null,
|
|
402
|
+
alert.message ?? null,
|
|
403
|
+
alert.created_at,
|
|
404
|
+
alert.acknowledged,
|
|
405
|
+
);
|
|
406
|
+
} catch {
|
|
407
|
+
// Logging must never crash the calling tool
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Query price alerts, newest first. */
|
|
412
|
+
export function queryPriceAlerts(
|
|
413
|
+
db: DatabaseSync,
|
|
414
|
+
opts: { strategyId?: string; limit?: number; offset?: number } = {},
|
|
415
|
+
): PriceAlertEntry[] {
|
|
416
|
+
const { limit = 50, offset = 0, strategyId } = opts;
|
|
417
|
+
if (strategyId) {
|
|
418
|
+
return db
|
|
419
|
+
.prepare(
|
|
420
|
+
`SELECT * FROM price_alerts WHERE strategy_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
421
|
+
)
|
|
422
|
+
.all(strategyId, limit, offset) as PriceAlertEntry[];
|
|
423
|
+
}
|
|
424
|
+
return db
|
|
425
|
+
.prepare(`SELECT * FROM price_alerts ORDER BY created_at DESC LIMIT ? OFFSET ?`)
|
|
426
|
+
.all(limit, offset) as PriceAlertEntry[];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Mark a price alert as acknowledged. */
|
|
430
|
+
export function acknowledgePriceAlert(db: DatabaseSync, id: string): void {
|
|
431
|
+
try {
|
|
432
|
+
db.prepare(`UPDATE price_alerts SET acknowledged = 1 WHERE id = ?`).run(id);
|
|
433
|
+
} catch {
|
|
434
|
+
// Logging must never crash the calling tool
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
284
438
|
// ── Reads ─────────────────────────────────────────────────────────────────
|
|
285
439
|
|
|
286
440
|
/** Query agent_activity_log, newest first. */
|
package/src/db/schema.ts
CHANGED
|
@@ -99,6 +99,41 @@ export function ensureSchema(db: DatabaseSync): void {
|
|
|
99
99
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_ae_timestamp ON agent_events(timestamp);`);
|
|
100
100
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_ae_strategy_id ON agent_events(strategy_id);`);
|
|
101
101
|
|
|
102
|
+
// ── scan_history ─────────────────────────────────────────────────────────
|
|
103
|
+
db.exec(`
|
|
104
|
+
CREATE TABLE IF NOT EXISTS scan_history (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
scan_type TEXT NOT NULL,
|
|
107
|
+
started_at TEXT NOT NULL,
|
|
108
|
+
completed_at TEXT,
|
|
109
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
110
|
+
strategies_scanned INTEGER DEFAULT 0,
|
|
111
|
+
news_found INTEGER DEFAULT 0,
|
|
112
|
+
actions_taken INTEGER DEFAULT 0,
|
|
113
|
+
summary TEXT,
|
|
114
|
+
detail_json TEXT
|
|
115
|
+
);
|
|
116
|
+
`);
|
|
117
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_scan_history_started ON scan_history(started_at);`);
|
|
118
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_scan_history_type ON scan_history(scan_type);`);
|
|
119
|
+
|
|
120
|
+
// ── price_alerts ────────────────────────────────────────────────────────
|
|
121
|
+
db.exec(`
|
|
122
|
+
CREATE TABLE IF NOT EXISTS price_alerts (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
strategy_id TEXT NOT NULL,
|
|
125
|
+
symbol TEXT NOT NULL,
|
|
126
|
+
alert_type TEXT NOT NULL,
|
|
127
|
+
trigger_value REAL,
|
|
128
|
+
threshold REAL,
|
|
129
|
+
message TEXT,
|
|
130
|
+
created_at TEXT NOT NULL,
|
|
131
|
+
acknowledged INTEGER DEFAULT 0
|
|
132
|
+
);
|
|
133
|
+
`);
|
|
134
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_created ON price_alerts(created_at);`);
|
|
135
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_strategy ON price_alerts(strategy_id);`);
|
|
136
|
+
|
|
102
137
|
// ── migrations (add columns to existing databases) ──────────────────────
|
|
103
138
|
}
|
|
104
139
|
|