tickflow-assist 0.3.6 → 0.3.7
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 +7 -39
- package/dist/analysis/types/composite-analysis.d.ts +27 -0
- package/dist/bootstrap.js +15 -4
- package/dist/config/tickflow-access.d.ts +2 -1
- package/dist/config/tickflow-access.js +10 -3
- package/dist/dev/tickflow-assist-cli.js +4 -3
- package/dist/dev/validate-mx-search.js +10 -2
- 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 +5 -1
- 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.js +3 -17
- 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 +165 -11
- 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.js +1 -1
- 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/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/test-alert.tool.js +56 -19
- 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 +79 -2
- package/package.json +5 -5
- package/skills/stock-analysis/SKILL.md +8 -18
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.7` 接入 TickFlow 标的池行业映射与申万三级同业上下文,优化盘前简报提炼与收盘复盘市场信息展示,并对齐 OpenClaw `v2026.4.14` 兼容与社区元数据。完整发布记录见 <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.14` 上兼容。
|
|
8
8
|
|
|
9
9
|
## 安装前准备
|
|
10
10
|
|
|
@@ -32,41 +32,9 @@ openclaw config validate
|
|
|
32
32
|
openclaw gateway restart
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
如果你希望先审阅配置,再只打印最少的后续步骤,可使用:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
node ~/.openclaw/extensions/tickflow-assist/dist/dev/tickflow-assist-cli.js configure-openclaw --no-enable --no-restart
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
如果你在 Linux 或 macOS 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体,例如:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
# Debian / Ubuntu
|
|
49
|
-
sudo apt-get update
|
|
50
|
-
sudo apt-get install -y fontconfig fonts-noto-cjk
|
|
51
|
-
fc-cache -fv
|
|
52
|
-
|
|
53
|
-
# RHEL / Fedora / Rocky / AlmaLinux
|
|
54
|
-
sudo dnf install -y fontconfig google-noto-sans-cjk-ttc-fonts
|
|
55
|
-
fc-cache -fv
|
|
56
|
-
|
|
57
|
-
# Arch / Manjaro
|
|
58
|
-
sudo pacman -Sy --noconfirm fontconfig noto-fonts-cjk
|
|
59
|
-
fc-cache -fv
|
|
60
|
-
|
|
61
|
-
# Alpine
|
|
62
|
-
sudo apk add fontconfig font-noto-cjk
|
|
63
|
-
fc-cache -fv
|
|
64
|
-
|
|
65
|
-
# macOS (Homebrew)
|
|
66
|
-
brew install fontconfig
|
|
67
|
-
brew install --cask font-noto-sans-cjk
|
|
68
|
-
fc-cache -fv
|
|
69
|
-
```
|
|
35
|
+
- `configure-openclaw` 会把配置写入 `~/.openclaw/openclaw.json` 的 `plugins.entries["tickflow-assist"].config`。
|
|
36
|
+
- 核心必填建议先准备:`tickflowApiKey`、`tickflowApiKeyLevel`、`llmApiKey`、`llmBaseUrl`、`llmModel`;告警场景再补 `alertChannel`、`alertTarget`、`alertAccount`。
|
|
37
|
+
- 如果你不想把密钥落盘,优先把环境变量写进 `~/.openclaw/.env`,再运行配置向导补齐非密钥项;如需 PNG 告警卡正常显示中文,请自行安装 `fontconfig` 与 Noto CJK 字体。
|
|
70
38
|
|
|
71
39
|
社区安装后的升级方式:
|
|
72
40
|
|
|
@@ -126,8 +94,8 @@ plugins.entries["tickflow-assist"].config
|
|
|
126
94
|
|
|
127
95
|
## 依赖与可选能力
|
|
128
96
|
|
|
129
|
-
- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE)
|
|
130
|
-
- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24
|
|
97
|
+
- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):`Free` 可用日线与实时行情;`Starter` 起可用标的池,插件会用来做申万行业映射与申万 3 级同业表现;`Pro` 起可用分钟K;`Expert` 才走 TickFlow 财务数据,非 `Expert` 默认回退妙想 lite。
|
|
98
|
+
- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。独立的金十数据 Skill 详见 [OpenClaw Skill](https://clawhub.ai/robinspt/jin10) / [Hermes Skill](https://github.com/robinspt/hermes-skills)。
|
|
131
99
|
- [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_select_stock` 与非 `Expert` 财务链路的 lite 补充。
|
|
132
100
|
|
|
133
101
|
## 仓库
|
|
@@ -114,10 +114,37 @@ export interface FlashNewsContext {
|
|
|
114
114
|
stockAlerts: FlashNewsItem[];
|
|
115
115
|
marketOverviewFlashes: FlashNewsItem[];
|
|
116
116
|
}
|
|
117
|
+
export interface IndustryPeerMover {
|
|
118
|
+
symbol: string;
|
|
119
|
+
name: string;
|
|
120
|
+
changePct: number;
|
|
121
|
+
}
|
|
122
|
+
export interface IndustryPeerContext {
|
|
123
|
+
available: boolean;
|
|
124
|
+
summary: string;
|
|
125
|
+
sw1Name: string | null;
|
|
126
|
+
sw2Name: string | null;
|
|
127
|
+
sw3Name: string | null;
|
|
128
|
+
sw3UniverseId: string | null;
|
|
129
|
+
peerCount: number;
|
|
130
|
+
otherStockCount: number;
|
|
131
|
+
advanceCount: number;
|
|
132
|
+
declineCount: number;
|
|
133
|
+
flatCount: number;
|
|
134
|
+
averageChangePct: number | null;
|
|
135
|
+
medianChangePct: number | null;
|
|
136
|
+
targetChangePct: number | null;
|
|
137
|
+
targetRank: number | null;
|
|
138
|
+
targetPercentile: number | null;
|
|
139
|
+
leaders: IndustryPeerMover[];
|
|
140
|
+
laggards: IndustryPeerMover[];
|
|
141
|
+
note: string | null;
|
|
142
|
+
}
|
|
117
143
|
export interface PostCloseReviewInput extends CompositeAnalysisInput {
|
|
118
144
|
compositeResult: CompositeAnalysisResult;
|
|
119
145
|
validation: PriorKeyLevelValidationContext;
|
|
120
146
|
flashContext: FlashNewsContext;
|
|
147
|
+
peerContext: IndustryPeerContext;
|
|
121
148
|
}
|
|
122
149
|
export interface PostCloseReviewResult {
|
|
123
150
|
analysisText: string;
|
package/dist/bootstrap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { supportsUniverseAccess } from "./config/tickflow-access.js";
|
|
2
3
|
import { TickFlowClient } from "./services/tickflow-client.js";
|
|
3
4
|
import { InstrumentService } from "./services/instrument-service.js";
|
|
4
5
|
import { KlineService } from "./services/kline-service.js";
|
|
@@ -22,6 +23,8 @@ import { NewsAnalysisRepository } from "./storage/repositories/news-analysis-rep
|
|
|
22
23
|
import { CompositeAnalysisRepository } from "./storage/repositories/composite-analysis-repo.js";
|
|
23
24
|
import { Jin10FlashRepository } from "./storage/repositories/jin10-flash-repo.js";
|
|
24
25
|
import { Jin10FlashDeliveryRepository } from "./storage/repositories/jin10-flash-delivery-repo.js";
|
|
26
|
+
import { UniverseRepository } from "./storage/repositories/universe-repo.js";
|
|
27
|
+
import { UniverseMembershipRepository } from "./storage/repositories/universe-membership-repo.js";
|
|
25
28
|
import { WatchlistService } from "./services/watchlist-service.js";
|
|
26
29
|
import { WatchlistProfileService } from "./services/watchlist-profile-service.js";
|
|
27
30
|
import { AnalysisService } from "./services/analysis-service.js";
|
|
@@ -37,6 +40,8 @@ import { KeyLevelsBacktestService } from "./services/key-levels-backtest-service
|
|
|
37
40
|
import { PostCloseReviewService } from "./services/post-close-review-service.js";
|
|
38
41
|
import { PreMarketBriefService } from "./services/pre-market-brief-service.js";
|
|
39
42
|
import { ReviewMemoryService } from "./services/review-memory-service.js";
|
|
43
|
+
import { TickFlowUniverseService } from "./services/tickflow-universe-service.js";
|
|
44
|
+
import { IndustryPeerService } from "./services/industry-peer-service.js";
|
|
40
45
|
import { CompositeAnalysisOrchestrator } from "./analysis/orchestrators/composite-analysis.orchestrator.js";
|
|
41
46
|
import { MarketAnalysisProvider } from "./analysis/providers/market-analysis.provider.js";
|
|
42
47
|
import { FinancialAnalysisProvider } from "./analysis/providers/financial-analysis.provider.js";
|
|
@@ -100,6 +105,8 @@ export function createAppContext(config, options = {}) {
|
|
|
100
105
|
const compositeAnalysisRepository = new CompositeAnalysisRepository(database);
|
|
101
106
|
const jin10FlashRepository = new Jin10FlashRepository(database);
|
|
102
107
|
const jin10FlashDeliveryRepository = new Jin10FlashDeliveryRepository(database);
|
|
108
|
+
const universeRepository = new UniverseRepository(database);
|
|
109
|
+
const universeMembershipRepository = new UniverseMembershipRepository(database);
|
|
103
110
|
const instrumentService = new InstrumentService(tickflowClient);
|
|
104
111
|
const klineService = new KlineService(tickflowClient);
|
|
105
112
|
const quoteService = new QuoteService(tickflowClient);
|
|
@@ -108,7 +115,10 @@ export function createAppContext(config, options = {}) {
|
|
|
108
115
|
const jin10McpService = new Jin10McpService(config.jin10McpUrl, config.jin10ApiToken);
|
|
109
116
|
const financialLiteService = new FinancialLiteService(mxApiService);
|
|
110
117
|
const analysisService = new AnalysisService(config.llmBaseUrl, config.llmApiKey, config.llmModel, analysisLogRepository);
|
|
111
|
-
const
|
|
118
|
+
const tickFlowUniverseService = supportsUniverseAccess(config.tickflowApiKeyLevel)
|
|
119
|
+
? new TickFlowUniverseService(tickflowClient, universeRepository, universeMembershipRepository)
|
|
120
|
+
: null;
|
|
121
|
+
const watchlistProfileService = new WatchlistProfileService(tickFlowUniverseService, mxApiService, analysisService);
|
|
112
122
|
const tradingCalendarService = new TradingCalendarService(config.calendarFile);
|
|
113
123
|
const alertDiagnosticLogger = createAlertDiagnosticLogger(config.databasePath);
|
|
114
124
|
const alertService = new AlertService({
|
|
@@ -143,7 +153,8 @@ export function createAppContext(config, options = {}) {
|
|
|
143
153
|
const monitorService = new MonitorService(config.databasePath, config.requestInterval, config.alertChannel, watchlistService, quoteService, tradingCalendarService, keyLevelsRepository, alertLogRepository, klinesRepository, intradayKlinesRepository, klineService, alertService, alertMediaService, alertDiagnosticLogger);
|
|
144
154
|
const jin10FlashMonitorService = new Jin10FlashMonitorService(config.databasePath, config.jin10FlashPollInterval, config.jin10FlashRetentionDays, config.jin10FlashNightAlert, watchlistService, jin10McpService, analysisService, alertService, jin10FlashRepository, jin10FlashDeliveryRepository);
|
|
145
155
|
const updateService = new UpdateService(klineService, config.tickflowApiKeyLevel, indicatorService, klinesRepository, indicatorsRepository, intradayKlinesRepository, watchlistService, tradingCalendarService);
|
|
146
|
-
const
|
|
156
|
+
const industryPeerService = new IndustryPeerService(tickFlowUniverseService, quoteService);
|
|
157
|
+
const postCloseReviewService = new PostCloseReviewService(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, jin10FlashDeliveryRepository, jin10FlashRepository, industryPeerService);
|
|
147
158
|
const preMarketBriefService = new PreMarketBriefService(watchlistService, jin10McpService, jin10FlashRepository, analysisService);
|
|
148
159
|
const realtimeMonitorWorker = new RealtimeMonitorWorker(monitorService, config.requestInterval * 1000);
|
|
149
160
|
const jin10FlashWorker = new Jin10FlashWorker(jin10FlashMonitorService, config.jin10FlashPollInterval * 1000);
|
|
@@ -153,7 +164,7 @@ export function createAppContext(config, options = {}) {
|
|
|
153
164
|
return {
|
|
154
165
|
config,
|
|
155
166
|
tools: [
|
|
156
|
-
addStockTool(watchlistService, klineService, klinesRepository, indicatorService, indicatorsRepository),
|
|
167
|
+
addStockTool(config.tickflowApiKeyLevel, watchlistService, klineService, klinesRepository, indicatorService, indicatorsRepository),
|
|
157
168
|
analyzeTool(compositeAnalysisOrchestrator),
|
|
158
169
|
backtestKeyLevelsTool(keyLevelsBacktestService),
|
|
159
170
|
dailyUpdateStatusTool(dailyUpdateWorker, runtime.configSource),
|
|
@@ -167,7 +178,7 @@ export function createAppContext(config, options = {}) {
|
|
|
167
178
|
mxSelectStockTool(mxApiService),
|
|
168
179
|
queryDatabaseTool(database),
|
|
169
180
|
refreshWatchlistNamesTool(watchlistService),
|
|
170
|
-
refreshWatchlistProfilesTool(watchlistService),
|
|
181
|
+
refreshWatchlistProfilesTool(config.tickflowApiKeyLevel, watchlistService),
|
|
171
182
|
removeStockTool(watchlistService),
|
|
172
183
|
startDailyUpdateTool(dailyUpdateWorker, config, runtime.configSource, runtime),
|
|
173
184
|
startMonitorTool(monitorService, runtime),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type TickflowApiKeyLevel = "free" | "
|
|
1
|
+
export type TickflowApiKeyLevel = "free" | "starter" | "pro" | "expert";
|
|
2
2
|
export declare function normalizeTickflowApiKeyLevel(value: unknown, fallback?: TickflowApiKeyLevel): TickflowApiKeyLevel;
|
|
3
3
|
export declare function supportsIntradayKlines(level: TickflowApiKeyLevel): boolean;
|
|
4
|
+
export declare function supportsUniverseAccess(level: TickflowApiKeyLevel): boolean;
|
|
4
5
|
export declare function formatTickflowApiKeyLevel(level: TickflowApiKeyLevel): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const INTRADAY_ENABLED_LEVELS = new Set(["pro", "expert"]);
|
|
2
|
+
const UNIVERSE_ENABLED_LEVELS = new Set(["starter", "pro", "expert"]);
|
|
2
3
|
export function normalizeTickflowApiKeyLevel(value, fallback = "free") {
|
|
3
4
|
const normalized = String(value ?? "")
|
|
4
5
|
.trim()
|
|
@@ -6,7 +7,10 @@ export function normalizeTickflowApiKeyLevel(value, fallback = "free") {
|
|
|
6
7
|
if (normalized === "export") {
|
|
7
8
|
return "expert";
|
|
8
9
|
}
|
|
9
|
-
if (normalized === "
|
|
10
|
+
if (normalized === "start") {
|
|
11
|
+
return "starter";
|
|
12
|
+
}
|
|
13
|
+
if (normalized === "free" || normalized === "starter" || normalized === "pro" || normalized === "expert") {
|
|
10
14
|
return normalized;
|
|
11
15
|
}
|
|
12
16
|
return fallback;
|
|
@@ -14,12 +18,15 @@ export function normalizeTickflowApiKeyLevel(value, fallback = "free") {
|
|
|
14
18
|
export function supportsIntradayKlines(level) {
|
|
15
19
|
return INTRADAY_ENABLED_LEVELS.has(level);
|
|
16
20
|
}
|
|
21
|
+
export function supportsUniverseAccess(level) {
|
|
22
|
+
return UNIVERSE_ENABLED_LEVELS.has(level);
|
|
23
|
+
}
|
|
17
24
|
export function formatTickflowApiKeyLevel(level) {
|
|
18
25
|
switch (level) {
|
|
19
26
|
case "free":
|
|
20
27
|
return "Free";
|
|
21
|
-
case "
|
|
22
|
-
return "
|
|
28
|
+
case "starter":
|
|
29
|
+
return "Starter";
|
|
23
30
|
case "pro":
|
|
24
31
|
return "Pro";
|
|
25
32
|
case "expert":
|
|
@@ -46,7 +46,7 @@ Options:
|
|
|
46
46
|
--no-font-setup Do not print Chinese font setup guidance
|
|
47
47
|
--openclaw-bin <path> OpenClaw CLI binary name used in printed next steps
|
|
48
48
|
--tickflow-api-key <key>
|
|
49
|
-
--tickflow-api-key-level <Free|
|
|
49
|
+
--tickflow-api-key-level <Free|Starter|Pro|Expert>
|
|
50
50
|
--mx-search-api-key <key>
|
|
51
51
|
--jin10-mcp-url <url>
|
|
52
52
|
--jin10-api-token <token>
|
|
@@ -198,7 +198,8 @@ function normalizeApiKeyLevel(value) {
|
|
|
198
198
|
case "free":
|
|
199
199
|
return "Free";
|
|
200
200
|
case "start":
|
|
201
|
-
|
|
201
|
+
case "starter":
|
|
202
|
+
return "Starter";
|
|
202
203
|
case "pro":
|
|
203
204
|
return "Pro";
|
|
204
205
|
case "expert":
|
|
@@ -485,7 +486,7 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
|
|
|
485
486
|
});
|
|
486
487
|
seed.tickflowApiKeyLevel = normalizeApiKeyLevel(await promptSelect(rl, "TickFlow 订阅等级", [
|
|
487
488
|
{ value: "Free", label: "Free" },
|
|
488
|
-
{ value: "
|
|
489
|
+
{ value: "Starter", label: "Starter" },
|
|
489
490
|
{ value: "Pro", label: "Pro" },
|
|
490
491
|
{ value: "Expert", label: "Expert" },
|
|
491
492
|
], seed.tickflowApiKeyLevel));
|
|
@@ -7,8 +7,12 @@ import { buildWatchlistProfileExtractionUserPrompt, } from "../prompts/analysis/
|
|
|
7
7
|
import { AnalysisService } from "../services/analysis-service.js";
|
|
8
8
|
import { MxApiService, normalizeMxSearchDocuments } from "../services/mx-search-service.js";
|
|
9
9
|
import { buildBoardNewsQuery, formatWatchlistProfileDocuments, WatchlistProfileService, } from "../services/watchlist-profile-service.js";
|
|
10
|
+
import { TickFlowClient } from "../services/tickflow-client.js";
|
|
11
|
+
import { TickFlowUniverseService } from "../services/tickflow-universe-service.js";
|
|
10
12
|
import { Database } from "../storage/db.js";
|
|
11
13
|
import { AnalysisLogRepository } from "../storage/repositories/analysis-log-repo.js";
|
|
14
|
+
import { UniverseMembershipRepository } from "../storage/repositories/universe-membership-repo.js";
|
|
15
|
+
import { UniverseRepository } from "../storage/repositories/universe-repo.js";
|
|
12
16
|
import { WatchlistRepository } from "../storage/repositories/watchlist-repo.js";
|
|
13
17
|
async function main() {
|
|
14
18
|
runFixtureValidation();
|
|
@@ -119,7 +123,7 @@ function runFixtureValidation() {
|
|
|
119
123
|
sector: "计算机-软件开发-垂直应用软件",
|
|
120
124
|
themes: ["OpenClaw概念", "托育概念", "一体机概念", "信创"],
|
|
121
125
|
});
|
|
122
|
-
assert.equal(boardQuery, "
|
|
126
|
+
assert.equal(boardQuery, "计算机 软件开发 垂直应用软件 OpenClaw概念 托育概念 一体机概念 板块 题材 最新新闻 政策 资金");
|
|
123
127
|
const emptyBoardQuery = buildBoardNewsQuery({
|
|
124
128
|
sector: null,
|
|
125
129
|
themes: [],
|
|
@@ -135,7 +139,11 @@ async function runLiveValidation(limit) {
|
|
|
135
139
|
const database = new Database(config.databasePath);
|
|
136
140
|
const watchlistRepository = new WatchlistRepository(database);
|
|
137
141
|
const analysisLogRepository = new AnalysisLogRepository(database);
|
|
142
|
+
const universeRepository = new UniverseRepository(database);
|
|
143
|
+
const universeMembershipRepository = new UniverseMembershipRepository(database);
|
|
138
144
|
const mxApiService = new MxApiService(config.mxSearchApiUrl, config.mxSearchApiKey);
|
|
145
|
+
const tickFlowClient = new TickFlowClient(config.tickflowApiUrl, config.tickflowApiKey);
|
|
146
|
+
const tickFlowUniverseService = new TickFlowUniverseService(tickFlowClient, universeRepository, universeMembershipRepository);
|
|
139
147
|
const analysisService = new AnalysisService(config.llmBaseUrl, config.llmApiKey, config.llmModel, analysisLogRepository);
|
|
140
148
|
const mxConfigError = mxApiService.getConfigurationError();
|
|
141
149
|
if (mxConfigError) {
|
|
@@ -152,7 +160,7 @@ async function runLiveValidation(limit) {
|
|
|
152
160
|
console.log("[validate:mx-search] live validation skipped: no samples available");
|
|
153
161
|
return;
|
|
154
162
|
}
|
|
155
|
-
const watchlistProfileService = new WatchlistProfileService(mxApiService, analysisService);
|
|
163
|
+
const watchlistProfileService = new WatchlistProfileService(tickFlowUniverseService, mxApiService, analysisService);
|
|
156
164
|
console.log(`[validate:mx-search] live validation start: ${samples.length} samples`);
|
|
157
165
|
for (const sample of samples) {
|
|
158
166
|
const profile = await watchlistProfileService.resolve(sample.symbol, sample.name, currentTimestamp());
|
package/dist/plugin.js
CHANGED
|
@@ -10,12 +10,10 @@ const PLUGIN_DESCRIPTION = "A-share watchlist analysis, monitoring, and alert de
|
|
|
10
10
|
const STOCK_AGENT_ID = "stock";
|
|
11
11
|
const STOCK_PROMPT_ENFORCEMENT = [
|
|
12
12
|
"You are handling the stock agent.",
|
|
13
|
-
"
|
|
14
|
-
"If the user asks to add a stock and provides a symbol,
|
|
15
|
-
"If the user asks to remove a stock and provides symbol,
|
|
16
|
-
"If the user asks for watchlist,
|
|
17
|
-
"Do not call read, write, edit, query_database, session tools, or environment-inspection tools to figure out how to perform add/remove/list watchlist actions.",
|
|
18
|
-
"Do not say you need to inspect the environment, confirm available tools, or find the method first when add_stock/remove_stock/list_watchlist are available.",
|
|
13
|
+
"Prefer TickFlow Assist plugin tools for watchlist, stock status, analysis, monitoring, and alert intents.",
|
|
14
|
+
"If the user asks to add a stock and provides a symbol, call add_stock first.",
|
|
15
|
+
"If the user asks to remove a stock and provides a symbol, call remove_stock first.",
|
|
16
|
+
"If the user asks for the watchlist, call list_watchlist first.",
|
|
19
17
|
"If a required tool parameter is missing, ask only for that missing parameter.",
|
|
20
18
|
].join("\n");
|
|
21
19
|
const GENERIC_TOOL_PARAMETERS_SCHEMA = Type.Object({}, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatCostPrice, formatCostRelationship } from "../../utils/cost-price.js";
|
|
2
|
+
import { resolveTickFlowQuoteChangePct } from "../../utils/tickflow-quote.js";
|
|
2
3
|
const MAX_INTRADAY_FULL_ROWS = 40;
|
|
3
4
|
const MAX_INTRADAY_OPEN_ROWS = 8;
|
|
4
5
|
const MAX_INTRADAY_CLOSE_ROWS = 12;
|
|
@@ -139,7 +140,7 @@ function buildRealtimeLines(quote) {
|
|
|
139
140
|
"",
|
|
140
141
|
`- 最新价: ${fmt(quote.last_price)}`,
|
|
141
142
|
`- 前收: ${fmt(quote.prev_close)}`,
|
|
142
|
-
`- 涨跌幅: ${fmtPercent(quote
|
|
143
|
+
`- 涨跌幅: ${fmtPercent(resolveTickFlowQuoteChangePct(quote))}`,
|
|
143
144
|
`- 成交量: ${Math.trunc(Number(quote.volume ?? 0))}`,
|
|
144
145
|
`- 行情时间: ${formatTimestamp(quote.timestamp)}`,
|
|
145
146
|
];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatCostPrice, formatCostRelationship } from "../../utils/cost-price.js";
|
|
2
|
+
import { normalizeTickFlowChangePct, resolveTickFlowKlineChangePct } from "../../utils/tickflow-quote.js";
|
|
2
3
|
import { buildReferencedNarrative, truncatePromptText } from "./prompt-text-utils.js";
|
|
3
4
|
import { indentPromptBlock, KEY_LEVELS_FIELD_GUIDANCE, KEY_LEVELS_JSON_SCHEMA_INNER, } from "./shared-schema.js";
|
|
4
5
|
const MAX_VALIDATION_SUMMARY_LENGTH = 220;
|
|
@@ -50,11 +51,13 @@ export function buildPostCloseReviewUserPrompt(input) {
|
|
|
50
51
|
const latestClose = input.market.klines[input.market.klines.length - 1]?.close ?? 0;
|
|
51
52
|
const latestRealtimePrice = input.market.realtimeQuote?.last_price ?? latestClose;
|
|
52
53
|
const watchlistItem = input.market.watchlistItem;
|
|
54
|
+
const latestChangePct = resolveDailyChangePct(input);
|
|
53
55
|
return [
|
|
54
56
|
`请对 ${input.market.companyName}(${input.market.symbol})生成收盘复盘。`,
|
|
55
57
|
`用户成本价: ${formatCostPrice(watchlistItem?.costPrice ?? null)}`,
|
|
56
58
|
`最新收盘价: ${latestClose.toFixed(2)}`,
|
|
57
59
|
`最新实时价: ${latestRealtimePrice.toFixed(2)}`,
|
|
60
|
+
`当日涨跌幅: ${formatSignedPct(latestChangePct)}`,
|
|
58
61
|
`相对成本价: ${formatCostRelationship(latestRealtimePrice, watchlistItem?.costPrice ?? null)}`,
|
|
59
62
|
`申万行业分类: ${watchlistItem?.sector ?? "未记录"}`,
|
|
60
63
|
`概念板块: ${watchlistItem?.themes.length ? watchlistItem.themes.join(";") : "未记录"}`,
|
|
@@ -79,6 +82,9 @@ export function buildPostCloseReviewUserPrompt(input) {
|
|
|
79
82
|
"## 行业分类/概念板块资讯摘要",
|
|
80
83
|
input.news.boardDocuments.length > 0 ? formatDocuments(input.news.boardDocuments) : "未获取到有效行业分类/概念板块资讯。",
|
|
81
84
|
"",
|
|
85
|
+
"## 申万三级同业表现",
|
|
86
|
+
formatIndustryPeerContext(input.peerContext),
|
|
87
|
+
"",
|
|
82
88
|
"## 结构化参考",
|
|
83
89
|
`当前综合关键位: ${formatLevels(input.compositeResult.levels ?? input.technicalResult.levels)}`,
|
|
84
90
|
`基本面评分/倾向: ${input.financialResult.score ?? "-"} / ${input.financialResult.bias}`,
|
|
@@ -88,7 +94,7 @@ export function buildPostCloseReviewUserPrompt(input) {
|
|
|
88
94
|
`资讯催化: ${joinList(input.newsResult.catalysts)}`,
|
|
89
95
|
`资讯风险: ${joinList(input.newsResult.risks)}`,
|
|
90
96
|
"",
|
|
91
|
-
"请按系统要求输出正文和最终 JSON
|
|
97
|
+
"请按系统要求输出正文和最终 JSON。正文重点回答:昨天关键位到底是否有效;今天盘面是否得到大盘、行业分类/概念板块、申万三级同业强弱、新闻的解释;明天该沿用、微调、重算还是暂停关键位。",
|
|
92
98
|
].join("\n");
|
|
93
99
|
}
|
|
94
100
|
function formatDocuments(documents) {
|
|
@@ -143,3 +149,36 @@ function formatFlashSection(label, items) {
|
|
|
143
149
|
}),
|
|
144
150
|
].join("\n");
|
|
145
151
|
}
|
|
152
|
+
function formatIndustryPeerContext(context) {
|
|
153
|
+
if (!context.available) {
|
|
154
|
+
return context.note ?? context.summary ?? "未获取到申万三级同业表现。";
|
|
155
|
+
}
|
|
156
|
+
return [
|
|
157
|
+
context.summary,
|
|
158
|
+
`行业层级: ${joinList([context.sw1Name, context.sw2Name, context.sw3Name].filter(isNonEmptyText))}`,
|
|
159
|
+
`领涨样本: ${formatPeerMovers(context.leaders)}`,
|
|
160
|
+
`领跌样本: ${formatPeerMovers(context.laggards)}`,
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
function formatPeerMovers(movers) {
|
|
164
|
+
if (movers.length === 0) {
|
|
165
|
+
return "无";
|
|
166
|
+
}
|
|
167
|
+
return movers
|
|
168
|
+
.map((item) => `${item.name || item.symbol}(${item.symbol} ${item.changePct >= 0 ? "+" : ""}${item.changePct.toFixed(2)}%)`)
|
|
169
|
+
.join(";");
|
|
170
|
+
}
|
|
171
|
+
function isNonEmptyText(value) {
|
|
172
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
173
|
+
}
|
|
174
|
+
function resolveDailyChangePct(input) {
|
|
175
|
+
const latestKline = input.market.klines[input.market.klines.length - 1];
|
|
176
|
+
return normalizeTickFlowChangePct(input.market.realtimeQuote?.ext?.change_pct)
|
|
177
|
+
?? resolveTickFlowKlineChangePct(latestKline);
|
|
178
|
+
}
|
|
179
|
+
function formatSignedPct(value) {
|
|
180
|
+
if (value == null || !Number.isFinite(value)) {
|
|
181
|
+
return "未获取";
|
|
182
|
+
}
|
|
183
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(2)}%`;
|
|
184
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WatchlistItem } from "../../types/domain.js";
|
|
2
|
-
export declare const PRE_MARKET_BRIEF_SYSTEM_PROMPT = "\n\u4F60\u662F\u4E00\u4F4DA\u80A1\u5F00\u76D8\u524D\u8D44\u8BAF\u7B80\u62A5\u7F16\u8F91\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u76D8\u524D\u7A97\u53E3\u5185\u7684\u91D1\u5341\u6570\u636E\u6574\u7406\u5FEB\u8BAF\uFF0C\u4EE5\u53CA\u7528\u6237\u81EA\u9009\u80A1\u7684\u884C\u4E1A/\u9898\u6750\u4FE1\u606F\uFF0C\u751F\u6210\u4E00\u4EFD\u9002\u5408 9:20 \u63A8\u9001\u7684\u5F00\u76D8\u524D\u6458\u8981\u3002\n\n\u8F93\u51FA\u8981\u6C42\uFF1A\n1. \u6309\u4EE5\u4E0B\u6807\u9898\u8F93\u51FA\uFF0C\u6BCF\u8282\u4F7F\u7528 2-5 \u6761\u7B80\u6D01\u4E2D\u6587\u8981\u70B9\uFF1A\n- \u91CD\u5927\u8981\u95FB\n- \u81EA\u9009\u76F8\u5173\n- \u6F5C\u5728\u673A\u4F1A\n- \u98CE\u9669\u63D0\u793A\n- \u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\n2. \u201C\u91CD\u5927\u8981\u95FB\u201D\u53EA\u63D0\u70BC\u4F1A\u5F71\u54CD A \u80A1\u5F00\u76D8\u60C5\u7EEA\u3001\u884C\u4E1A\u98CE\u9669\u504F\u597D\u6216\u91CD\u8981\u4EA4\u6613\u7EBF\u7D22\u7684\u5185\u5BB9\uFF0C\u4E0D\u8981\u673A\u68B0\u7F57\u5217\u6240\u6709\u5FEB\u8BAF\u3002\n3. \u201C\u81EA\u9009\u76F8\u5173\u201D\u8981\u4F18\u5148\u70B9\u540D\u4E0E\u81EA\u9009\u80A1\u3001\u884C\u4E1A\u6216\u9898\u6750\u76F4\u63A5\u76F8\u5173\u7684\u5185\u5BB9\uFF1B\u82E5\u6CA1\u6709\u76F4\u63A5\u547D\u4E2D\uFF0C\u4E5F\u8981\u660E\u786E\u5199\u51FA\u201C\u672A\u53D1\u73B0\u76F4\u63A5\u547D\u4E2D\u81EA\u9009\u80A1\u201D\u3002\n4. \u201C\u6F5C\u5728\u673A\u4F1A\u201D\u53EA\u4FDD\u7559\u5B58\u5728\u6E05\u6670\u50AC\u5316\u94FE\u6761\u7684\u65B9\u5411\uFF0C\u4F8B\u5982\u653F\u7B56\u3001\u4EA7\u4E1A\u8D8B\u52BF\u3001\u4E1A\u7EE9\u3001\u8BA2\u5355\u3001\u8D44\u91D1\u504F\u597D\u53D8\u5316\uFF1B\u6CA1\u6709\u660E\u786E\u673A\u4F1A\u65F6\u8981\u76F4\u8BF4\u3002\n5. \u201C\u98CE\u9669\u63D0\u793A\u201D\u8981\u6307\u51FA\u4E0D\u5229\u4E8E\u5F00\u76D8\u51B3\u7B56\u7684\u6270\u52A8\u9879\uFF0C\u4F8B\u5982\u76D1\u7BA1\u3001\u6D77\u5916\u6270\u52A8\u3001\u4E1A\u7EE9\u98CE\u9669\u3001\u9898\u6750\u9000\u6F6E\u3001\u6D88\u606F\u4E0D\u786E\u5B9A\u6027\u3002\n6. \u201C\u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\u201D\u8F93\u51FA 3-6 \u6761\u53EF\u6267\u884C\u89C2\u5BDF\u70B9\uFF0C\u5C3D\u91CF\u5199\u6E05\u695A\u5E94\u89C2\u5BDF\u7684\u80A1\u7968\u3001\u677F\u5757\u6216\u4FE1\u53F7\u3002\n7. \u4E0D\u8981\u7F16\u9020\u672A\u5728\u8F93\u5165\u4E2D\u51FA\u73B0\u7684\u516C\u53F8\u3001\u653F\u7B56\u3001\u884C\u4E1A\u4FE1\u606F\u6216\u5FEB\u8BAF\u7ED3\u8BBA\u3002\n8. \u8F93\u51FA\u6B63\u6587\u5373\u53EF\uFF0C\u4E0D\u8981\u9644\u52A0 JSON\u3002\n";
|
|
2
|
+
export declare const PRE_MARKET_BRIEF_SYSTEM_PROMPT = "\n\u4F60\u662F\u4E00\u4F4DA\u80A1\u5F00\u76D8\u524D\u8D44\u8BAF\u7B80\u62A5\u7F16\u8F91\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u76D8\u524D\u7A97\u53E3\u5185\u7684\u91D1\u5341\u6570\u636E\u6574\u7406\u5FEB\u8BAF\uFF0C\u4EE5\u53CA\u7528\u6237\u81EA\u9009\u80A1\u7684\u884C\u4E1A/\u9898\u6750\u4FE1\u606F\uFF0C\u751F\u6210\u4E00\u4EFD\u9002\u5408 9:20 \u63A8\u9001\u7684\u5F00\u76D8\u524D\u6458\u8981\u3002\n\n\u8F93\u51FA\u8981\u6C42\uFF1A\n1. \u6309\u4EE5\u4E0B\u6807\u9898\u8F93\u51FA\uFF0C\u6BCF\u8282\u4F7F\u7528 2-5 \u6761\u7B80\u6D01\u4E2D\u6587\u8981\u70B9\uFF1A\n- \u91CD\u5927\u8981\u95FB\n- \u81EA\u9009\u76F8\u5173\n- \u6F5C\u5728\u673A\u4F1A\n- \u98CE\u9669\u63D0\u793A\n- \u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\n2. \u201C\u91CD\u5927\u8981\u95FB\u201D\u53EA\u63D0\u70BC\u4F1A\u5F71\u54CD A \u80A1\u5F00\u76D8\u60C5\u7EEA\u3001\u884C\u4E1A\u98CE\u9669\u504F\u597D\u6216\u91CD\u8981\u4EA4\u6613\u7EBF\u7D22\u7684\u5185\u5BB9\uFF0C\u4E0D\u8981\u673A\u68B0\u7F57\u5217\u6240\u6709\u5FEB\u8BAF\u3002\n3. \u201C\u81EA\u9009\u76F8\u5173\u201D\u8981\u4F18\u5148\u70B9\u540D\u4E0E\u81EA\u9009\u80A1\u3001\u884C\u4E1A\u6216\u9898\u6750\u76F4\u63A5\u76F8\u5173\u7684\u5185\u5BB9\uFF1B\u82E5\u6CA1\u6709\u76F4\u63A5\u547D\u4E2D\uFF0C\u4E5F\u8981\u660E\u786E\u5199\u51FA\u201C\u672A\u53D1\u73B0\u76F4\u63A5\u547D\u4E2D\u81EA\u9009\u80A1\u201D\u3002\n4. \u201C\u6F5C\u5728\u673A\u4F1A\u201D\u53EA\u4FDD\u7559\u5B58\u5728\u6E05\u6670\u50AC\u5316\u94FE\u6761\u7684\u65B9\u5411\uFF0C\u4F8B\u5982\u653F\u7B56\u3001\u4EA7\u4E1A\u8D8B\u52BF\u3001\u4E1A\u7EE9\u3001\u8BA2\u5355\u3001\u8D44\u91D1\u504F\u597D\u53D8\u5316\uFF1B\u6CA1\u6709\u660E\u786E\u673A\u4F1A\u65F6\u8981\u76F4\u8BF4\u3002\n5. \u201C\u98CE\u9669\u63D0\u793A\u201D\u8981\u6307\u51FA\u4E0D\u5229\u4E8E\u5F00\u76D8\u51B3\u7B56\u7684\u6270\u52A8\u9879\uFF0C\u4F8B\u5982\u76D1\u7BA1\u3001\u6D77\u5916\u6270\u52A8\u3001\u4E1A\u7EE9\u98CE\u9669\u3001\u9898\u6750\u9000\u6F6E\u3001\u6D88\u606F\u4E0D\u786E\u5B9A\u6027\u3002\n6. \u201C\u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\u201D\u8F93\u51FA 3-6 \u6761\u53EF\u6267\u884C\u89C2\u5BDF\u70B9\uFF0C\u5C3D\u91CF\u5199\u6E05\u695A\u5E94\u89C2\u5BDF\u7684\u80A1\u7968\u3001\u677F\u5757\u6216\u4FE1\u53F7\u3002\n7. \u4E0D\u8981\u7F16\u9020\u672A\u5728\u8F93\u5165\u4E2D\u51FA\u73B0\u7684\u516C\u53F8\u3001\u653F\u7B56\u3001\u884C\u4E1A\u4FE1\u606F\u6216\u5FEB\u8BAF\u7ED3\u8BBA\u3002\n8. \u4E25\u7981\u53EA\u590D\u8FF0\u201C\u91D1\u5341\u6570\u636E\u6574\u7406\uFF1A...\u201D\u6807\u9898\u3002\u6BCF\u6761\u8981\u70B9\u90FD\u5FC5\u987B\u4F18\u5148\u4F7F\u7528\u8F93\u5165\u4E2D\u7684\u201C\u63D0\u70BC\u6458\u8981\u201D\u6216\u201C\u6B63\u6587\u8981\u70B9\u201D\uFF0C\u5199\u51FA\u81F3\u5C11\u4E00\u4E2A\u5177\u4F53\u4E8B\u5B9E\u3001\u5F71\u54CD\u94FE\u6761\u6216\u89C2\u5BDF\u65B9\u5411\u3002\n9. \u5982\u679C\u67D0\u6761\u6574\u7406\u5FEB\u8BAF\u53EA\u6709\u6807\u9898\u3001\u6CA1\u6709\u53EF\u7528\u7EC6\u8282\uFF0C\u53EF\u4EE5\u660E\u786E\u5199\u201C\u4EC5\u4E3A\u6807\u9898\u7EA7\u7EBF\u7D22\uFF0C\u7EC6\u8282\u4E0D\u8DB3\u201D\uFF0C\u4F46\u4E0D\u8981\u628A\u6807\u9898\u672C\u8EAB\u5F53\u6210\u5B8C\u6574\u7ED3\u8BBA\u3002\n10. \u8F93\u51FA\u6B63\u6587\u5373\u53EF\uFF0C\u4E0D\u8981\u9644\u52A0 JSON\u3002\n";
|
|
3
3
|
export declare function buildPreMarketBriefUserPrompt(params: {
|
|
4
4
|
windowStartAt: string;
|
|
5
5
|
windowEndAt: string;
|
|
@@ -7,6 +7,8 @@ export declare function buildPreMarketBriefUserPrompt(params: {
|
|
|
7
7
|
flashes: Array<{
|
|
8
8
|
publishedAt: string;
|
|
9
9
|
headline: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
keyPoints: string[];
|
|
10
12
|
content: string;
|
|
11
13
|
url: string;
|
|
12
14
|
matchedSymbols: string[];
|
|
@@ -14,7 +14,9 @@ export const PRE_MARKET_BRIEF_SYSTEM_PROMPT = `
|
|
|
14
14
|
5. “风险提示”要指出不利于开盘决策的扰动项,例如监管、海外扰动、业绩风险、题材退潮、消息不确定性。
|
|
15
15
|
6. “开盘前关注清单”输出 3-6 条可执行观察点,尽量写清楚应观察的股票、板块或信号。
|
|
16
16
|
7. 不要编造未在输入中出现的公司、政策、行业信息或快讯结论。
|
|
17
|
-
8.
|
|
17
|
+
8. 严禁只复述“金十数据整理:...”标题。每条要点都必须优先使用输入中的“提炼摘要”或“正文要点”,写出至少一个具体事实、影响链条或观察方向。
|
|
18
|
+
9. 如果某条整理快讯只有标题、没有可用细节,可以明确写“仅为标题级线索,细节不足”,但不要把标题本身当成完整结论。
|
|
19
|
+
10. 输出正文即可,不要附加 JSON。
|
|
18
20
|
`;
|
|
19
21
|
export function buildPreMarketBriefUserPrompt(params) {
|
|
20
22
|
return [
|
|
@@ -43,6 +45,8 @@ function formatFlash(index, flash) {
|
|
|
43
45
|
return [
|
|
44
46
|
`${index}. [${flash.publishedAt}] ${flash.headline || "未提取到标题"}`,
|
|
45
47
|
` 关联提示: ${flash.matchedSymbols.length > 0 ? flash.matchedSymbols.join("、") : "无直接规则命中"}`,
|
|
48
|
+
` 提炼摘要: ${flash.summary}`,
|
|
49
|
+
` 正文要点: ${flash.keyPoints.length > 0 ? flash.keyPoints.map((item) => `- ${item}`).join("; ") : "未提取到稳定要点"}`,
|
|
46
50
|
` 正文: ${flash.content}`,
|
|
47
51
|
` 来源: ${flash.url}`,
|
|
48
52
|
].join("\n");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IndustryPeerContext } from "../analysis/types/composite-analysis.js";
|
|
2
|
+
import { QuoteService } from "./quote-service.js";
|
|
3
|
+
import { TickFlowUniverseService } from "./tickflow-universe-service.js";
|
|
4
|
+
export declare class IndustryPeerService {
|
|
5
|
+
private readonly universeService;
|
|
6
|
+
private readonly quoteService;
|
|
7
|
+
constructor(universeService: TickFlowUniverseService | null, quoteService: QuoteService);
|
|
8
|
+
buildContext(symbol: string): Promise<IndustryPeerContext>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const MAX_PEER_MOVERS = 3;
|
|
2
|
+
export class IndustryPeerService {
|
|
3
|
+
universeService;
|
|
4
|
+
quoteService;
|
|
5
|
+
constructor(universeService, quoteService) {
|
|
6
|
+
this.universeService = universeService;
|
|
7
|
+
this.quoteService = quoteService;
|
|
8
|
+
}
|
|
9
|
+
async buildContext(symbol) {
|
|
10
|
+
if (!this.universeService) {
|
|
11
|
+
return buildUnavailableContext("当前 TickFlow API Key Level 不支持标的池,已跳过申万三级同业表现。");
|
|
12
|
+
}
|
|
13
|
+
const industryProfile = await this.universeService.resolveIndustryProfile(symbol);
|
|
14
|
+
if (!industryProfile?.sw3UniverseId || !industryProfile.sw3Name) {
|
|
15
|
+
return buildUnavailableContext("未获取到可用的申万3级行业映射。");
|
|
16
|
+
}
|
|
17
|
+
const peerSymbols = await this.universeService.listUniverseSymbols(industryProfile.sw3UniverseId);
|
|
18
|
+
if (peerSymbols.length === 0) {
|
|
19
|
+
return buildUnavailableContext(`申万3级 ${industryProfile.sw3Name} 暂无可用成分股。`, industryProfile);
|
|
20
|
+
}
|
|
21
|
+
const quotes = await this.quoteService.fetchQuotes(peerSymbols);
|
|
22
|
+
const snapshots = quotes
|
|
23
|
+
.map(toPeerQuoteSnapshot)
|
|
24
|
+
.filter((item) => item != null)
|
|
25
|
+
.sort((left, right) => right.changePct - left.changePct || left.symbol.localeCompare(right.symbol));
|
|
26
|
+
if (snapshots.length === 0) {
|
|
27
|
+
return buildUnavailableContext(`申万3级 ${industryProfile.sw3Name} 暂未返回有效行情。`, industryProfile);
|
|
28
|
+
}
|
|
29
|
+
const targetIndex = snapshots.findIndex((item) => item.symbol === symbol);
|
|
30
|
+
const target = targetIndex >= 0 ? snapshots[targetIndex] : null;
|
|
31
|
+
const others = snapshots.filter((item) => item.symbol !== symbol);
|
|
32
|
+
const advanceCount = others.filter((item) => item.changePct > 0.0001).length;
|
|
33
|
+
const declineCount = others.filter((item) => item.changePct < -0.0001).length;
|
|
34
|
+
const flatCount = Math.max(0, others.length - advanceCount - declineCount);
|
|
35
|
+
const changeValues = others.map((item) => item.changePct);
|
|
36
|
+
const averageChangePct = changeValues.length > 0 ? average(changeValues) : null;
|
|
37
|
+
const medianChangePct = changeValues.length > 0 ? median(changeValues) : null;
|
|
38
|
+
const leaders = others.slice(0, MAX_PEER_MOVERS).map(toPeerMover);
|
|
39
|
+
const laggards = [...others]
|
|
40
|
+
.sort((left, right) => left.changePct - right.changePct || left.symbol.localeCompare(right.symbol))
|
|
41
|
+
.slice(0, MAX_PEER_MOVERS)
|
|
42
|
+
.map(toPeerMover);
|
|
43
|
+
const targetRank = targetIndex >= 0 ? targetIndex + 1 : null;
|
|
44
|
+
const targetPercentile = targetRank != null && snapshots.length > 1
|
|
45
|
+
? 1 - ((targetRank - 1) / (snapshots.length - 1))
|
|
46
|
+
: targetRank != null ? 1 : null;
|
|
47
|
+
return {
|
|
48
|
+
available: true,
|
|
49
|
+
summary: buildSummary({
|
|
50
|
+
industryName: industryProfile.sw3Name,
|
|
51
|
+
peerCount: snapshots.length,
|
|
52
|
+
otherStockCount: others.length,
|
|
53
|
+
advanceCount,
|
|
54
|
+
declineCount,
|
|
55
|
+
flatCount,
|
|
56
|
+
averageChangePct,
|
|
57
|
+
medianChangePct,
|
|
58
|
+
target,
|
|
59
|
+
targetRank,
|
|
60
|
+
}),
|
|
61
|
+
sw1Name: industryProfile.sw1Name,
|
|
62
|
+
sw2Name: industryProfile.sw2Name,
|
|
63
|
+
sw3Name: industryProfile.sw3Name,
|
|
64
|
+
sw3UniverseId: industryProfile.sw3UniverseId,
|
|
65
|
+
peerCount: snapshots.length,
|
|
66
|
+
otherStockCount: others.length,
|
|
67
|
+
advanceCount,
|
|
68
|
+
declineCount,
|
|
69
|
+
flatCount,
|
|
70
|
+
averageChangePct,
|
|
71
|
+
medianChangePct,
|
|
72
|
+
targetChangePct: target?.changePct ?? null,
|
|
73
|
+
targetRank,
|
|
74
|
+
targetPercentile,
|
|
75
|
+
leaders,
|
|
76
|
+
laggards,
|
|
77
|
+
note: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function toPeerQuoteSnapshot(quote) {
|
|
82
|
+
const prevClose = Number(quote.prev_close ?? 0);
|
|
83
|
+
const lastPrice = Number(quote.last_price ?? 0);
|
|
84
|
+
if (!Number.isFinite(prevClose) || !Number.isFinite(lastPrice) || prevClose <= 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
symbol: String(quote.symbol ?? "").trim(),
|
|
89
|
+
name: String(quote.name ?? quote.ext?.name ?? quote.symbol ?? "").trim(),
|
|
90
|
+
changePct: ((lastPrice - prevClose) / prevClose) * 100,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function toPeerMover(item) {
|
|
94
|
+
return {
|
|
95
|
+
symbol: item.symbol,
|
|
96
|
+
name: item.name || item.symbol,
|
|
97
|
+
changePct: item.changePct,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildSummary(input) {
|
|
101
|
+
const parts = [
|
|
102
|
+
`申万3级 ${input.industryName} 共 ${input.peerCount} 只`,
|
|
103
|
+
`除本股外上涨 ${input.advanceCount} / 下跌 ${input.declineCount} / 平 ${input.flatCount}`,
|
|
104
|
+
];
|
|
105
|
+
if (input.averageChangePct != null) {
|
|
106
|
+
parts.push(`均值 ${formatSignedPct(input.averageChangePct)}`);
|
|
107
|
+
}
|
|
108
|
+
if (input.medianChangePct != null) {
|
|
109
|
+
parts.push(`中位数 ${formatSignedPct(input.medianChangePct)}`);
|
|
110
|
+
}
|
|
111
|
+
if (input.target && input.targetRank != null) {
|
|
112
|
+
parts.push(`本股 ${formatSignedPct(input.target.changePct)},位列 ${input.targetRank}/${input.peerCount}`);
|
|
113
|
+
}
|
|
114
|
+
return parts.join(";");
|
|
115
|
+
}
|
|
116
|
+
function buildUnavailableContext(note, profile) {
|
|
117
|
+
return {
|
|
118
|
+
available: false,
|
|
119
|
+
summary: note,
|
|
120
|
+
sw1Name: profile?.sw1Name ?? null,
|
|
121
|
+
sw2Name: profile?.sw2Name ?? null,
|
|
122
|
+
sw3Name: profile?.sw3Name ?? null,
|
|
123
|
+
sw3UniverseId: profile?.sw3UniverseId ?? null,
|
|
124
|
+
peerCount: 0,
|
|
125
|
+
otherStockCount: 0,
|
|
126
|
+
advanceCount: 0,
|
|
127
|
+
declineCount: 0,
|
|
128
|
+
flatCount: 0,
|
|
129
|
+
averageChangePct: null,
|
|
130
|
+
medianChangePct: null,
|
|
131
|
+
targetChangePct: null,
|
|
132
|
+
targetRank: null,
|
|
133
|
+
targetPercentile: null,
|
|
134
|
+
leaders: [],
|
|
135
|
+
laggards: [],
|
|
136
|
+
note,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function average(values) {
|
|
140
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
141
|
+
}
|
|
142
|
+
function median(values) {
|
|
143
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
144
|
+
const middle = Math.floor(sorted.length / 2);
|
|
145
|
+
if (sorted.length % 2 === 0) {
|
|
146
|
+
return (sorted[middle - 1] + sorted[middle]) / 2;
|
|
147
|
+
}
|
|
148
|
+
return sorted[middle] ?? 0;
|
|
149
|
+
}
|
|
150
|
+
function formatSignedPct(value) {
|
|
151
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(2)}%`;
|
|
152
|
+
}
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { chinaHour, chinaToday, formatChinaDateTime } from "../utils/china-time.js";
|
|
7
|
+
import { extractSectorKeywords } from "./watchlist-profile-service.js";
|
|
7
8
|
const DEFAULT_STATE = {
|
|
8
9
|
initialized: false,
|
|
9
10
|
lastSeenKey: null,
|
|
@@ -446,7 +447,7 @@ function buildDirectKeywords(item) {
|
|
|
446
447
|
}
|
|
447
448
|
function buildBoardKeywords(item) {
|
|
448
449
|
return uniqueStrings([
|
|
449
|
-
item.sector
|
|
450
|
+
...extractSectorKeywords(item.sector),
|
|
450
451
|
...item.themes,
|
|
451
452
|
]).filter((keyword) => isUsefulBoardKeyword(keyword));
|
|
452
453
|
}
|