tickflow-assist 0.3.6 → 0.3.8
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 +11 -42
- package/dist/analysis/types/composite-analysis.d.ts +27 -0
- package/dist/background/realtime-monitor.worker.d.ts +1 -1
- package/dist/background/realtime-monitor.worker.js +3 -4
- package/dist/bootstrap.js +24 -4
- package/dist/config/tickflow-access.d.ts +2 -1
- package/dist/config/tickflow-access.js +10 -3
- package/dist/dev/run-monitor-loop.js +0 -1
- package/dist/dev/tickflow-assist-cli.js +4 -3
- package/dist/dev/validate-mx-search.js +10 -2
- package/dist/plugin-commands.js +27 -0
- package/dist/plugin.js +4 -6
- package/dist/prompts/analysis/kline-analysis-user-prompt.js +2 -1
- package/dist/prompts/analysis/post-close-review-user-prompt.js +40 -1
- package/dist/prompts/analysis/pre-market-brief-prompt.d.ts +3 -1
- package/dist/prompts/analysis/pre-market-brief-prompt.js +8 -3
- package/dist/services/industry-peer-service.d.ts +9 -0
- package/dist/services/industry-peer-service.js +152 -0
- package/dist/services/jin10-flash-monitor-service.js +2 -1
- package/dist/services/monitor-service.d.ts +1 -1
- package/dist/services/monitor-service.js +21 -26
- package/dist/services/mx-search-service.d.ts +8 -1
- package/dist/services/mx-search-service.js +400 -10
- package/dist/services/post-close-review-service.d.ts +11 -4
- package/dist/services/post-close-review-service.js +113 -10
- package/dist/services/pre-market-brief-service.js +500 -42
- package/dist/services/tickflow-client.d.ts +4 -1
- package/dist/services/tickflow-client.js +32 -0
- package/dist/services/tickflow-universe-service.d.ts +26 -0
- package/dist/services/tickflow-universe-service.js +213 -0
- package/dist/services/watchlist-profile-service.d.ts +4 -1
- package/dist/services/watchlist-profile-service.js +58 -29
- package/dist/services/watchlist-service.d.ts +5 -1
- package/dist/services/watchlist-service.js +9 -4
- package/dist/storage/repositories/universe-membership-repo.d.ts +11 -0
- package/dist/storage/repositories/universe-membership-repo.js +38 -0
- package/dist/storage/repositories/universe-repo.d.ts +17 -0
- package/dist/storage/repositories/universe-repo.js +62 -0
- package/dist/storage/schemas.d.ts +2 -0
- package/dist/storage/schemas.js +13 -0
- package/dist/tools/add-stock.tool.d.ts +2 -1
- package/dist/tools/add-stock.tool.js +10 -1
- package/dist/tools/eastmoney-watchlist.tool.d.ts +31 -0
- package/dist/tools/eastmoney-watchlist.tool.js +294 -0
- package/dist/tools/mx-data.tool.d.ts +8 -0
- package/dist/tools/mx-data.tool.js +94 -0
- package/dist/tools/mx-select-stock.tool.js +6 -2
- package/dist/tools/query-database.tool.js +6 -0
- package/dist/tools/refresh-watchlist-profiles.tool.d.ts +2 -1
- package/dist/tools/refresh-watchlist-profiles.tool.js +11 -1
- package/dist/tools/screen-stock-candidates.tool.d.ts +34 -0
- package/dist/tools/screen-stock-candidates.tool.js +477 -0
- package/dist/tools/test-alert.tool.js +56 -19
- package/dist/types/mx-data.d.ts +23 -0
- package/dist/types/mx-data.js +1 -0
- package/dist/types/mx-select-stock.d.ts +1 -0
- package/dist/types/mx-self-select.d.ts +30 -0
- package/dist/types/mx-self-select.js +1 -0
- package/dist/types/tickflow.d.ts +12 -0
- package/dist/utils/tickflow-quote.d.ts +5 -0
- package/dist/utils/tickflow-quote.js +31 -0
- package/openclaw.plugin.json +83 -6
- package/package.json +6 -6
- package/skills/stock-analysis/SKILL.md +39 -20
- package/skills/usage-help/SKILL.md +33 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { PRE_MARKET_BRIEF_SYSTEM_PROMPT, buildPreMarketBriefUserPrompt, } from "../prompts/analysis/index.js";
|
|
3
3
|
import { formatChinaDateTime } from "../utils/china-time.js";
|
|
4
|
+
import { extractSectorKeywords } from "./watchlist-profile-service.js";
|
|
4
5
|
const PRE_MARKET_BRIEF_KEYWORD = "金十数据整理";
|
|
5
6
|
const PRE_MARKET_READY_TIME = "09:20:00";
|
|
6
7
|
const PRE_MARKET_SYNC_MAX_PAGES = 12;
|
|
@@ -32,6 +33,76 @@ const RISK_KEYWORDS = [
|
|
|
32
33
|
"制裁",
|
|
33
34
|
"关税",
|
|
34
35
|
];
|
|
36
|
+
const FALLBACK_MAJOR_NEWS_LIMIT = 5;
|
|
37
|
+
const FALLBACK_TOPIC_KEY_POINT_LIMIT = 6;
|
|
38
|
+
const TOPIC_RULES = [
|
|
39
|
+
{
|
|
40
|
+
category: "地缘与能源",
|
|
41
|
+
keywords: [
|
|
42
|
+
"伊朗",
|
|
43
|
+
"以色列",
|
|
44
|
+
"中东",
|
|
45
|
+
"海湾",
|
|
46
|
+
"霍尔木兹",
|
|
47
|
+
"俄乌",
|
|
48
|
+
"俄罗斯",
|
|
49
|
+
"乌克兰",
|
|
50
|
+
"原油",
|
|
51
|
+
"石油",
|
|
52
|
+
"油轮",
|
|
53
|
+
"EIA",
|
|
54
|
+
"能源",
|
|
55
|
+
"侵略",
|
|
56
|
+
"制裁",
|
|
57
|
+
"开战",
|
|
58
|
+
],
|
|
59
|
+
score: 8,
|
|
60
|
+
macroImplication: "地缘风险与能源价格预期可能影响开盘风险偏好",
|
|
61
|
+
riskImplication: "海外扰动容易传导到原油、航运、防务及周期品情绪",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
category: "科技产业",
|
|
65
|
+
keywords: [
|
|
66
|
+
"AI",
|
|
67
|
+
"人工智能",
|
|
68
|
+
"大模型",
|
|
69
|
+
"算力",
|
|
70
|
+
"芯片",
|
|
71
|
+
"半导体",
|
|
72
|
+
"台积电",
|
|
73
|
+
"量子",
|
|
74
|
+
"机器人",
|
|
75
|
+
"开源",
|
|
76
|
+
"3D生成",
|
|
77
|
+
],
|
|
78
|
+
score: 7,
|
|
79
|
+
macroImplication: "科技成长方向的产业催化可能影响题材活跃度",
|
|
80
|
+
opportunityImplication: "产业链、算力、应用和设备端是否出现扩散",
|
|
81
|
+
riskImplication: "高热度题材若只停留在消息刺激,开盘后容易高开分化",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
category: "政策与支付互联",
|
|
85
|
+
keywords: ["政策", "微信支付", "二维码", "跨境", "互联互通", "试点", "监管", "关税"],
|
|
86
|
+
score: 6,
|
|
87
|
+
macroImplication: "政策或跨境互联线索可能改变相关行业预期",
|
|
88
|
+
opportunityImplication: "支付、出海、消费场景和平台生态是否获得资金确认",
|
|
89
|
+
riskImplication: "监管或外部政策变化可能压制相关板块估值与情绪",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
category: "产业与订单",
|
|
93
|
+
keywords: ["订单", "中标", "业绩", "回购", "增持", "涨价", "并购", "并购重组", "发布", "恢复"],
|
|
94
|
+
score: 5,
|
|
95
|
+
macroImplication: "产业事件或公司行为可能提供结构性交易线索",
|
|
96
|
+
opportunityImplication: "订单、业绩、价格和资本运作线索是否带来板块联动",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
category: "资金与宏观",
|
|
100
|
+
keywords: ["美联储", "利率", "美元", "人民币", "股市", "出口", "进口", "产量", "库存"],
|
|
101
|
+
score: 5,
|
|
102
|
+
macroImplication: "宏观和资金面变化可能影响开盘定价与风险偏好",
|
|
103
|
+
riskImplication: "宏观数据变化若与市场预期背离,可能造成高开低走或避险交易",
|
|
104
|
+
},
|
|
105
|
+
];
|
|
35
106
|
export class PreMarketBriefService {
|
|
36
107
|
watchlistService;
|
|
37
108
|
jin10McpService;
|
|
@@ -80,10 +151,7 @@ export class PreMarketBriefService {
|
|
|
80
151
|
matchedWatchlistCount: 0,
|
|
81
152
|
};
|
|
82
153
|
}
|
|
83
|
-
const matchContexts = flashes.map((flash) => (
|
|
84
|
-
flash,
|
|
85
|
-
matchedItems: findMatchedItems(flash, watchlist),
|
|
86
|
-
}));
|
|
154
|
+
const matchContexts = flashes.map((flash) => buildFlashMatchContext(flash, watchlist));
|
|
87
155
|
const matchedWatchlistCount = new Set(matchContexts.flatMap((context) => context.matchedItems.map((item) => item.symbol))).size;
|
|
88
156
|
const summary = await this.buildSummary(window, watchlist, matchContexts);
|
|
89
157
|
return {
|
|
@@ -128,7 +196,9 @@ export class PreMarketBriefService {
|
|
|
128
196
|
watchlist,
|
|
129
197
|
flashes: matchContexts.map((context) => ({
|
|
130
198
|
publishedAt: context.flash.published_at,
|
|
131
|
-
headline:
|
|
199
|
+
headline: context.headline,
|
|
200
|
+
summary: context.summary,
|
|
201
|
+
keyPoints: context.keyPoints,
|
|
132
202
|
content: context.flash.content,
|
|
133
203
|
url: context.flash.url,
|
|
134
204
|
matchedSymbols: context.matchedItems.map((item) => item.symbol),
|
|
@@ -136,10 +206,13 @@ export class PreMarketBriefService {
|
|
|
136
206
|
};
|
|
137
207
|
if (this.analysisService.isConfigured()) {
|
|
138
208
|
try {
|
|
139
|
-
|
|
209
|
+
const generated = await this.analysisService.generateText(PRE_MARKET_BRIEF_SYSTEM_PROMPT, buildPreMarketBriefUserPrompt(promptInput), {
|
|
140
210
|
maxTokens: 1600,
|
|
141
211
|
temperature: 0.2,
|
|
142
212
|
});
|
|
213
|
+
if (!isLowSignalSummary(generated, matchContexts)) {
|
|
214
|
+
return generated;
|
|
215
|
+
}
|
|
143
216
|
}
|
|
144
217
|
catch {
|
|
145
218
|
// Fall through to deterministic fallback so the scheduled push still lands.
|
|
@@ -182,74 +255,305 @@ function matchesPreMarketBrief(record) {
|
|
|
182
255
|
function findMatchedItems(flash, watchlist) {
|
|
183
256
|
const normalizedContent = normalizeText(flash.content);
|
|
184
257
|
return watchlist.filter((item) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.map((keyword) => keyword.replace(/\s+/g, "").trim())
|
|
188
|
-
.filter((keyword) => keyword.length >= 2);
|
|
189
|
-
return [...directKeywords, ...boardKeywords]
|
|
190
|
-
.map((keyword) => normalizeText(keyword))
|
|
191
|
-
.some((keyword) => keyword && normalizedContent.includes(keyword));
|
|
258
|
+
return buildWatchlistKeywordEntries(item)
|
|
259
|
+
.some((entry) => normalizedContent.includes(entry.normalized));
|
|
192
260
|
});
|
|
193
261
|
}
|
|
262
|
+
function buildFlashMatchContext(flash, watchlist) {
|
|
263
|
+
const insight = extractFlashInsight(flash.content);
|
|
264
|
+
return {
|
|
265
|
+
flash,
|
|
266
|
+
matchedItems: findMatchedItems(flash, watchlist),
|
|
267
|
+
headline: insight.headline,
|
|
268
|
+
summary: insight.summary,
|
|
269
|
+
keyPoints: insight.keyPoints,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
194
272
|
function buildFallbackSummary(matchContexts) {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
273
|
+
const topics = buildFlashTopics(matchContexts);
|
|
274
|
+
const opportunityTopics = topics.filter(isOpportunityTopic);
|
|
275
|
+
const riskTopics = topics.filter(isRiskTopic);
|
|
197
276
|
return [
|
|
198
277
|
formatSectionTitle("🧭", "重大要闻"),
|
|
199
|
-
|
|
278
|
+
formatMajorNewsBullets(topics),
|
|
200
279
|
"",
|
|
201
280
|
formatSectionTitle("🎯", "自选相关"),
|
|
202
|
-
formatMatchedBullets(matchContexts, 5),
|
|
281
|
+
formatMatchedBullets(matchContexts, topics, 5),
|
|
203
282
|
"",
|
|
204
283
|
formatSectionTitle("💡", "潜在机会"),
|
|
205
|
-
|
|
206
|
-
?
|
|
284
|
+
opportunityTopics.length > 0
|
|
285
|
+
? formatOpportunityBullets(opportunityTopics, 4)
|
|
207
286
|
: "• 未发现基于当前整理快讯可直接确认的新增机会方向。",
|
|
208
287
|
"",
|
|
209
288
|
formatSectionTitle("⚠️", "风险提示"),
|
|
210
|
-
|
|
211
|
-
?
|
|
289
|
+
riskTopics.length > 0
|
|
290
|
+
? formatRiskBullets(riskTopics, 4)
|
|
212
291
|
: "• 当前整理快讯中未发现特别突出的新增风险,但仍需留意开盘后的情绪变化。",
|
|
213
292
|
"",
|
|
214
293
|
formatSectionTitle("📌", "开盘前关注清单"),
|
|
215
|
-
buildFocusBullets(matchContexts),
|
|
294
|
+
buildFocusBullets(topics, matchContexts),
|
|
216
295
|
].join("\n");
|
|
217
296
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
297
|
+
function buildFlashTopics(contexts) {
|
|
298
|
+
const seen = new Set();
|
|
299
|
+
const topics = [];
|
|
300
|
+
for (const context of contexts) {
|
|
301
|
+
const texts = context.keyPoints.length > 0
|
|
302
|
+
? context.keyPoints
|
|
303
|
+
: [formatContextSummary(context)];
|
|
304
|
+
const limitedTexts = texts.slice(0, FALLBACK_TOPIC_KEY_POINT_LIMIT);
|
|
305
|
+
for (const text of limitedTexts) {
|
|
306
|
+
const normalizedText = normalizeDigestText(text);
|
|
307
|
+
if (!normalizedText || seen.has(normalizedText)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
seen.add(normalizedText);
|
|
311
|
+
const rule = inferTopicRule(`${context.headline} ${text}`);
|
|
312
|
+
topics.push({
|
|
313
|
+
context,
|
|
314
|
+
text,
|
|
315
|
+
time: context.flash.published_at.slice(11, 16),
|
|
316
|
+
rule,
|
|
317
|
+
score: scoreTopic(text, context, rule),
|
|
318
|
+
matchedItems: context.matchedItems.filter((item) => topicMatchesWatchlistItem(text, item)),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (topics.length > 0) {
|
|
323
|
+
return topics;
|
|
324
|
+
}
|
|
325
|
+
return contexts.map((context) => {
|
|
326
|
+
const text = formatContextSummary(context);
|
|
327
|
+
const rule = inferTopicRule(`${context.headline} ${text}`);
|
|
328
|
+
return {
|
|
329
|
+
context,
|
|
330
|
+
text,
|
|
331
|
+
time: context.flash.published_at.slice(11, 16),
|
|
332
|
+
rule,
|
|
333
|
+
score: scoreTopic(text, context, rule),
|
|
334
|
+
matchedItems: context.matchedItems,
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function formatMajorNewsBullets(topics) {
|
|
339
|
+
const selected = selectDiverseTopics(topics, FALLBACK_MAJOR_NEWS_LIMIT);
|
|
340
|
+
if (selected.length === 0) {
|
|
341
|
+
return "• 未提取到足够正文细节,今日重大要闻暂只能按标题级线索观察。";
|
|
342
|
+
}
|
|
343
|
+
return selected
|
|
344
|
+
.map((topic) => `• [${topic.time}] ${topic.rule.category}: ${topic.text};${topic.rule.macroImplication}。`)
|
|
345
|
+
.join("\n");
|
|
346
|
+
}
|
|
347
|
+
function formatOpportunityBullets(topics, limit) {
|
|
348
|
+
return selectDiverseTopics(topics, limit)
|
|
349
|
+
.map((topic) => {
|
|
350
|
+
const implication = topic.rule.opportunityImplication ?? "相关方向是否获得资金确认";
|
|
351
|
+
return `• [${topic.time}] ${topic.rule.category}: ${topic.text};观察${implication}。`;
|
|
352
|
+
})
|
|
353
|
+
.join("\n");
|
|
354
|
+
}
|
|
355
|
+
function formatRiskBullets(topics, limit) {
|
|
356
|
+
return selectDiverseTopics(topics, limit)
|
|
357
|
+
.map((topic) => {
|
|
358
|
+
const implication = topic.rule.riskImplication ?? "消息不确定性对开盘情绪的扰动";
|
|
359
|
+
return `• [${topic.time}] ${topic.rule.category}: ${topic.text};风险点在于${implication}。`;
|
|
224
360
|
})
|
|
225
361
|
.join("\n");
|
|
226
362
|
}
|
|
227
|
-
function formatMatchedBullets(contexts, limit) {
|
|
228
|
-
const
|
|
229
|
-
|
|
363
|
+
function formatMatchedBullets(contexts, topics, limit) {
|
|
364
|
+
const matchedBySymbol = new Map();
|
|
365
|
+
for (const topic of topics) {
|
|
366
|
+
for (const item of topic.matchedItems) {
|
|
367
|
+
addWatchlistCue(matchedBySymbol, item, {
|
|
368
|
+
text: topic.text,
|
|
369
|
+
reason: describeWatchlistMatch(item, topic.text),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const context of contexts) {
|
|
374
|
+
for (const item of context.matchedItems) {
|
|
375
|
+
if (matchedBySymbol.has(item.symbol)) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const cue = findBestWatchlistCue(context, item);
|
|
379
|
+
addWatchlistCue(matchedBySymbol, item, {
|
|
380
|
+
text: cue,
|
|
381
|
+
reason: describeWatchlistMatch(item, cue),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const entries = [...matchedBySymbol.values()].slice(0, limit);
|
|
386
|
+
if (entries.length === 0) {
|
|
230
387
|
return "• 未发现直接命中自选股、行业或题材的盘前整理快讯。";
|
|
231
388
|
}
|
|
232
|
-
return
|
|
233
|
-
const
|
|
234
|
-
|
|
389
|
+
return entries.map(({ item, cues }) => {
|
|
390
|
+
const text = cues
|
|
391
|
+
.slice(0, 2)
|
|
392
|
+
.map((cue) => `${cue.reason}: ${cue.text}`)
|
|
393
|
+
.join(";");
|
|
394
|
+
return `• ${item.name}(${item.symbol}): ${text}`;
|
|
235
395
|
}).join("\n");
|
|
236
396
|
}
|
|
237
|
-
function buildFocusBullets(contexts) {
|
|
397
|
+
function buildFocusBullets(topics, contexts) {
|
|
238
398
|
const bullets = [];
|
|
239
|
-
const
|
|
240
|
-
for (const
|
|
241
|
-
const labels =
|
|
242
|
-
bullets
|
|
399
|
+
const matchedTopics = topics.filter((topic) => topic.matchedItems.length > 0);
|
|
400
|
+
for (const topic of selectDiverseTopics(matchedTopics, 2)) {
|
|
401
|
+
const labels = topic.matchedItems.map((item) => item.name).join("、");
|
|
402
|
+
addUniqueBullet(bullets, `• 关注 ${labels} 与“${formatFocusCueText(topic.text)}”的联动强度,开盘看竞价、放量承接和回落后的资金回流。`);
|
|
403
|
+
}
|
|
404
|
+
for (const topic of selectDiverseTopics(topics.filter(isRiskTopic), 2)) {
|
|
405
|
+
addUniqueBullet(bullets, `• 观察 ${topic.rule.category} 是否压制风险偏好,重点看“${formatFocusCueText(topic.text)}”有无后续快讯确认。`);
|
|
406
|
+
}
|
|
407
|
+
for (const topic of selectDiverseTopics(topics.filter(isOpportunityTopic), 2)) {
|
|
408
|
+
addUniqueBullet(bullets, `• 观察 ${topic.rule.category} 主题是否扩散,重点看“${formatFocusCueText(topic.text)}”对应方向高开后的承接而非单点冲高。`);
|
|
243
409
|
}
|
|
244
410
|
if (bullets.length < 3) {
|
|
245
|
-
for (const context of contexts
|
|
246
|
-
bullets
|
|
411
|
+
for (const context of contexts) {
|
|
412
|
+
addUniqueBullet(bullets, `• 复核“${formatFocusCue(context)}”是否有后续消息或竞价强化,避免仅按标题级线索追高。`);
|
|
413
|
+
if (bullets.length >= 3) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
247
416
|
}
|
|
248
417
|
}
|
|
249
418
|
return bullets.slice(0, 5).join("\n");
|
|
250
419
|
}
|
|
420
|
+
function selectDiverseTopics(topics, limit) {
|
|
421
|
+
const sorted = [...topics].sort(compareTopicsByImportance);
|
|
422
|
+
const selected = [];
|
|
423
|
+
const selectedCategories = new Set();
|
|
424
|
+
const selectedTexts = new Set();
|
|
425
|
+
for (const topic of sorted) {
|
|
426
|
+
if (selected.length >= limit) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const normalizedText = normalizeDigestText(topic.text);
|
|
430
|
+
if (selectedTexts.has(normalizedText) || selectedCategories.has(topic.rule.category)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
selected.push(topic);
|
|
434
|
+
selectedTexts.add(normalizedText);
|
|
435
|
+
selectedCategories.add(topic.rule.category);
|
|
436
|
+
}
|
|
437
|
+
for (const topic of sorted) {
|
|
438
|
+
if (selected.length >= limit) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
const normalizedText = normalizeDigestText(topic.text);
|
|
442
|
+
if (selectedTexts.has(normalizedText)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
selected.push(topic);
|
|
446
|
+
selectedTexts.add(normalizedText);
|
|
447
|
+
}
|
|
448
|
+
return selected.sort((left, right) => left.context.flash.published_ts - right.context.flash.published_ts);
|
|
449
|
+
}
|
|
450
|
+
function compareTopicsByImportance(left, right) {
|
|
451
|
+
return right.score - left.score || left.context.flash.published_ts - right.context.flash.published_ts;
|
|
452
|
+
}
|
|
453
|
+
function inferTopicRule(text) {
|
|
454
|
+
return TOPIC_RULES
|
|
455
|
+
.filter((rule) => containsAnyKeyword(text, rule.keywords))
|
|
456
|
+
.sort((left, right) => right.score - left.score)[0]
|
|
457
|
+
?? {
|
|
458
|
+
category: "市场情绪",
|
|
459
|
+
keywords: [],
|
|
460
|
+
score: 3,
|
|
461
|
+
macroImplication: "该线索可能影响局部题材情绪,需结合竞价强弱确认",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function scoreTopic(text, context, rule) {
|
|
465
|
+
let score = rule.score;
|
|
466
|
+
if (containsAnyKeyword(text, OPPORTUNITY_KEYWORDS)) {
|
|
467
|
+
score += 2;
|
|
468
|
+
}
|
|
469
|
+
if (containsAnyKeyword(text, RISK_KEYWORDS)) {
|
|
470
|
+
score += 2;
|
|
471
|
+
}
|
|
472
|
+
if (context.matchedItems.length > 0) {
|
|
473
|
+
score += 1;
|
|
474
|
+
}
|
|
475
|
+
if (text.length >= 18) {
|
|
476
|
+
score += 1;
|
|
477
|
+
}
|
|
478
|
+
return score;
|
|
479
|
+
}
|
|
480
|
+
function isOpportunityTopic(topic) {
|
|
481
|
+
return Boolean(topic.rule.opportunityImplication) || containsAnyKeyword(topic.text, OPPORTUNITY_KEYWORDS);
|
|
482
|
+
}
|
|
483
|
+
function isRiskTopic(topic) {
|
|
484
|
+
return Boolean(topic.rule.riskImplication) || containsAnyKeyword(topic.text, RISK_KEYWORDS);
|
|
485
|
+
}
|
|
486
|
+
function addWatchlistCue(matchedBySymbol, item, cue) {
|
|
487
|
+
const entry = matchedBySymbol.get(item.symbol) ?? { item, cues: [] };
|
|
488
|
+
const normalizedCue = normalizeDigestText(cue.text);
|
|
489
|
+
if (!entry.cues.some((existing) => normalizeDigestText(existing.text) === normalizedCue)) {
|
|
490
|
+
entry.cues.push(cue);
|
|
491
|
+
}
|
|
492
|
+
matchedBySymbol.set(item.symbol, entry);
|
|
493
|
+
}
|
|
494
|
+
function findBestWatchlistCue(context, item) {
|
|
495
|
+
const exactCue = context.keyPoints.find((point) => topicMatchesWatchlistItem(point, item));
|
|
496
|
+
return exactCue ?? formatContextSummary(context);
|
|
497
|
+
}
|
|
498
|
+
function topicMatchesWatchlistItem(text, item) {
|
|
499
|
+
const normalizedText = normalizeText(text);
|
|
500
|
+
return buildWatchlistKeywordEntries(item)
|
|
501
|
+
.some((entry) => normalizedText.includes(entry.normalized));
|
|
502
|
+
}
|
|
503
|
+
function describeWatchlistMatch(item, text) {
|
|
504
|
+
const normalizedText = normalizeText(text);
|
|
505
|
+
const entries = buildWatchlistKeywordEntries(item);
|
|
506
|
+
const directMatch = entries
|
|
507
|
+
.find((entry) => entry.kind === "direct" && normalizedText.includes(entry.normalized));
|
|
508
|
+
if (directMatch) {
|
|
509
|
+
return "直接命中";
|
|
510
|
+
}
|
|
511
|
+
const themeMatch = entries
|
|
512
|
+
.find((entry) => entry.kind === "theme" && normalizedText.includes(entry.normalized));
|
|
513
|
+
if (themeMatch) {
|
|
514
|
+
return `题材${themeMatch.keyword}`;
|
|
515
|
+
}
|
|
516
|
+
const sectorMatch = entries
|
|
517
|
+
.find((entry) => entry.kind === "sector" && normalizedText.includes(entry.normalized));
|
|
518
|
+
if (sectorMatch) {
|
|
519
|
+
return `行业${sectorMatch.keyword}`;
|
|
520
|
+
}
|
|
521
|
+
return "规则命中";
|
|
522
|
+
}
|
|
523
|
+
function buildWatchlistKeywordEntries(item) {
|
|
524
|
+
const entries = [
|
|
525
|
+
...[item.symbol, item.symbol.slice(0, 6), item.name].map((keyword) => ({ keyword, kind: "direct" })),
|
|
526
|
+
...extractSectorKeywords(item.sector).map((keyword) => ({ keyword, kind: "sector" })),
|
|
527
|
+
...item.themes.map((keyword) => ({ keyword, kind: "theme" })),
|
|
528
|
+
];
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
return entries
|
|
531
|
+
.map((entry) => ({
|
|
532
|
+
...entry,
|
|
533
|
+
keyword: entry.keyword.replace(/\s+/g, "").trim(),
|
|
534
|
+
normalized: normalizeText(entry.keyword),
|
|
535
|
+
}))
|
|
536
|
+
.filter((entry) => entry.normalized.length >= 2)
|
|
537
|
+
.filter((entry) => {
|
|
538
|
+
if (seen.has(entry.normalized)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
seen.add(entry.normalized);
|
|
542
|
+
return true;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function addUniqueBullet(bullets, bullet) {
|
|
546
|
+
const normalizedBullet = normalizeDigestText(bullet);
|
|
547
|
+
if (!bullets.some((existing) => normalizeDigestText(existing) === normalizedBullet)) {
|
|
548
|
+
bullets.push(bullet);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function formatFocusCueText(text) {
|
|
552
|
+
return truncateText(text, 34);
|
|
553
|
+
}
|
|
251
554
|
function containsAnyKeyword(content, keywords) {
|
|
252
|
-
|
|
555
|
+
const normalizedContent = normalizeText(content);
|
|
556
|
+
return keywords.some((keyword) => normalizedContent.includes(normalizeText(keyword)));
|
|
253
557
|
}
|
|
254
558
|
function normalizeText(value) {
|
|
255
559
|
return value.toLowerCase().replace(/\s+/g, "");
|
|
@@ -264,6 +568,160 @@ function extractHeadlineFromContent(content) {
|
|
|
264
568
|
function extractHeadlineText(content) {
|
|
265
569
|
return content.split(/[\n。!!]/)[0]?.trim() ?? "";
|
|
266
570
|
}
|
|
571
|
+
function extractFlashInsight(content) {
|
|
572
|
+
const headline = extractHeadlineFromContent(content);
|
|
573
|
+
const keyPoints = extractFlashKeyPoints(content, headline);
|
|
574
|
+
if (keyPoints.length === 0) {
|
|
575
|
+
return {
|
|
576
|
+
headline,
|
|
577
|
+
summary: buildTitleOnlySummary(headline),
|
|
578
|
+
keyPoints: [],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
headline,
|
|
583
|
+
summary: keyPoints.slice(0, 2).join(";"),
|
|
584
|
+
keyPoints,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function extractFlashKeyPoints(content, headline) {
|
|
588
|
+
const body = stripHeadline(content, headline);
|
|
589
|
+
if (!body) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
const lineCandidates = body
|
|
593
|
+
.replace(/\r/g, "\n")
|
|
594
|
+
.replace(/\n{2,}/g, "\n")
|
|
595
|
+
.replace(/([。;!?!?])\s*(?=\d+\s*[、..))])/g, "$1\n")
|
|
596
|
+
.split(/\n+/)
|
|
597
|
+
.map((segment) => cleanFlashSegment(segment))
|
|
598
|
+
.filter(Boolean);
|
|
599
|
+
const candidates = lineCandidates.length > 1
|
|
600
|
+
? lineCandidates
|
|
601
|
+
: body
|
|
602
|
+
.split(/[。;!?!?]/)
|
|
603
|
+
.map((segment) => cleanFlashSegment(segment))
|
|
604
|
+
.filter(Boolean);
|
|
605
|
+
const normalizedHeadline = normalizeText(headline);
|
|
606
|
+
const deduped = new Set();
|
|
607
|
+
const keyPoints = [];
|
|
608
|
+
for (const segment of candidates) {
|
|
609
|
+
const normalizedSegment = normalizeText(segment);
|
|
610
|
+
if (!normalizedSegment || normalizedSegment === normalizedHeadline) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (normalizedHeadline && normalizedHeadline.includes(normalizedSegment) && segment.length < Math.max(12, headline.length)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (segment.length < 8) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (deduped.has(normalizedSegment)) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
deduped.add(normalizedSegment);
|
|
623
|
+
keyPoints.push(truncateText(segment, 88));
|
|
624
|
+
if (keyPoints.length >= FALLBACK_TOPIC_KEY_POINT_LIMIT) {
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return keyPoints;
|
|
629
|
+
}
|
|
630
|
+
function stripHeadline(content, headline) {
|
|
631
|
+
const trimmed = content.trim();
|
|
632
|
+
if (!headline) {
|
|
633
|
+
return trimmed;
|
|
634
|
+
}
|
|
635
|
+
const isTruncatedHeadline = headline.endsWith("...");
|
|
636
|
+
if (!isTruncatedHeadline && trimmed.startsWith(headline)) {
|
|
637
|
+
return trimmed.slice(headline.length).replace(/^[::。;,、\s-]+/, "").trim();
|
|
638
|
+
}
|
|
639
|
+
const withoutDigestPrefix = trimmed
|
|
640
|
+
.replace(/^【?金十数据整理[::]\s*/, "")
|
|
641
|
+
.replace(/^】\s*/, "")
|
|
642
|
+
.trim();
|
|
643
|
+
if (withoutDigestPrefix !== trimmed) {
|
|
644
|
+
return withoutDigestPrefix;
|
|
645
|
+
}
|
|
646
|
+
if (isTruncatedHeadline) {
|
|
647
|
+
return trimmed;
|
|
648
|
+
}
|
|
649
|
+
return trimmed;
|
|
650
|
+
}
|
|
651
|
+
function cleanFlashSegment(segment) {
|
|
652
|
+
return segment
|
|
653
|
+
.trim()
|
|
654
|
+
.replace(/^【?金十数据整理[::]\s*/, "")
|
|
655
|
+
.replace(/^([^】]{2,40})】\s*/, "")
|
|
656
|
+
.replace(/^[-•●▪◦]\s*/, "")
|
|
657
|
+
.replace(/^\d+\s*[、..))]\s*/, "")
|
|
658
|
+
.replace(/^[((]?\d+[))]\s*/, "")
|
|
659
|
+
.replace(/^[::;,。、\s]+/, "")
|
|
660
|
+
.replace(/[::;,。、\s]+$/, "")
|
|
661
|
+
.replace(/\s+/g, " ");
|
|
662
|
+
}
|
|
663
|
+
function buildTitleOnlySummary(headline) {
|
|
664
|
+
const coreHeadline = headline.replace(/^【?金十数据整理[::]\s*/, "").replace(/】$/, "").trim();
|
|
665
|
+
if (!coreHeadline) {
|
|
666
|
+
return "该整理快讯未提取到可用细节,暂只能作为标题级线索参考。";
|
|
667
|
+
}
|
|
668
|
+
return `${coreHeadline},但正文未提取到更具体细节,暂只能作为标题级线索参考。`;
|
|
669
|
+
}
|
|
670
|
+
function formatContextSummary(context) {
|
|
671
|
+
return context.summary || buildTitleOnlySummary(context.headline);
|
|
672
|
+
}
|
|
673
|
+
function formatFocusCue(context) {
|
|
674
|
+
return context.keyPoints[0] ?? context.summary ?? context.headline;
|
|
675
|
+
}
|
|
676
|
+
function truncateText(value, maxLength) {
|
|
677
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength)}...`;
|
|
678
|
+
}
|
|
679
|
+
function isLowSignalSummary(summary, contexts) {
|
|
680
|
+
const bulletLines = summary
|
|
681
|
+
.split(/\n+/)
|
|
682
|
+
.map((line) => line.trim())
|
|
683
|
+
.filter((line) => /^(?:•|-|\d+\.)/.test(line));
|
|
684
|
+
if (bulletLines.length === 0) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
const titleOnlyCount = bulletLines.filter((line) => isTitleOnlyBullet(line, contexts)).length;
|
|
688
|
+
return titleOnlyCount >= Math.max(2, Math.ceil(bulletLines.length / 3));
|
|
689
|
+
}
|
|
690
|
+
function isTitleOnlyBullet(line, contexts) {
|
|
691
|
+
const candidates = normalizeBulletForComparison(line);
|
|
692
|
+
if (candidates.length === 0) {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
return contexts.some((context) => {
|
|
696
|
+
const headlineForms = [
|
|
697
|
+
normalizeDigestText(context.headline),
|
|
698
|
+
normalizeDigestText(stripDigestPrefix(context.headline)),
|
|
699
|
+
].filter(Boolean);
|
|
700
|
+
return headlineForms.some((headline) => candidates.some((candidate) => candidate.includes(headline) && candidate.length <= headline.length + 6));
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function normalizeBulletForComparison(line) {
|
|
704
|
+
const cleaned = line
|
|
705
|
+
.replace(/^(?:•|-|\d+\.)\s*/, "")
|
|
706
|
+
.replace(/^\[[0-9:]+\]\s*/, "");
|
|
707
|
+
const normalizedVariants = [
|
|
708
|
+
cleaned,
|
|
709
|
+
cleaned.split(/[::]/).at(-1) ?? cleaned,
|
|
710
|
+
]
|
|
711
|
+
.map((item) => normalizeDigestText(item))
|
|
712
|
+
.filter(Boolean);
|
|
713
|
+
return [...new Set(normalizedVariants)];
|
|
714
|
+
}
|
|
715
|
+
function stripDigestPrefix(value) {
|
|
716
|
+
return value.replace(/^【?金十数据整理[::]\s*/, "").replace(/】$/, "").trim();
|
|
717
|
+
}
|
|
718
|
+
function normalizeDigestText(value) {
|
|
719
|
+
return value
|
|
720
|
+
.toLowerCase()
|
|
721
|
+
.replace(/[【】[\]()()"'“”‘’]/g, "")
|
|
722
|
+
.replace(/[::]/g, "")
|
|
723
|
+
.replace(/\s+/g, "");
|
|
724
|
+
}
|
|
267
725
|
function toFlashRecord(item) {
|
|
268
726
|
const published = new Date(item.time);
|
|
269
727
|
if (Number.isNaN(published.getTime())) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TickFlowBalanceSheetRecord, TickFlowCashFlowRecord, TickFlowFinancialMetricsRecord, TickFlowFinancialQueryOptions, TickFlowIncomeRecord, TickFlowInstrument, TickFlowQuote } from "../types/tickflow.js";
|
|
1
|
+
import type { TickFlowBalanceSheetRecord, TickFlowCashFlowRecord, TickFlowFinancialMetricsRecord, TickFlowFinancialQueryOptions, TickFlowIncomeRecord, TickFlowInstrument, TickFlowQuote, TickFlowUniverseDetail, TickFlowUniverseSummary } from "../types/tickflow.js";
|
|
2
2
|
export declare class TickFlowClientError extends Error {
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
@@ -10,6 +10,9 @@ export declare class TickFlowClient {
|
|
|
10
10
|
getApiKey(): string;
|
|
11
11
|
fetchInstruments(symbols: string[]): Promise<TickFlowInstrument[]>;
|
|
12
12
|
fetchQuotes(symbols: string[]): Promise<TickFlowQuote[]>;
|
|
13
|
+
listUniverses(): Promise<TickFlowUniverseSummary[]>;
|
|
14
|
+
fetchUniverse(id: string): Promise<TickFlowUniverseDetail | null>;
|
|
15
|
+
fetchUniverseBatch(ids: string[]): Promise<Record<string, TickFlowUniverseDetail>>;
|
|
13
16
|
fetchKlinesBatch<T = unknown>(symbols: string[], params?: {
|
|
14
17
|
period?: string;
|
|
15
18
|
count?: number;
|
|
@@ -39,6 +39,38 @@ export class TickFlowClient {
|
|
|
39
39
|
});
|
|
40
40
|
return response.data ?? [];
|
|
41
41
|
}
|
|
42
|
+
async listUniverses() {
|
|
43
|
+
const url = new URL("/v1/universes", this.baseUrl);
|
|
44
|
+
const response = await this.requestJson(url.toString(), {
|
|
45
|
+
method: "GET",
|
|
46
|
+
});
|
|
47
|
+
return response.data ?? [];
|
|
48
|
+
}
|
|
49
|
+
async fetchUniverse(id) {
|
|
50
|
+
const normalizedId = String(id ?? "").trim();
|
|
51
|
+
if (!normalizedId) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const url = new URL(`/v1/universes/${encodeURIComponent(normalizedId)}`, this.baseUrl);
|
|
55
|
+
const response = await this.requestJson(url.toString(), {
|
|
56
|
+
method: "GET",
|
|
57
|
+
});
|
|
58
|
+
return response.data ?? null;
|
|
59
|
+
}
|
|
60
|
+
async fetchUniverseBatch(ids) {
|
|
61
|
+
const normalizedIds = ids
|
|
62
|
+
.map((id) => String(id ?? "").trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
if (normalizedIds.length === 0) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
const url = new URL("/v1/universes/batch", this.baseUrl);
|
|
68
|
+
const response = await this.requestJson(url.toString(), {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({ ids: normalizedIds }),
|
|
71
|
+
});
|
|
72
|
+
return response.data ?? {};
|
|
73
|
+
}
|
|
42
74
|
async fetchKlinesBatch(symbols, params = {}) {
|
|
43
75
|
if (symbols.length === 0) {
|
|
44
76
|
return { data: {} };
|