tickflow-assist 0.3.1 → 0.3.2
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/README.md +3 -3
- package/dist/analysis/types/composite-analysis.d.ts +11 -0
- package/dist/bootstrap.js +2 -2
- package/dist/config/normalize.js +1 -0
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +1 -0
- package/dist/dev/tickflow-assist-cli.js +25 -0
- package/dist/prompts/analysis/post-close-review-user-prompt.js +18 -0
- package/dist/services/jin10-flash-monitor-service.d.ts +2 -1
- package/dist/services/jin10-flash-monitor-service.js +11 -2
- package/dist/services/post-close-review-service.d.ts +6 -1
- package/dist/services/post-close-review-service.js +35 -1
- package/dist/storage/repositories/jin10-flash-delivery-repo.d.ts +1 -0
- package/dist/storage/repositories/jin10-flash-delivery-repo.js +36 -0
- package/dist/storage/repositories/jin10-flash-repo.d.ts +1 -0
- package/dist/storage/repositories/jin10-flash-repo.js +18 -0
- package/dist/utils/china-time.d.ts +1 -0
- package/dist/utils/china-time.js +5 -0
- package/openclaw.plugin.json +11 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。
|
|
4
4
|
|
|
5
|
-
最近更新:`v0.3.
|
|
5
|
+
最近更新:`v0.3.2` 为金十快讯新增夜间静默配置并将默认值改为“开启静默”,同时把个股关联快讯与市场概览快讯接入收盘复盘上下文。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。
|
|
6
6
|
|
|
7
|
-
当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.
|
|
7
|
+
当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.5` 上兼容。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -83,7 +83,7 @@ plugins.entries["tickflow-assist"].config
|
|
|
83
83
|
- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`
|
|
84
84
|
- 能力补充:`mxSearchApiKey`、`jin10ApiToken`
|
|
85
85
|
|
|
86
|
-
其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。
|
|
86
|
+
其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。
|
|
87
87
|
|
|
88
88
|
## 功能
|
|
89
89
|
|
|
@@ -104,9 +104,20 @@ export interface PriorKeyLevelValidationContext {
|
|
|
104
104
|
summary: string;
|
|
105
105
|
lines: string[];
|
|
106
106
|
}
|
|
107
|
+
export interface FlashNewsItem {
|
|
108
|
+
publishedAt: string;
|
|
109
|
+
content: string;
|
|
110
|
+
headline: string;
|
|
111
|
+
source: "stock_alert" | "market_overview";
|
|
112
|
+
}
|
|
113
|
+
export interface FlashNewsContext {
|
|
114
|
+
stockAlerts: FlashNewsItem[];
|
|
115
|
+
marketOverviewFlashes: FlashNewsItem[];
|
|
116
|
+
}
|
|
107
117
|
export interface PostCloseReviewInput extends CompositeAnalysisInput {
|
|
108
118
|
compositeResult: CompositeAnalysisResult;
|
|
109
119
|
validation: PriorKeyLevelValidationContext;
|
|
120
|
+
flashContext: FlashNewsContext;
|
|
110
121
|
}
|
|
111
122
|
export interface PostCloseReviewResult {
|
|
112
123
|
analysisText: string;
|
package/dist/bootstrap.js
CHANGED
|
@@ -137,9 +137,9 @@ export function createAppContext(config, options = {}) {
|
|
|
137
137
|
const compositeStockAnalysisTask = new CompositeStockAnalysisTask(keyLevelsRepository, analysisLogRepository);
|
|
138
138
|
const compositeAnalysisOrchestrator = new CompositeAnalysisOrchestrator(analysisService, marketAnalysisProvider, financialAnalysisProvider, newsAnalysisProvider, klineTechnicalSignalTask, financialFundamentalTask, financialFundamentalLiteTask, newsCatalystTask, compositeStockAnalysisTask, technicalAnalysisRepository, financialAnalysisRepository, newsAnalysisRepository, compositeAnalysisRepository);
|
|
139
139
|
const monitorService = new MonitorService(config.databasePath, config.requestInterval, config.alertChannel, watchlistService, quoteService, tradingCalendarService, keyLevelsRepository, alertLogRepository, klinesRepository, intradayKlinesRepository, klineService, alertService, alertMediaService);
|
|
140
|
-
const jin10FlashMonitorService = new Jin10FlashMonitorService(config.databasePath, config.jin10FlashPollInterval, config.jin10FlashRetentionDays, watchlistService, jin10McpService, analysisService, alertService, jin10FlashRepository, jin10FlashDeliveryRepository);
|
|
140
|
+
const jin10FlashMonitorService = new Jin10FlashMonitorService(config.databasePath, config.jin10FlashPollInterval, config.jin10FlashRetentionDays, config.jin10FlashNightAlert, watchlistService, jin10McpService, analysisService, alertService, jin10FlashRepository, jin10FlashDeliveryRepository);
|
|
141
141
|
const updateService = new UpdateService(klineService, config.tickflowApiKeyLevel, indicatorService, klinesRepository, indicatorsRepository, intradayKlinesRepository, watchlistService, tradingCalendarService);
|
|
142
|
-
const postCloseReviewService = new PostCloseReviewService(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository);
|
|
142
|
+
const postCloseReviewService = new PostCloseReviewService(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, jin10FlashDeliveryRepository, jin10FlashRepository);
|
|
143
143
|
const realtimeMonitorWorker = new RealtimeMonitorWorker(monitorService, config.requestInterval * 1000);
|
|
144
144
|
const jin10FlashWorker = new Jin10FlashWorker(jin10FlashMonitorService, config.jin10FlashPollInterval * 1000);
|
|
145
145
|
const dailyUpdateWorker = new DailyUpdateWorker(updateService, postCloseReviewService, tradingCalendarService, config.databasePath, alertService, config.dailyUpdateNotify, runtime.configSource);
|
package/dist/config/normalize.js
CHANGED
|
@@ -52,6 +52,7 @@ export function normalizePluginConfig(input) {
|
|
|
52
52
|
jin10ApiToken: normalizeString(raw.jin10ApiToken),
|
|
53
53
|
jin10FlashPollInterval: normalizeInteger(raw.jin10FlashPollInterval, DEFAULT_PLUGIN_CONFIG.jin10FlashPollInterval),
|
|
54
54
|
jin10FlashRetentionDays: normalizeInteger(raw.jin10FlashRetentionDays, DEFAULT_PLUGIN_CONFIG.jin10FlashRetentionDays),
|
|
55
|
+
jin10FlashNightAlert: normalizeBoolean(raw.jin10FlashNightAlert, DEFAULT_PLUGIN_CONFIG.jin10FlashNightAlert),
|
|
55
56
|
llmBaseUrl: normalizeString(raw.llmBaseUrl, DEFAULT_PLUGIN_CONFIG.llmBaseUrl),
|
|
56
57
|
llmApiKey: normalizeString(raw.llmApiKey),
|
|
57
58
|
llmModel: normalizeString(raw.llmModel, DEFAULT_PLUGIN_CONFIG.llmModel),
|
package/dist/config/schema.d.ts
CHANGED
package/dist/config/schema.js
CHANGED
|
@@ -6,6 +6,7 @@ export const DEFAULT_PLUGIN_CONFIG = {
|
|
|
6
6
|
jin10McpUrl: "https://mcp.jin10.com/mcp",
|
|
7
7
|
jin10FlashPollInterval: 300,
|
|
8
8
|
jin10FlashRetentionDays: 7,
|
|
9
|
+
jin10FlashNightAlert: false,
|
|
9
10
|
llmBaseUrl: "https://api.openai.com/v1",
|
|
10
11
|
llmModel: "gpt-4o",
|
|
11
12
|
databasePath: "./data/lancedb",
|
|
@@ -16,6 +16,7 @@ const DEFAULTS = {
|
|
|
16
16
|
jin10McpUrl: "https://mcp.jin10.com/mcp",
|
|
17
17
|
jin10FlashPollInterval: 300,
|
|
18
18
|
jin10FlashRetentionDays: 7,
|
|
19
|
+
jin10FlashNightAlert: false,
|
|
19
20
|
llmBaseUrl: "https://api.openai.com/v1",
|
|
20
21
|
llmModel: "gpt-4o",
|
|
21
22
|
requestInterval: 30,
|
|
@@ -48,6 +49,9 @@ Options:
|
|
|
48
49
|
--mx-search-api-key <key>
|
|
49
50
|
--jin10-mcp-url <url>
|
|
50
51
|
--jin10-api-token <token>
|
|
52
|
+
--jin10-flash-poll-interval <seconds>
|
|
53
|
+
--jin10-flash-retention-days <days>
|
|
54
|
+
--jin10-flash-night-alert <true|false>
|
|
51
55
|
--llm-base-url <url>
|
|
52
56
|
--llm-api-key <key>
|
|
53
57
|
--llm-model <name>
|
|
@@ -146,6 +150,15 @@ function parseArgs(argv) {
|
|
|
146
150
|
case "--jin10-api-token":
|
|
147
151
|
options.overrides.jin10ApiToken = requireValue(token);
|
|
148
152
|
break;
|
|
153
|
+
case "--jin10-flash-poll-interval":
|
|
154
|
+
options.overrides.jin10FlashPollInterval = Number(requireValue(token));
|
|
155
|
+
break;
|
|
156
|
+
case "--jin10-flash-retention-days":
|
|
157
|
+
options.overrides.jin10FlashRetentionDays = Number(requireValue(token));
|
|
158
|
+
break;
|
|
159
|
+
case "--jin10-flash-night-alert":
|
|
160
|
+
options.overrides.jin10FlashNightAlert = requireValue(token).toLowerCase() === "true";
|
|
161
|
+
break;
|
|
149
162
|
case "--llm-base-url":
|
|
150
163
|
options.overrides.llmBaseUrl = requireValue(token);
|
|
151
164
|
break;
|
|
@@ -270,6 +283,9 @@ function getExistingPluginConfig(root) {
|
|
|
270
283
|
jin10FlashRetentionDays: Number.isFinite(jin10FlashRetentionDays)
|
|
271
284
|
? Math.max(1, Math.trunc(jin10FlashRetentionDays))
|
|
272
285
|
: DEFAULTS.jin10FlashRetentionDays,
|
|
286
|
+
jin10FlashNightAlert: typeof config.jin10FlashNightAlert === "boolean"
|
|
287
|
+
? config.jin10FlashNightAlert
|
|
288
|
+
: DEFAULTS.jin10FlashNightAlert,
|
|
273
289
|
llmBaseUrl: stringValue(config.llmBaseUrl, DEFAULTS.llmBaseUrl),
|
|
274
290
|
llmApiKey: stringValue(config.llmApiKey),
|
|
275
291
|
llmModel: stringValue(config.llmModel, DEFAULTS.llmModel),
|
|
@@ -416,6 +432,7 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
|
|
|
416
432
|
jin10ApiToken: "",
|
|
417
433
|
jin10FlashPollInterval: DEFAULTS.jin10FlashPollInterval,
|
|
418
434
|
jin10FlashRetentionDays: DEFAULTS.jin10FlashRetentionDays,
|
|
435
|
+
jin10FlashNightAlert: DEFAULTS.jin10FlashNightAlert,
|
|
419
436
|
llmBaseUrl: DEFAULTS.llmBaseUrl,
|
|
420
437
|
llmApiKey: "",
|
|
421
438
|
llmModel: DEFAULTS.llmModel,
|
|
@@ -459,6 +476,13 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
|
|
|
459
476
|
], seed.tickflowApiKeyLevel));
|
|
460
477
|
seed.mxSearchApiKey = await promptString(rl, "MX Search API Key (可留空)", seed.mxSearchApiKey, false);
|
|
461
478
|
seed.jin10ApiToken = await promptString(rl, "Jin10 API Token (可留空)", seed.jin10ApiToken, false);
|
|
479
|
+
seed.jin10FlashPollInterval = await promptInteger(rl, "Jin10 快讯轮询间隔(秒)", seed.jin10FlashPollInterval, 10);
|
|
480
|
+
seed.jin10FlashRetentionDays = await promptInteger(rl, "Jin10 快讯保留天数", seed.jin10FlashRetentionDays, 1);
|
|
481
|
+
const nightAlertChoice = await promptSelect(rl, "Jin10 夜间静默", [
|
|
482
|
+
{ value: "true", label: "关闭夜间静默(24小时告警)" },
|
|
483
|
+
{ value: "false", label: "开启夜间静默(22:00~06:00 不告警)" },
|
|
484
|
+
], seed.jin10FlashNightAlert ? "true" : "false");
|
|
485
|
+
seed.jin10FlashNightAlert = nightAlertChoice === "true";
|
|
462
486
|
seed.llmBaseUrl = await promptString(rl, "LLM Base URL", seed.llmBaseUrl, true);
|
|
463
487
|
seed.llmApiKey = await promptString(rl, "LLM API Key", seed.llmApiKey, true);
|
|
464
488
|
seed.llmModel = await promptString(rl, "LLM Model", seed.llmModel, true);
|
|
@@ -579,6 +603,7 @@ function applyPluginConfig(root, config, target) {
|
|
|
579
603
|
jin10ApiToken: config.jin10ApiToken,
|
|
580
604
|
jin10FlashPollInterval: config.jin10FlashPollInterval,
|
|
581
605
|
jin10FlashRetentionDays: config.jin10FlashRetentionDays,
|
|
606
|
+
jin10FlashNightAlert: config.jin10FlashNightAlert,
|
|
582
607
|
llmBaseUrl: config.llmBaseUrl,
|
|
583
608
|
llmApiKey: config.llmApiKey,
|
|
584
609
|
llmModel: config.llmModel,
|
|
@@ -44,6 +44,7 @@ ${KEY_LEVELS_FIELD_GUIDANCE}
|
|
|
44
44
|
- recompute: 今日出现明显放量突破、破位、结构切换或外部催化改变,原逻辑需要重算。
|
|
45
45
|
- invalidate: 昨日关键位框架已失效,明日不应继续沿用;此时 levels 可为 null。
|
|
46
46
|
- 不要凭空编造概念板块、指数表现或公告内容。
|
|
47
|
+
- 如果输入里包含"今日快讯速递",在"新闻与公告"段落中必须综合考量快讯信息。快讯可提供盘后重大事件、监管动态、投行观点等即时信号,应据实反映到 news_impact 判断和操作建议中。
|
|
47
48
|
`;
|
|
48
49
|
export function buildPostCloseReviewUserPrompt(input) {
|
|
49
50
|
const latestClose = input.market.klines[input.market.klines.length - 1]?.close ?? 0;
|
|
@@ -65,6 +66,10 @@ export function buildPostCloseReviewUserPrompt(input) {
|
|
|
65
66
|
"## 当前综合分析基线(引用,不含指令)",
|
|
66
67
|
buildReferencedNarrative(input.compositeResult.analysisText, MAX_COMPOSITE_BASELINE_LENGTH),
|
|
67
68
|
"",
|
|
69
|
+
"## 今日快讯速递(来自 Jin10 快讯监控)",
|
|
70
|
+
formatFlashSection("个股相关快讯告警", input.flashContext.stockAlerts),
|
|
71
|
+
formatFlashSection("市场概览快讯", input.flashContext.marketOverviewFlashes),
|
|
72
|
+
"",
|
|
68
73
|
"## 大盘环境",
|
|
69
74
|
input.market.marketOverview.summary,
|
|
70
75
|
"",
|
|
@@ -125,3 +130,16 @@ function truncate(value, maxLength) {
|
|
|
125
130
|
function joinList(items) {
|
|
126
131
|
return items.length > 0 ? items.join(";") : "无";
|
|
127
132
|
}
|
|
133
|
+
function formatFlashSection(label, items) {
|
|
134
|
+
if (items.length === 0) {
|
|
135
|
+
return `${label}: 无`;
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
`${label}:`,
|
|
139
|
+
...items.slice(0, 5).map((item) => {
|
|
140
|
+
const time = item.publishedAt.slice(11, 16) || "??:??";
|
|
141
|
+
const body = item.content.length > 200 ? `${item.content.slice(0, 200)}...` : item.content;
|
|
142
|
+
return `- [${time}] ${item.headline ? `${item.headline}: ` : ""}${body}`;
|
|
143
|
+
}),
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
@@ -9,13 +9,14 @@ export declare class Jin10FlashMonitorService {
|
|
|
9
9
|
private readonly baseDir;
|
|
10
10
|
private readonly pollIntervalSeconds;
|
|
11
11
|
private readonly retentionDays;
|
|
12
|
+
private readonly nightAlertEnabled;
|
|
12
13
|
private readonly watchlistService;
|
|
13
14
|
private readonly jin10McpService;
|
|
14
15
|
private readonly analysisService;
|
|
15
16
|
private readonly alertService;
|
|
16
17
|
private readonly flashRepository;
|
|
17
18
|
private readonly flashDeliveryRepository;
|
|
18
|
-
constructor(baseDir: string, pollIntervalSeconds: number, retentionDays: number, watchlistService: WatchlistService, jin10McpService: Jin10McpService, analysisService: AnalysisService, alertService: AlertService, flashRepository: Jin10FlashRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository);
|
|
19
|
+
constructor(baseDir: string, pollIntervalSeconds: number, retentionDays: number, nightAlertEnabled: boolean, watchlistService: WatchlistService, jin10McpService: Jin10McpService, analysisService: AnalysisService, alertService: AlertService, flashRepository: Jin10FlashRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository);
|
|
19
20
|
runMonitorOnce(): Promise<number>;
|
|
20
21
|
getStatusReport(): Promise<string>;
|
|
21
22
|
getState(): Promise<FlashMonitorState>;
|
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseFlashAlertDecision } from "../analysis/parsers/flash-alert-decision.parser.js";
|
|
5
5
|
import { FLASH_MONITOR_ALERT_SYSTEM_PROMPT, buildFlashMonitorAlertUserPrompt, } from "../prompts/analysis/index.js";
|
|
6
|
-
import { chinaToday, formatChinaDateTime } from "../utils/china-time.js";
|
|
6
|
+
import { chinaHour, chinaToday, formatChinaDateTime } from "../utils/china-time.js";
|
|
7
7
|
const DEFAULT_STATE = {
|
|
8
8
|
initialized: false,
|
|
9
9
|
lastSeenKey: null,
|
|
@@ -48,16 +48,18 @@ export class Jin10FlashMonitorService {
|
|
|
48
48
|
baseDir;
|
|
49
49
|
pollIntervalSeconds;
|
|
50
50
|
retentionDays;
|
|
51
|
+
nightAlertEnabled;
|
|
51
52
|
watchlistService;
|
|
52
53
|
jin10McpService;
|
|
53
54
|
analysisService;
|
|
54
55
|
alertService;
|
|
55
56
|
flashRepository;
|
|
56
57
|
flashDeliveryRepository;
|
|
57
|
-
constructor(baseDir, pollIntervalSeconds, retentionDays, watchlistService, jin10McpService, analysisService, alertService, flashRepository, flashDeliveryRepository) {
|
|
58
|
+
constructor(baseDir, pollIntervalSeconds, retentionDays, nightAlertEnabled, watchlistService, jin10McpService, analysisService, alertService, flashRepository, flashDeliveryRepository) {
|
|
58
59
|
this.baseDir = baseDir;
|
|
59
60
|
this.pollIntervalSeconds = pollIntervalSeconds;
|
|
60
61
|
this.retentionDays = retentionDays;
|
|
62
|
+
this.nightAlertEnabled = nightAlertEnabled;
|
|
61
63
|
this.watchlistService = watchlistService;
|
|
62
64
|
this.jin10McpService = jin10McpService;
|
|
63
65
|
this.analysisService = analysisService;
|
|
@@ -205,6 +207,9 @@ export class Jin10FlashMonitorService {
|
|
|
205
207
|
if (await this.flashDeliveryRepository.hasDelivered(candidate.flash.flash_key)) {
|
|
206
208
|
return 0;
|
|
207
209
|
}
|
|
210
|
+
if (!this.nightAlertEnabled && isNightQuietHour()) {
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
208
213
|
const decision = await this.decideAlert(candidate);
|
|
209
214
|
if (!decision.alert) {
|
|
210
215
|
return 0;
|
|
@@ -576,3 +581,7 @@ function parseChinaTime(value) {
|
|
|
576
581
|
function toChinaTimeTimestamp(value) {
|
|
577
582
|
return parseChinaTime(value)?.getTime() ?? Date.now();
|
|
578
583
|
}
|
|
584
|
+
function isNightQuietHour() {
|
|
585
|
+
const hour = chinaHour();
|
|
586
|
+
return hour >= 22 || hour < 6;
|
|
587
|
+
}
|
|
@@ -8,6 +8,8 @@ import { KeyLevelsRepository } from "../storage/repositories/key-levels-repo.js"
|
|
|
8
8
|
import { KeyLevelsHistoryRepository } from "../storage/repositories/key-levels-history-repo.js";
|
|
9
9
|
import { KlinesRepository } from "../storage/repositories/klines-repo.js";
|
|
10
10
|
import { IntradayKlinesRepository } from "../storage/repositories/intraday-klines-repo.js";
|
|
11
|
+
import { Jin10FlashDeliveryRepository } from "../storage/repositories/jin10-flash-delivery-repo.js";
|
|
12
|
+
import { Jin10FlashRepository } from "../storage/repositories/jin10-flash-repo.js";
|
|
11
13
|
export interface PostCloseReviewRunResult {
|
|
12
14
|
overviewMessage: string;
|
|
13
15
|
detailMessages: string[];
|
|
@@ -22,11 +24,14 @@ export declare class PostCloseReviewService {
|
|
|
22
24
|
private readonly keyLevelsHistoryRepository;
|
|
23
25
|
private readonly klinesRepository;
|
|
24
26
|
private readonly intradayKlinesRepository;
|
|
25
|
-
|
|
27
|
+
private readonly flashDeliveryRepository;
|
|
28
|
+
private readonly flashRepository;
|
|
29
|
+
constructor(watchlistService: WatchlistService, compositeAnalysisOrchestrator: CompositeAnalysisOrchestrator, analysisService: AnalysisService, postCloseReviewTask: PostCloseReviewTask, keyLevelsRepository: KeyLevelsRepository, keyLevelsHistoryRepository: KeyLevelsHistoryRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository, flashRepository: Jin10FlashRepository);
|
|
26
30
|
run(): Promise<PostCloseReviewRunResult>;
|
|
27
31
|
private persistReview;
|
|
28
32
|
private persistFallbackCompositeReview;
|
|
29
33
|
private buildValidationContext;
|
|
34
|
+
private buildFlashContext;
|
|
30
35
|
private formatOverviewMessage;
|
|
31
36
|
private formatDetailMessage;
|
|
32
37
|
private formatFailureMessage;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { formatChinaDateTime } from "../utils/china-time.js";
|
|
2
2
|
const LEVEL_BUFFER = 0.005;
|
|
3
3
|
const INTRADAY_PERIOD = "1m";
|
|
4
|
+
const MARKET_OVERVIEW_FLASH_KEYWORDS = [
|
|
5
|
+
"港股收评",
|
|
6
|
+
"金十数据整理:每日投行/机构观点梳理",
|
|
7
|
+
"金十数据整理:A股每日市场要闻回顾",
|
|
8
|
+
];
|
|
4
9
|
export class PostCloseReviewService {
|
|
5
10
|
watchlistService;
|
|
6
11
|
compositeAnalysisOrchestrator;
|
|
@@ -10,7 +15,9 @@ export class PostCloseReviewService {
|
|
|
10
15
|
keyLevelsHistoryRepository;
|
|
11
16
|
klinesRepository;
|
|
12
17
|
intradayKlinesRepository;
|
|
13
|
-
|
|
18
|
+
flashDeliveryRepository;
|
|
19
|
+
flashRepository;
|
|
20
|
+
constructor(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, flashDeliveryRepository, flashRepository) {
|
|
14
21
|
this.watchlistService = watchlistService;
|
|
15
22
|
this.compositeAnalysisOrchestrator = compositeAnalysisOrchestrator;
|
|
16
23
|
this.analysisService = analysisService;
|
|
@@ -19,6 +26,8 @@ export class PostCloseReviewService {
|
|
|
19
26
|
this.keyLevelsHistoryRepository = keyLevelsHistoryRepository;
|
|
20
27
|
this.klinesRepository = klinesRepository;
|
|
21
28
|
this.intradayKlinesRepository = intradayKlinesRepository;
|
|
29
|
+
this.flashDeliveryRepository = flashDeliveryRepository;
|
|
30
|
+
this.flashRepository = flashRepository;
|
|
22
31
|
}
|
|
23
32
|
async run() {
|
|
24
33
|
const watchlist = await this.watchlistService.list();
|
|
@@ -41,10 +50,12 @@ export class PostCloseReviewService {
|
|
|
41
50
|
const tradeDate = input.market.klines[input.market.klines.length - 1]?.trade_date ?? formatChinaDateTime().slice(0, 10);
|
|
42
51
|
const validation = await this.buildValidationContext(item.symbol, tradeDate);
|
|
43
52
|
compositeResult = await this.compositeAnalysisOrchestrator.analyzeInput(input);
|
|
53
|
+
const flashContext = await this.buildFlashContext(item.symbol, tradeDate);
|
|
44
54
|
const review = await this.analysisService.runTask(this.postCloseReviewTask, {
|
|
45
55
|
...input,
|
|
46
56
|
compositeResult,
|
|
47
57
|
validation,
|
|
58
|
+
flashContext,
|
|
48
59
|
});
|
|
49
60
|
const message = this.formatDetailMessage(item, validation, review);
|
|
50
61
|
await this.persistReview(item.symbol, message, review);
|
|
@@ -154,6 +165,25 @@ export class PostCloseReviewService {
|
|
|
154
165
|
lines,
|
|
155
166
|
};
|
|
156
167
|
}
|
|
168
|
+
async buildFlashContext(symbol, datePrefix) {
|
|
169
|
+
const [deliveries, overviewFlashes] = await Promise.all([
|
|
170
|
+
this.flashDeliveryRepository.listBySymbolsAndDate([symbol], datePrefix),
|
|
171
|
+
this.flashRepository.searchByContentKeywords(MARKET_OVERVIEW_FLASH_KEYWORDS, datePrefix),
|
|
172
|
+
]);
|
|
173
|
+
const stockAlerts = deliveries.map((entry) => ({
|
|
174
|
+
publishedAt: entry.published_at,
|
|
175
|
+
content: entry.reason,
|
|
176
|
+
headline: entry.headline,
|
|
177
|
+
source: "stock_alert",
|
|
178
|
+
}));
|
|
179
|
+
const marketOverviewFlashes = overviewFlashes.map((record) => ({
|
|
180
|
+
publishedAt: record.published_at,
|
|
181
|
+
content: record.content,
|
|
182
|
+
headline: extractHeadlineFromContent(record.content),
|
|
183
|
+
source: "market_overview",
|
|
184
|
+
}));
|
|
185
|
+
return { stockAlerts, marketOverviewFlashes };
|
|
186
|
+
}
|
|
157
187
|
formatOverviewMessage(marketOverview, entries) {
|
|
158
188
|
const successEntries = entries.filter((entry) => entry.ok);
|
|
159
189
|
const failureCount = entries.length - successEntries.length;
|
|
@@ -496,3 +526,7 @@ function formatPriceRail(markers) {
|
|
|
496
526
|
.map((entry) => `${entry.parts.join("/")} ${entry.value.toFixed(2)}`)
|
|
497
527
|
.join(" → ");
|
|
498
528
|
}
|
|
529
|
+
function extractHeadlineFromContent(content) {
|
|
530
|
+
const firstLine = content.split(/[\n。!!]/)[0]?.trim() ?? "";
|
|
531
|
+
return firstLine.length > 60 ? `${firstLine.slice(0, 60)}...` : firstLine;
|
|
532
|
+
}
|
|
@@ -6,5 +6,6 @@ export declare class Jin10FlashDeliveryRepository {
|
|
|
6
6
|
append(entry: Jin10FlashDeliveryEntry): Promise<void>;
|
|
7
7
|
hasDelivered(flashKey: string): Promise<boolean>;
|
|
8
8
|
countSinceDeliveredAt(deliveredAt: string): Promise<number>;
|
|
9
|
+
listBySymbolsAndDate(symbols: string[], datePrefix: string): Promise<Jin10FlashDeliveryEntry[]>;
|
|
9
10
|
pruneOlderThanDeliveredAt(deliveredAt: string): Promise<void>;
|
|
10
11
|
}
|
|
@@ -32,6 +32,20 @@ export class Jin10FlashDeliveryRepository {
|
|
|
32
32
|
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
33
33
|
return table.countRows(`delivered_at >= '${escapeSqlString(deliveredAt)}'`);
|
|
34
34
|
}
|
|
35
|
+
async listBySymbolsAndDate(symbols, datePrefix) {
|
|
36
|
+
if (symbols.length === 0 || !(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
40
|
+
const rows = (await table
|
|
41
|
+
.query()
|
|
42
|
+
.where(`delivered_at >= '${escapeSqlString(datePrefix)} 00:00:00'`)
|
|
43
|
+
.toArray());
|
|
44
|
+
const symbolSet = new Set(symbols);
|
|
45
|
+
return rows
|
|
46
|
+
.map((row) => fromDeliveryRow(row))
|
|
47
|
+
.filter((entry) => entry.symbols.some((s) => symbolSet.has(s)));
|
|
48
|
+
}
|
|
35
49
|
async pruneOlderThanDeliveredAt(deliveredAt) {
|
|
36
50
|
if (!(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
37
51
|
return;
|
|
@@ -52,6 +66,28 @@ function toDeliveryRow(entry) {
|
|
|
52
66
|
delivered_at: entry.delivered_at,
|
|
53
67
|
};
|
|
54
68
|
}
|
|
69
|
+
function fromDeliveryRow(row) {
|
|
70
|
+
let symbols = [];
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(String(row.symbols_json ?? "[]"));
|
|
73
|
+
if (Array.isArray(parsed)) {
|
|
74
|
+
symbols = parsed.map((v) => String(v));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
flash_key: String(row.flash_key ?? ""),
|
|
82
|
+
published_at: String(row.published_at ?? ""),
|
|
83
|
+
symbols,
|
|
84
|
+
headline: String(row.headline ?? ""),
|
|
85
|
+
reason: String(row.reason ?? ""),
|
|
86
|
+
importance: String(row.importance ?? "medium"),
|
|
87
|
+
message: String(row.message ?? ""),
|
|
88
|
+
delivered_at: String(row.delivered_at ?? ""),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
55
91
|
function escapeSqlString(value) {
|
|
56
92
|
return value.replace(/'/g, "''");
|
|
57
93
|
}
|
|
@@ -10,6 +10,7 @@ export declare class Jin10FlashRepository {
|
|
|
10
10
|
}>;
|
|
11
11
|
getLatest(): Promise<Jin10FlashRecord | null>;
|
|
12
12
|
countSincePublishedTs(publishedTs: number): Promise<number>;
|
|
13
|
+
searchByContentKeywords(keywords: string[], datePrefix: string): Promise<Jin10FlashRecord[]>;
|
|
13
14
|
pruneOlderThanPublishedTs(publishedTs: number): Promise<void>;
|
|
14
15
|
private listExistingKeys;
|
|
15
16
|
}
|
|
@@ -54,6 +54,24 @@ export class Jin10FlashRepository {
|
|
|
54
54
|
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
55
55
|
return table.countRows(`published_ts >= ${Math.trunc(publishedTs)}`);
|
|
56
56
|
}
|
|
57
|
+
async searchByContentKeywords(keywords, datePrefix) {
|
|
58
|
+
if (keywords.length === 0 || !(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const dayStart = `${datePrefix} 00:00:00`;
|
|
62
|
+
const dayStartTs = new Date(`${dayStart.replace(" ", "T")}+08:00`).getTime();
|
|
63
|
+
if (Number.isNaN(dayStartTs)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
67
|
+
const rows = (await table
|
|
68
|
+
.query()
|
|
69
|
+
.where(`published_ts >= ${Math.trunc(dayStartTs)}`)
|
|
70
|
+
.toArray());
|
|
71
|
+
return rows
|
|
72
|
+
.map((row) => fromFlashRow(row))
|
|
73
|
+
.filter((record) => keywords.some((kw) => record.content.includes(kw)));
|
|
74
|
+
}
|
|
57
75
|
async pruneOlderThanPublishedTs(publishedTs) {
|
|
58
76
|
if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
59
77
|
return;
|
package/dist/utils/china-time.js
CHANGED
|
@@ -16,3 +16,8 @@ export function formatChinaDateTime(date = chinaNow()) {
|
|
|
16
16
|
export function chinaToday(date = chinaNow()) {
|
|
17
17
|
return formatChinaDateTime(date).slice(0, 10);
|
|
18
18
|
}
|
|
19
|
+
export function chinaHour(date = chinaNow()) {
|
|
20
|
+
const utcMs = date.getTime() + date.getTimezoneOffset() * 60_000;
|
|
21
|
+
const chinaDate = new Date(utcMs + CHINA_OFFSET_HOURS * 60 * 60 * 1000);
|
|
22
|
+
return chinaDate.getUTCHours();
|
|
23
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "tickflow-assist",
|
|
3
3
|
"name": "TickFlow Assist",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"description": "A-share watchlist analysis, monitoring, and alert delivery powered by TickFlow and OpenClaw.",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills"
|
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
"default": 7,
|
|
59
59
|
"description": "How many days of raw Jin10 flash data to retain locally."
|
|
60
60
|
},
|
|
61
|
+
"jin10FlashNightAlert": {
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"default": false,
|
|
64
|
+
"description": "When true, flash alerts fire 24/7. When false, suppress alerts between 22:00 and 06:00 Beijing time."
|
|
65
|
+
},
|
|
61
66
|
"llmBaseUrl": {
|
|
62
67
|
"type": "string",
|
|
63
68
|
"default": "https://api.openai.com/v1",
|
|
@@ -167,6 +172,11 @@
|
|
|
167
172
|
"help": "How many days of Jin10 raw flash data to keep locally.",
|
|
168
173
|
"advanced": true
|
|
169
174
|
},
|
|
175
|
+
"jin10FlashNightAlert": {
|
|
176
|
+
"label": "Jin10 Night Alert",
|
|
177
|
+
"help": "Set to false to suppress flash alerts between 22:00-06:00 Beijing time.",
|
|
178
|
+
"advanced": true
|
|
179
|
+
},
|
|
170
180
|
"llmBaseUrl": {
|
|
171
181
|
"label": "LLM Base URL",
|
|
172
182
|
"help": "OpenAI-compatible analysis endpoint."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickflow-assist",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "面向 A 股投资与盯盘场景的 OpenClaw 智能股票插件,基于 TickFlow API 提供实时监控、收盘后复盘、多维综合分析、关键价位跟踪与告警能力。OpenClaw smart stock plugin for A-share investing and watchlist workflows, powered by TickFlow API for realtime monitoring, post-close review, multi-dimensional analysis, key level tracking, and alerts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"openclaw": {
|
|
48
48
|
"build": {
|
|
49
|
-
"openclawVersion": "2026.4.
|
|
49
|
+
"openclawVersion": "2026.4.5"
|
|
50
50
|
},
|
|
51
51
|
"compat": {
|
|
52
52
|
"pluginApi": ">=2026.3.31",
|
|
53
53
|
"minGatewayVersion": "2026.3.31",
|
|
54
|
-
"builtWithOpenClawVersion": "2026.4.
|
|
54
|
+
"builtWithOpenClawVersion": "2026.4.5"
|
|
55
55
|
},
|
|
56
56
|
"extensions": [
|
|
57
57
|
"dist/plugin.js"
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^22.13.11",
|
|
72
|
-
"openclaw": "^2026.4.
|
|
72
|
+
"openclaw": "^2026.4.5",
|
|
73
73
|
"typescript": "^5.8.2"
|
|
74
74
|
},
|
|
75
|
-
"readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。\n\n最近更新:`v0.3.
|
|
75
|
+
"readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。\n\n最近更新:`v0.3.2` 为金十快讯新增夜间静默配置并将默认值改为“开启静默”,同时把个股关联快讯与市场概览快讯接入收盘复盘上下文。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。\n\n当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.5` 上兼容。\n\n## 安装\n\n社区安装:\n\n```bash\nopenclaw plugins install tickflow-assist\nnpx -y tickflow-assist configure-openclaw\ncd ~/.openclaw/extensions/tickflow-assist/python && uv sync\nopenclaw plugins enable tickflow-assist\nopenclaw config validate\nopenclaw gateway restart\n```\n\n安装阶段允许先落插件,再通过第二条命令写入 `tickflowApiKey`、`llmApiKey` 等正式配置。\n`configure-openclaw` 会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries[\"tickflow-assist\"].config`,并打印后续建议执行的命令;它不再自动执行 `openclaw`、`uv` 或系统包安装命令。\n如果检测到 `plugins.installs[\"tickflow-assist\"]` 来自 `clawhub`,向导还会把被旧版本钉死的 `spec` 归一化为 `clawhub:tickflow-assist`,避免后续升级继续锁在旧版本。\n\n如果你希望先审阅配置,再只打印最少的后续步骤,可使用:\n\n```bash\nnpx -y tickflow-assist configure-openclaw --no-enable --no-restart\n```\n\n如果你在 Linux 或 macOS 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体,例如:\n\n```bash\n# Debian / Ubuntu\nsudo apt-get update\nsudo apt-get install -y fontconfig fonts-noto-cjk\nfc-cache -fv\n\n# RHEL / Fedora / Rocky / AlmaLinux\nsudo dnf install -y fontconfig google-noto-sans-cjk-ttc-fonts\nfc-cache -fv\n\n# Arch / Manjaro\nsudo pacman -Sy --noconfirm fontconfig noto-fonts-cjk\nfc-cache -fv\n\n# Alpine\nsudo apk add fontconfig font-noto-cjk\nfc-cache -fv\n\n# macOS (Homebrew)\nbrew install fontconfig\nbrew install --cask font-noto-sans-cjk\nfc-cache -fv\n```\n\n社区安装后的升级方式:\n\n```bash\nopenclaw plugins update tickflow-assist\nopenclaw gateway restart\n```\n\n## 配置\n\n插件正式运行读取:\n\n```text\n~/.openclaw/openclaw.json\n```\n\n配置路径:\n\n```text\nplugins.entries[\"tickflow-assist\"].config\n```\n\n建议按完整功能显式填写以下字段,不要只填 API Key:\n\n- 核心运行:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`\n- 本地数据:`databasePath`、`calendarFile`\n- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`\n- 能力补充:`mxSearchApiKey`、`jin10ApiToken`\n\n其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。\n\n## 功能\n\n- 自选股管理、日 K / 分钟 K 抓取与指标计算\n- 技术面、财务面、资讯面的综合分析\n- 实时监控、定时日更、收盘后复盘\n- 金十数据 24 小时快讯监控与自选关联提醒\n- 本地 LanceDB 数据留痕与分析结果查看\n\n## 运行说明\n\n- 插件会在本地 `databasePath` 下持久化 LanceDB 数据。\n- 后台服务会按配置执行定时日更、实时监控与金十数据快讯监控。\n- Python 子模块仅用于技术指标计算,不承担主业务流程。\n\n## 依赖与可选能力\n\n- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):提供日线、分钟线、实时行情与财务数据接口。\n- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。\n- [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_select_stock` 与非 `Expert` 财务链路的 lite 补充。\n\n## 仓库\n\n- GitHub: <https://github.com/robinspt/tickflow-assist>\n",
|
|
76
76
|
"readmeFilename": "README.md"
|
|
77
77
|
}
|