tickflow-assist 0.3.5 → 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 +9 -40
- package/dist/analysis/types/composite-analysis.d.ts +27 -0
- package/dist/bootstrap.js +19 -5
- 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/alert-service.d.ts +8 -0
- package/dist/services/alert-service.js +327 -45
- 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 +4 -1
- package/dist/services/monitor-service.js +51 -20
- 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 +58 -16
- package/dist/types/tickflow.d.ts +12 -0
- package/dist/utils/alert-diagnostic-log.d.ts +12 -0
- package/dist/utils/alert-diagnostic-log.js +60 -0
- package/dist/utils/tickflow-quote.d.ts +5 -0
- package/dist/utils/tickflow-quote.js +31 -0
- package/openclaw.plugin.json +108 -2
- package/package.json +10 -5
- package/skills/stock-analysis/SKILL.md +9 -20
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MonitorState } from "../types/monitor.js";
|
|
2
|
+
import { AlertDiagnosticLogger } from "../utils/alert-diagnostic-log.js";
|
|
2
3
|
import { QuoteService } from "./quote-service.js";
|
|
3
4
|
import { TradingCalendarService } from "./trading-calendar-service.js";
|
|
4
5
|
import { WatchlistService } from "./watchlist-service.js";
|
|
@@ -23,7 +24,8 @@ export declare class MonitorService {
|
|
|
23
24
|
private readonly klineService;
|
|
24
25
|
private readonly alertService;
|
|
25
26
|
private readonly alertMediaService;
|
|
26
|
-
|
|
27
|
+
private readonly diagnosticLogger?;
|
|
28
|
+
constructor(baseDir: string, requestInterval: number, alertChannel: string, watchlistService: WatchlistService, quoteService: QuoteService, tradingCalendarService: TradingCalendarService, keyLevelsRepository: KeyLevelsRepository, alertLogRepository: AlertLogRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, klineService: KlineService, alertService: AlertService, alertMediaService: AlertMediaService, diagnosticLogger?: AlertDiagnosticLogger | undefined);
|
|
27
29
|
start(): Promise<string>;
|
|
28
30
|
stop(): Promise<string>;
|
|
29
31
|
enableManagedLoop(): Promise<{
|
|
@@ -59,4 +61,5 @@ export declare class MonitorService {
|
|
|
59
61
|
private cleanupAlertMedia;
|
|
60
62
|
private getRunLockFilePath;
|
|
61
63
|
private getAlertClaimFilePath;
|
|
64
|
+
private logDiagnostic;
|
|
62
65
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { basenameOrUndefined, buildAlertMessageHash, truncateDiagnosticText, } from "../utils/alert-diagnostic-log.js";
|
|
3
4
|
import { formatChinaDateTime } from "../utils/china-time.js";
|
|
4
5
|
import { calculateProfitPct, formatCostPrice } from "../utils/cost-price.js";
|
|
6
|
+
import { resolveTickFlowQuoteChangePct } from "../utils/tickflow-quote.js";
|
|
5
7
|
const DEFAULT_STATE = {
|
|
6
8
|
running: false,
|
|
7
9
|
startedAt: null,
|
|
@@ -36,7 +38,8 @@ export class MonitorService {
|
|
|
36
38
|
klineService;
|
|
37
39
|
alertService;
|
|
38
40
|
alertMediaService;
|
|
39
|
-
|
|
41
|
+
diagnosticLogger;
|
|
42
|
+
constructor(baseDir, requestInterval, alertChannel, watchlistService, quoteService, tradingCalendarService, keyLevelsRepository, alertLogRepository, klinesRepository, intradayKlinesRepository, klineService, alertService, alertMediaService, diagnosticLogger) {
|
|
40
43
|
this.baseDir = baseDir;
|
|
41
44
|
this.requestInterval = requestInterval;
|
|
42
45
|
this.alertChannel = alertChannel;
|
|
@@ -50,6 +53,7 @@ export class MonitorService {
|
|
|
50
53
|
this.klineService = klineService;
|
|
51
54
|
this.alertService = alertService;
|
|
52
55
|
this.alertMediaService = alertMediaService;
|
|
56
|
+
this.diagnosticLogger = diagnosticLogger;
|
|
53
57
|
}
|
|
54
58
|
async start() {
|
|
55
59
|
const watchlist = await this.watchlistService.list();
|
|
@@ -464,21 +468,54 @@ export class MonitorService {
|
|
|
464
468
|
}
|
|
465
469
|
async trySendAlert(symbol, ruleName, input) {
|
|
466
470
|
const sessionKey = getSessionKey();
|
|
471
|
+
const message = typeof input === "string" ? input : input.message;
|
|
472
|
+
const messageHash = buildAlertMessageHash(message);
|
|
473
|
+
const hasMedia = typeof input !== "string" && Boolean(input.mediaPath);
|
|
474
|
+
await this.logDiagnostic("try_send_alert_enter", {
|
|
475
|
+
symbol,
|
|
476
|
+
ruleName,
|
|
477
|
+
sessionKey,
|
|
478
|
+
messageHash,
|
|
479
|
+
hasMedia,
|
|
480
|
+
mediaFile: typeof input === "string" ? undefined : basenameOrUndefined(input.mediaPath),
|
|
481
|
+
});
|
|
467
482
|
const claim = await this.tryAcquireAlertClaim(symbol, ruleName, sessionKey);
|
|
468
483
|
if (!claim) {
|
|
484
|
+
await this.logDiagnostic("try_send_alert_claim_busy", {
|
|
485
|
+
symbol,
|
|
486
|
+
ruleName,
|
|
487
|
+
sessionKey,
|
|
488
|
+
messageHash,
|
|
489
|
+
});
|
|
469
490
|
await this.cleanupAlertMedia(input);
|
|
470
491
|
return false;
|
|
471
492
|
}
|
|
472
493
|
try {
|
|
473
494
|
if (await this.alertLogRepository.isSentThisSession(symbol, ruleName, sessionKey)) {
|
|
495
|
+
await this.logDiagnostic("try_send_alert_already_sent", {
|
|
496
|
+
symbol,
|
|
497
|
+
ruleName,
|
|
498
|
+
sessionKey,
|
|
499
|
+
messageHash,
|
|
500
|
+
});
|
|
474
501
|
await this.cleanupAlertMedia(input);
|
|
475
502
|
return false;
|
|
476
503
|
}
|
|
477
504
|
const result = await this.sendAlertAndCleanupMedia(input);
|
|
478
|
-
|
|
505
|
+
await this.logDiagnostic("try_send_alert_result", {
|
|
506
|
+
symbol,
|
|
507
|
+
ruleName,
|
|
508
|
+
sessionKey,
|
|
509
|
+
messageHash,
|
|
510
|
+
ok: result.ok,
|
|
511
|
+
mediaAttempted: result.mediaAttempted,
|
|
512
|
+
mediaDelivered: result.mediaDelivered,
|
|
513
|
+
deliveryUncertain: result.deliveryUncertain === true,
|
|
514
|
+
error: result.error ? truncateDiagnosticText(result.error) : null,
|
|
515
|
+
});
|
|
516
|
+
if (!result.ok && !result.deliveryUncertain) {
|
|
479
517
|
return false;
|
|
480
518
|
}
|
|
481
|
-
const message = typeof input === "string" ? input : input.message;
|
|
482
519
|
await this.alertLogRepository.append({
|
|
483
520
|
symbol,
|
|
484
521
|
alert_date: sessionKey,
|
|
@@ -486,6 +523,12 @@ export class MonitorService {
|
|
|
486
523
|
message,
|
|
487
524
|
triggered_at: formatChinaDateTime(),
|
|
488
525
|
});
|
|
526
|
+
await this.logDiagnostic("try_send_alert_logged", {
|
|
527
|
+
symbol,
|
|
528
|
+
ruleName,
|
|
529
|
+
sessionKey,
|
|
530
|
+
messageHash,
|
|
531
|
+
});
|
|
489
532
|
return true;
|
|
490
533
|
}
|
|
491
534
|
finally {
|
|
@@ -616,6 +659,9 @@ export class MonitorService {
|
|
|
616
659
|
getAlertClaimFilePath(symbol, ruleName, sessionKey) {
|
|
617
660
|
return path.join(this.baseDir, "alert-claims", `${sanitizeAlertClaimPart(sessionKey)}_${sanitizeAlertClaimPart(symbol)}_${sanitizeAlertClaimPart(ruleName)}.lock`);
|
|
618
661
|
}
|
|
662
|
+
async logDiagnostic(event, details) {
|
|
663
|
+
await this.diagnosticLogger?.append("monitor_service", event, details);
|
|
664
|
+
}
|
|
619
665
|
}
|
|
620
666
|
function formatRunningState(state, requestInterval) {
|
|
621
667
|
const heartbeat = getHeartbeatStatus(state, requestInterval);
|
|
@@ -692,13 +738,7 @@ function formatTradingPhase(phase) {
|
|
|
692
738
|
}
|
|
693
739
|
function formatQuoteLine(item, quote) {
|
|
694
740
|
const lastPrice = Number(quote.last_price ?? 0);
|
|
695
|
-
const
|
|
696
|
-
const tickflowChangePct = quote.ext?.change_pct;
|
|
697
|
-
const changePct = tickflowChangePct != null
|
|
698
|
-
? Number(tickflowChangePct) * 100
|
|
699
|
-
: prevClose > 0
|
|
700
|
-
? ((lastPrice - prevClose) / prevClose) * 100
|
|
701
|
-
: null;
|
|
741
|
+
const changePct = resolveTickFlowQuoteChangePct(quote);
|
|
702
742
|
const quoteTime = formatQuoteTimestamp(quote.timestamp);
|
|
703
743
|
const profitPct = calculateProfitPct(lastPrice, item.costPrice);
|
|
704
744
|
let line = `• ${item.name}(${item.symbol}) ${lastPrice.toFixed(2)}`;
|
|
@@ -962,14 +1002,5 @@ function resolveAlertImageLabel(ruleName, fallbackTitle) {
|
|
|
962
1002
|
}
|
|
963
1003
|
}
|
|
964
1004
|
function getQuoteChangePct(quote) {
|
|
965
|
-
|
|
966
|
-
if (tickflowChangePct != null && Number.isFinite(Number(tickflowChangePct))) {
|
|
967
|
-
return Number(tickflowChangePct) * 100;
|
|
968
|
-
}
|
|
969
|
-
const lastPrice = Number(quote.last_price ?? 0);
|
|
970
|
-
const prevClose = Number(quote.prev_close ?? 0);
|
|
971
|
-
if (!(prevClose > 0)) {
|
|
972
|
-
return null;
|
|
973
|
-
}
|
|
974
|
-
return ((lastPrice - prevClose) / prevClose) * 100;
|
|
1005
|
+
return resolveTickFlowQuoteChangePct(quote);
|
|
975
1006
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WatchlistItem } from "../types/domain.js";
|
|
2
2
|
import { CompositeAnalysisOrchestrator } from "../analysis/orchestrators/composite-analysis.orchestrator.js";
|
|
3
|
-
import type { CompositeAnalysisResult, PostCloseReviewResult, PriorKeyLevelValidationContext } from "../analysis/types/composite-analysis.js";
|
|
3
|
+
import type { CompositeAnalysisResult, IndustryPeerContext, PostCloseReviewResult, PriorKeyLevelValidationContext } from "../analysis/types/composite-analysis.js";
|
|
4
4
|
import { PostCloseReviewTask } from "../analysis/tasks/post-close-review.task.js";
|
|
5
5
|
import { WatchlistService } from "./watchlist-service.js";
|
|
6
6
|
import { AnalysisService } from "./analysis-service.js";
|
|
@@ -10,6 +10,11 @@ import { KlinesRepository } from "../storage/repositories/klines-repo.js";
|
|
|
10
10
|
import { IntradayKlinesRepository } from "../storage/repositories/intraday-klines-repo.js";
|
|
11
11
|
import { Jin10FlashDeliveryRepository } from "../storage/repositories/jin10-flash-delivery-repo.js";
|
|
12
12
|
import { Jin10FlashRepository } from "../storage/repositories/jin10-flash-repo.js";
|
|
13
|
+
import { IndustryPeerService } from "./industry-peer-service.js";
|
|
14
|
+
interface ReviewMarketSummary {
|
|
15
|
+
latestClose: number | null;
|
|
16
|
+
dailyChangePct: number | null;
|
|
17
|
+
}
|
|
13
18
|
export interface PostCloseReviewRunResult {
|
|
14
19
|
overviewMessage: string;
|
|
15
20
|
detailMessages: string[];
|
|
@@ -26,7 +31,8 @@ export declare class PostCloseReviewService {
|
|
|
26
31
|
private readonly intradayKlinesRepository;
|
|
27
32
|
private readonly flashDeliveryRepository;
|
|
28
33
|
private readonly flashRepository;
|
|
29
|
-
|
|
34
|
+
private readonly industryPeerService;
|
|
35
|
+
constructor(watchlistService: WatchlistService, compositeAnalysisOrchestrator: CompositeAnalysisOrchestrator, analysisService: AnalysisService, postCloseReviewTask: PostCloseReviewTask, keyLevelsRepository: KeyLevelsRepository, keyLevelsHistoryRepository: KeyLevelsHistoryRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository, flashRepository: Jin10FlashRepository, industryPeerService: IndustryPeerService);
|
|
30
36
|
run(): Promise<PostCloseReviewRunResult>;
|
|
31
37
|
private persistReview;
|
|
32
38
|
private persistFallbackCompositeReview;
|
|
@@ -36,5 +42,6 @@ export declare class PostCloseReviewService {
|
|
|
36
42
|
private formatDetailMessage;
|
|
37
43
|
private formatFailureMessage;
|
|
38
44
|
}
|
|
39
|
-
export declare function formatPostCloseReviewDetailMessage(item: WatchlistItem, validation: PriorKeyLevelValidationContext, review: PostCloseReviewResult): string;
|
|
40
|
-
export declare function formatPostCloseReviewFailureMessage(item: WatchlistItem, errorMessage: string, compositeResult: CompositeAnalysisResult | null): string;
|
|
45
|
+
export declare function formatPostCloseReviewDetailMessage(item: WatchlistItem, validation: PriorKeyLevelValidationContext, review: PostCloseReviewResult, marketSummary?: ReviewMarketSummary | null, peerContext?: IndustryPeerContext | null): string;
|
|
46
|
+
export declare function formatPostCloseReviewFailureMessage(item: WatchlistItem, errorMessage: string, compositeResult: CompositeAnalysisResult | null, marketSummary?: ReviewMarketSummary | null): string;
|
|
47
|
+
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { formatChinaDateTime } from "../utils/china-time.js";
|
|
2
|
+
import { formatCostPrice } from "../utils/cost-price.js";
|
|
3
|
+
import { normalizeTickFlowChangePct, resolveTickFlowKlineChangePct } from "../utils/tickflow-quote.js";
|
|
2
4
|
const LEVEL_BUFFER = 0.005;
|
|
3
5
|
const INTRADAY_PERIOD = "1m";
|
|
4
6
|
const MARKET_OVERVIEW_FLASH_KEYWORDS = [
|
|
@@ -17,7 +19,8 @@ export class PostCloseReviewService {
|
|
|
17
19
|
intradayKlinesRepository;
|
|
18
20
|
flashDeliveryRepository;
|
|
19
21
|
flashRepository;
|
|
20
|
-
|
|
22
|
+
industryPeerService;
|
|
23
|
+
constructor(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, flashDeliveryRepository, flashRepository, industryPeerService) {
|
|
21
24
|
this.watchlistService = watchlistService;
|
|
22
25
|
this.compositeAnalysisOrchestrator = compositeAnalysisOrchestrator;
|
|
23
26
|
this.analysisService = analysisService;
|
|
@@ -28,6 +31,7 @@ export class PostCloseReviewService {
|
|
|
28
31
|
this.intradayKlinesRepository = intradayKlinesRepository;
|
|
29
32
|
this.flashDeliveryRepository = flashDeliveryRepository;
|
|
30
33
|
this.flashRepository = flashRepository;
|
|
34
|
+
this.industryPeerService = industryPeerService;
|
|
31
35
|
}
|
|
32
36
|
async run() {
|
|
33
37
|
const watchlist = await this.watchlistService.list();
|
|
@@ -44,11 +48,35 @@ export class PostCloseReviewService {
|
|
|
44
48
|
let marketOverview = null;
|
|
45
49
|
for (const item of watchlist) {
|
|
46
50
|
let compositeResult = null;
|
|
51
|
+
let marketSummary = null;
|
|
47
52
|
try {
|
|
48
53
|
const input = await this.compositeAnalysisOrchestrator.buildInput(item.symbol);
|
|
54
|
+
marketSummary = buildReviewMarketSummary(input.market.klines, input.market.realtimeQuote);
|
|
49
55
|
marketOverview ??= input.market.marketOverview;
|
|
50
56
|
const tradeDate = input.market.klines[input.market.klines.length - 1]?.trade_date ?? formatChinaDateTime().slice(0, 10);
|
|
51
57
|
const validation = await this.buildValidationContext(item.symbol, tradeDate);
|
|
58
|
+
const peerContext = await this.industryPeerService.buildContext(item.symbol)
|
|
59
|
+
.catch((error) => ({
|
|
60
|
+
available: false,
|
|
61
|
+
summary: "未获取到申万三级同业表现。",
|
|
62
|
+
sw1Name: null,
|
|
63
|
+
sw2Name: null,
|
|
64
|
+
sw3Name: null,
|
|
65
|
+
sw3UniverseId: null,
|
|
66
|
+
peerCount: 0,
|
|
67
|
+
otherStockCount: 0,
|
|
68
|
+
advanceCount: 0,
|
|
69
|
+
declineCount: 0,
|
|
70
|
+
flatCount: 0,
|
|
71
|
+
averageChangePct: null,
|
|
72
|
+
medianChangePct: null,
|
|
73
|
+
targetChangePct: null,
|
|
74
|
+
targetRank: null,
|
|
75
|
+
targetPercentile: null,
|
|
76
|
+
leaders: [],
|
|
77
|
+
laggards: [],
|
|
78
|
+
note: error instanceof Error ? error.message : String(error),
|
|
79
|
+
}));
|
|
52
80
|
compositeResult = await this.compositeAnalysisOrchestrator.analyzeInput(input);
|
|
53
81
|
const flashContext = await this.buildFlashContext(item.symbol, tradeDate);
|
|
54
82
|
const review = await this.analysisService.runTask(this.postCloseReviewTask, {
|
|
@@ -56,8 +84,9 @@ export class PostCloseReviewService {
|
|
|
56
84
|
compositeResult,
|
|
57
85
|
validation,
|
|
58
86
|
flashContext,
|
|
87
|
+
peerContext,
|
|
59
88
|
});
|
|
60
|
-
const message = this.formatDetailMessage(item, validation, review);
|
|
89
|
+
const message = this.formatDetailMessage(item, validation, review, marketSummary, peerContext);
|
|
61
90
|
await this.persistReview(item.symbol, message, review);
|
|
62
91
|
entries.push({
|
|
63
92
|
ok: true,
|
|
@@ -73,7 +102,7 @@ export class PostCloseReviewService {
|
|
|
73
102
|
await this.persistFallbackCompositeReview(item.symbol, compositeResult);
|
|
74
103
|
}
|
|
75
104
|
entries.push({ ok: false, item, errorMessage: message });
|
|
76
|
-
detailMessages.push(this.formatFailureMessage(item, message, compositeResult));
|
|
105
|
+
detailMessages.push(this.formatFailureMessage(item, message, compositeResult, marketSummary));
|
|
77
106
|
}
|
|
78
107
|
}
|
|
79
108
|
const overviewMessage = this.formatOverviewMessage(marketOverview, entries);
|
|
@@ -207,17 +236,20 @@ export class PostCloseReviewService {
|
|
|
207
236
|
];
|
|
208
237
|
return `**🧭 收盘复盘总览**\n\n${lines.join("\n")}`.trim();
|
|
209
238
|
}
|
|
210
|
-
formatDetailMessage(item, validation, review) {
|
|
211
|
-
return formatPostCloseReviewDetailMessage(item, validation, review);
|
|
239
|
+
formatDetailMessage(item, validation, review, marketSummary, peerContext = null) {
|
|
240
|
+
return formatPostCloseReviewDetailMessage(item, validation, review, marketSummary, peerContext);
|
|
212
241
|
}
|
|
213
|
-
formatFailureMessage(item, errorMessage, compositeResult) {
|
|
214
|
-
return formatPostCloseReviewFailureMessage(item, errorMessage, compositeResult);
|
|
242
|
+
formatFailureMessage(item, errorMessage, compositeResult, marketSummary) {
|
|
243
|
+
return formatPostCloseReviewFailureMessage(item, errorMessage, compositeResult, marketSummary);
|
|
215
244
|
}
|
|
216
245
|
}
|
|
217
|
-
export function formatPostCloseReviewDetailMessage(item, validation, review) {
|
|
246
|
+
export function formatPostCloseReviewDetailMessage(item, validation, review, marketSummary = null, peerContext = null) {
|
|
247
|
+
const marketMeta = formatReviewMarketMeta(item, marketSummary);
|
|
248
|
+
const industryPosition = formatIndustryPosition(peerContext);
|
|
218
249
|
const lines = [
|
|
219
250
|
`**📘 收盘复盘|${item.name}(${item.symbol})**`,
|
|
220
251
|
`${formatValidationVerdictBadge(validation.verdict)} 昨日验证:${formatValidationVerdictLabel(validation.verdict)} | ${formatDecisionBadge(review.decision)} 明日处理:${formatDecisionLabel(review.decision)}`,
|
|
252
|
+
...(marketMeta ? [marketMeta] : []),
|
|
221
253
|
"",
|
|
222
254
|
formatSectionTitle("📍", "昨日关键位验证"),
|
|
223
255
|
`• 结论:${validation.summary}`,
|
|
@@ -227,7 +259,11 @@ export function formatPostCloseReviewDetailMessage(item, validation, review) {
|
|
|
227
259
|
review.sessionSummary || "未生成盘面一句话总结。",
|
|
228
260
|
"",
|
|
229
261
|
formatSectionTitle("🌐", "大盘与板块"),
|
|
230
|
-
|
|
262
|
+
[
|
|
263
|
+
`• 风向:大盘 ${formatMarketBiasBadge(review.marketBias)}${formatMarketBiasLabel(review.marketBias)}`,
|
|
264
|
+
`板块 ${formatMarketBiasBadge(review.sectorBias)}${formatMarketBiasLabel(review.sectorBias)}`,
|
|
265
|
+
industryPosition ? `同业 ${industryPosition}` : null,
|
|
266
|
+
].filter(Boolean).join(" | "),
|
|
231
267
|
review.marketSectorSummary || "未生成大盘/板块总结。",
|
|
232
268
|
"",
|
|
233
269
|
formatSectionTitle("📰", "新闻与公告"),
|
|
@@ -255,12 +291,14 @@ export function formatPostCloseReviewDetailMessage(item, validation, review) {
|
|
|
255
291
|
lines.push(`• 支撑 ${formatMaybePrice(review.levels.support)} | 压力 ${formatMaybePrice(review.levels.resistance)} | 突破 ${formatMaybePrice(review.levels.breakthrough)}`, `• 止损 ${formatMaybePrice(review.levels.stop_loss)} | 止盈 ${formatMaybePrice(review.levels.take_profit)} | 评分 ${review.levels.score}/10`, ...(levelRail ? [`• 价位框架:${levelRail}`] : []), "", formatSectionTitle("✅", "操作建议"), review.actionAdvice || "按关键位和次日量价配合再决定是否执行。");
|
|
256
292
|
return lines.join("\n");
|
|
257
293
|
}
|
|
258
|
-
export function formatPostCloseReviewFailureMessage(item, errorMessage, compositeResult) {
|
|
294
|
+
export function formatPostCloseReviewFailureMessage(item, errorMessage, compositeResult, marketSummary = null) {
|
|
259
295
|
const fallback = compositeResult?.levels
|
|
260
296
|
? "已保留综合分析生成的关键位,可稍后用 view_analysis 或 analyze 复核。"
|
|
261
297
|
: "本轮未生成可用关键位。";
|
|
298
|
+
const marketMeta = formatReviewMarketMeta(item, marketSummary);
|
|
262
299
|
return [
|
|
263
300
|
`**⚠️ 收盘复盘|${item.name}(${item.symbol})**`,
|
|
301
|
+
...(marketMeta ? ["", marketMeta] : []),
|
|
264
302
|
"",
|
|
265
303
|
formatSectionTitle("❌", "失败原因"),
|
|
266
304
|
errorMessage,
|
|
@@ -289,6 +327,71 @@ function toHistoryEntry(symbol, analysisText, levels) {
|
|
|
289
327
|
score: levels.score,
|
|
290
328
|
};
|
|
291
329
|
}
|
|
330
|
+
function buildReviewMarketSummary(klines, realtimeQuote) {
|
|
331
|
+
const latestKline = klines[klines.length - 1] ?? null;
|
|
332
|
+
if (!latestKline && !realtimeQuote) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const latestClose = latestKline?.close ?? realtimeQuote?.last_price ?? null;
|
|
336
|
+
const dailyChangePct = normalizeTickFlowChangePct(realtimeQuote?.ext?.change_pct)
|
|
337
|
+
?? resolveTickFlowKlineChangePct(latestKline);
|
|
338
|
+
return {
|
|
339
|
+
latestClose,
|
|
340
|
+
dailyChangePct,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function formatReviewMarketMeta(item, marketSummary) {
|
|
344
|
+
const parts = [];
|
|
345
|
+
if (marketSummary?.latestClose != null && Number.isFinite(marketSummary.latestClose)) {
|
|
346
|
+
parts.push(`• 收盘 ${marketSummary.latestClose.toFixed(2)}`);
|
|
347
|
+
}
|
|
348
|
+
if (marketSummary?.dailyChangePct != null && Number.isFinite(marketSummary.dailyChangePct)) {
|
|
349
|
+
parts.push(`当日 ${formatSignedPct(marketSummary.dailyChangePct)}`);
|
|
350
|
+
}
|
|
351
|
+
if (item.costPrice != null && Number.isFinite(item.costPrice) && item.costPrice > 0) {
|
|
352
|
+
parts.push(`成本 ${formatCostPrice(item.costPrice)}`);
|
|
353
|
+
}
|
|
354
|
+
return parts.length > 0 ? parts.join(" | ") : null;
|
|
355
|
+
}
|
|
356
|
+
function formatSignedPct(value) {
|
|
357
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(2)}%`;
|
|
358
|
+
}
|
|
359
|
+
function formatIndustryPosition(context) {
|
|
360
|
+
if (!context?.available || !context.targetRank || !(context.peerCount > 0)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return `${classifyIndustryPosition(context)}(${context.targetRank}/${context.peerCount})`;
|
|
364
|
+
}
|
|
365
|
+
function classifyIndustryPosition(context) {
|
|
366
|
+
const { targetRank, peerCount } = context;
|
|
367
|
+
if (!targetRank || !(peerCount > 0)) {
|
|
368
|
+
return "位置未知";
|
|
369
|
+
}
|
|
370
|
+
if (targetRank === 1) {
|
|
371
|
+
return "领涨";
|
|
372
|
+
}
|
|
373
|
+
if (targetRank === peerCount) {
|
|
374
|
+
return "领跌";
|
|
375
|
+
}
|
|
376
|
+
if (peerCount <= 3) {
|
|
377
|
+
return "中游";
|
|
378
|
+
}
|
|
379
|
+
const percentile = context.targetPercentile
|
|
380
|
+
?? (peerCount > 1 ? 1 - ((targetRank - 1) / (peerCount - 1)) : 1);
|
|
381
|
+
if (percentile >= 0.8) {
|
|
382
|
+
return "领涨区";
|
|
383
|
+
}
|
|
384
|
+
if (percentile >= 0.6) {
|
|
385
|
+
return "偏强";
|
|
386
|
+
}
|
|
387
|
+
if (percentile > 0.4) {
|
|
388
|
+
return "中游";
|
|
389
|
+
}
|
|
390
|
+
if (percentile > 0.2) {
|
|
391
|
+
return "偏弱";
|
|
392
|
+
}
|
|
393
|
+
return "领跌区";
|
|
394
|
+
}
|
|
292
395
|
function evaluateSupport(snapshot, row) {
|
|
293
396
|
if (!(snapshot.support != null && snapshot.support > 0)) {
|
|
294
397
|
return "支撑: 昨日未设置支撑位。";
|
|
@@ -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;
|
|
@@ -80,10 +81,7 @@ export class PreMarketBriefService {
|
|
|
80
81
|
matchedWatchlistCount: 0,
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
|
-
const matchContexts = flashes.map((flash) => (
|
|
84
|
-
flash,
|
|
85
|
-
matchedItems: findMatchedItems(flash, watchlist),
|
|
86
|
-
}));
|
|
84
|
+
const matchContexts = flashes.map((flash) => buildFlashMatchContext(flash, watchlist));
|
|
87
85
|
const matchedWatchlistCount = new Set(matchContexts.flatMap((context) => context.matchedItems.map((item) => item.symbol))).size;
|
|
88
86
|
const summary = await this.buildSummary(window, watchlist, matchContexts);
|
|
89
87
|
return {
|
|
@@ -128,7 +126,9 @@ export class PreMarketBriefService {
|
|
|
128
126
|
watchlist,
|
|
129
127
|
flashes: matchContexts.map((context) => ({
|
|
130
128
|
publishedAt: context.flash.published_at,
|
|
131
|
-
headline:
|
|
129
|
+
headline: context.headline,
|
|
130
|
+
summary: context.summary,
|
|
131
|
+
keyPoints: context.keyPoints,
|
|
132
132
|
content: context.flash.content,
|
|
133
133
|
url: context.flash.url,
|
|
134
134
|
matchedSymbols: context.matchedItems.map((item) => item.symbol),
|
|
@@ -136,10 +136,13 @@ export class PreMarketBriefService {
|
|
|
136
136
|
};
|
|
137
137
|
if (this.analysisService.isConfigured()) {
|
|
138
138
|
try {
|
|
139
|
-
|
|
139
|
+
const generated = await this.analysisService.generateText(PRE_MARKET_BRIEF_SYSTEM_PROMPT, buildPreMarketBriefUserPrompt(promptInput), {
|
|
140
140
|
maxTokens: 1600,
|
|
141
141
|
temperature: 0.2,
|
|
142
142
|
});
|
|
143
|
+
if (!isLowSignalSummary(generated, matchContexts)) {
|
|
144
|
+
return generated;
|
|
145
|
+
}
|
|
143
146
|
}
|
|
144
147
|
catch {
|
|
145
148
|
// Fall through to deterministic fallback so the scheduled push still lands.
|
|
@@ -183,7 +186,7 @@ function findMatchedItems(flash, watchlist) {
|
|
|
183
186
|
const normalizedContent = normalizeText(flash.content);
|
|
184
187
|
return watchlist.filter((item) => {
|
|
185
188
|
const directKeywords = [item.symbol, item.symbol.slice(0, 6), item.name];
|
|
186
|
-
const boardKeywords = [item.sector
|
|
189
|
+
const boardKeywords = [...extractSectorKeywords(item.sector), ...item.themes]
|
|
187
190
|
.map((keyword) => keyword.replace(/\s+/g, "").trim())
|
|
188
191
|
.filter((keyword) => keyword.length >= 2);
|
|
189
192
|
return [...directKeywords, ...boardKeywords]
|
|
@@ -191,6 +194,16 @@ function findMatchedItems(flash, watchlist) {
|
|
|
191
194
|
.some((keyword) => keyword && normalizedContent.includes(keyword));
|
|
192
195
|
});
|
|
193
196
|
}
|
|
197
|
+
function buildFlashMatchContext(flash, watchlist) {
|
|
198
|
+
const insight = extractFlashInsight(flash.content);
|
|
199
|
+
return {
|
|
200
|
+
flash,
|
|
201
|
+
matchedItems: findMatchedItems(flash, watchlist),
|
|
202
|
+
headline: insight.headline,
|
|
203
|
+
summary: insight.summary,
|
|
204
|
+
keyPoints: insight.keyPoints,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
194
207
|
function buildFallbackSummary(matchContexts) {
|
|
195
208
|
const opportunityContexts = matchContexts.filter((context) => containsAnyKeyword(context.flash.content, OPPORTUNITY_KEYWORDS));
|
|
196
209
|
const riskContexts = matchContexts.filter((context) => containsAnyKeyword(context.flash.content, RISK_KEYWORDS));
|
|
@@ -220,7 +233,7 @@ function formatFlashBullets(contexts, limit) {
|
|
|
220
233
|
.slice(0, limit)
|
|
221
234
|
.map((context) => {
|
|
222
235
|
const time = context.flash.published_at.slice(11, 16);
|
|
223
|
-
return `• [${time}] ${
|
|
236
|
+
return `• [${time}] ${formatContextSummary(context)}`;
|
|
224
237
|
})
|
|
225
238
|
.join("\n");
|
|
226
239
|
}
|
|
@@ -231,7 +244,7 @@ function formatMatchedBullets(contexts, limit) {
|
|
|
231
244
|
}
|
|
232
245
|
return matched.map((context) => {
|
|
233
246
|
const labels = context.matchedItems.map((item) => `${item.name}(${item.symbol})`).join("、");
|
|
234
|
-
return `• ${labels}: ${
|
|
247
|
+
return `• ${labels}: ${formatContextSummary(context)}`;
|
|
235
248
|
}).join("\n");
|
|
236
249
|
}
|
|
237
250
|
function buildFocusBullets(contexts) {
|
|
@@ -239,11 +252,11 @@ function buildFocusBullets(contexts) {
|
|
|
239
252
|
const matchedContexts = contexts.filter((context) => context.matchedItems.length > 0);
|
|
240
253
|
for (const context of matchedContexts.slice(0, 3)) {
|
|
241
254
|
const labels = context.matchedItems.map((item) => item.name).join("、");
|
|
242
|
-
bullets.push(`• 关注 ${labels}
|
|
255
|
+
bullets.push(`• 关注 ${labels} 开盘后的量价反馈,重点核实“${formatFocusCue(context)}”是否继续发酵。`);
|
|
243
256
|
}
|
|
244
257
|
if (bullets.length < 3) {
|
|
245
258
|
for (const context of contexts.slice(0, 3 - bullets.length)) {
|
|
246
|
-
bullets.push(`• 关注“${
|
|
259
|
+
bullets.push(`• 关注“${formatFocusCue(context)}”对应板块是否出现竞价强化或高开分歧。`);
|
|
247
260
|
}
|
|
248
261
|
}
|
|
249
262
|
return bullets.slice(0, 5).join("\n");
|
|
@@ -264,6 +277,147 @@ function extractHeadlineFromContent(content) {
|
|
|
264
277
|
function extractHeadlineText(content) {
|
|
265
278
|
return content.split(/[\n。!!]/)[0]?.trim() ?? "";
|
|
266
279
|
}
|
|
280
|
+
function extractFlashInsight(content) {
|
|
281
|
+
const headline = extractHeadlineFromContent(content);
|
|
282
|
+
const keyPoints = extractFlashKeyPoints(content, headline);
|
|
283
|
+
if (keyPoints.length === 0) {
|
|
284
|
+
return {
|
|
285
|
+
headline,
|
|
286
|
+
summary: buildTitleOnlySummary(headline),
|
|
287
|
+
keyPoints: [],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
headline,
|
|
292
|
+
summary: keyPoints.slice(0, 2).join(";"),
|
|
293
|
+
keyPoints,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function extractFlashKeyPoints(content, headline) {
|
|
297
|
+
const body = stripHeadline(content, headline);
|
|
298
|
+
if (!body) {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const lineCandidates = body
|
|
302
|
+
.replace(/\r/g, "\n")
|
|
303
|
+
.replace(/\n{2,}/g, "\n")
|
|
304
|
+
.replace(/([。;!?!?])\s*(?=\d+\s*[、..))])/g, "$1\n")
|
|
305
|
+
.split(/\n+/)
|
|
306
|
+
.map((segment) => cleanFlashSegment(segment))
|
|
307
|
+
.filter(Boolean);
|
|
308
|
+
const candidates = lineCandidates.length > 1
|
|
309
|
+
? lineCandidates
|
|
310
|
+
: body
|
|
311
|
+
.split(/[。;!?!?]/)
|
|
312
|
+
.map((segment) => cleanFlashSegment(segment))
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
const normalizedHeadline = normalizeText(headline);
|
|
315
|
+
const deduped = new Set();
|
|
316
|
+
const keyPoints = [];
|
|
317
|
+
for (const segment of candidates) {
|
|
318
|
+
const normalizedSegment = normalizeText(segment);
|
|
319
|
+
if (!normalizedSegment || normalizedSegment === normalizedHeadline) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (normalizedHeadline && normalizedHeadline.includes(normalizedSegment) && segment.length < Math.max(12, headline.length)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (segment.length < 8) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (deduped.has(normalizedSegment)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
deduped.add(normalizedSegment);
|
|
332
|
+
keyPoints.push(truncateText(segment, 88));
|
|
333
|
+
if (keyPoints.length >= 3) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return keyPoints;
|
|
338
|
+
}
|
|
339
|
+
function stripHeadline(content, headline) {
|
|
340
|
+
const trimmed = content.trim();
|
|
341
|
+
if (!headline) {
|
|
342
|
+
return trimmed;
|
|
343
|
+
}
|
|
344
|
+
if (!trimmed.startsWith(headline)) {
|
|
345
|
+
return trimmed;
|
|
346
|
+
}
|
|
347
|
+
return trimmed.slice(headline.length).replace(/^[::。;,、\s-]+/, "").trim();
|
|
348
|
+
}
|
|
349
|
+
function cleanFlashSegment(segment) {
|
|
350
|
+
return segment
|
|
351
|
+
.trim()
|
|
352
|
+
.replace(/^[-•●▪◦]\s*/, "")
|
|
353
|
+
.replace(/^\d+\s*[、..))]\s*/, "")
|
|
354
|
+
.replace(/^[((]?\d+[))]\s*/, "")
|
|
355
|
+
.replace(/^[::;,。、\s]+/, "")
|
|
356
|
+
.replace(/[::;,。、\s]+$/, "")
|
|
357
|
+
.replace(/\s+/g, " ");
|
|
358
|
+
}
|
|
359
|
+
function buildTitleOnlySummary(headline) {
|
|
360
|
+
const coreHeadline = headline.replace(/^【?金十数据整理[::]\s*/, "").replace(/】$/, "").trim();
|
|
361
|
+
if (!coreHeadline) {
|
|
362
|
+
return "该整理快讯未提取到可用细节,暂只能作为标题级线索参考。";
|
|
363
|
+
}
|
|
364
|
+
return `${coreHeadline},但正文未提取到更具体细节,暂只能作为标题级线索参考。`;
|
|
365
|
+
}
|
|
366
|
+
function formatContextSummary(context) {
|
|
367
|
+
return context.summary || buildTitleOnlySummary(context.headline);
|
|
368
|
+
}
|
|
369
|
+
function formatFocusCue(context) {
|
|
370
|
+
return context.keyPoints[0] ?? context.summary ?? context.headline;
|
|
371
|
+
}
|
|
372
|
+
function truncateText(value, maxLength) {
|
|
373
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength)}...`;
|
|
374
|
+
}
|
|
375
|
+
function isLowSignalSummary(summary, contexts) {
|
|
376
|
+
const bulletLines = summary
|
|
377
|
+
.split(/\n+/)
|
|
378
|
+
.map((line) => line.trim())
|
|
379
|
+
.filter((line) => /^(?:•|-|\d+\.)/.test(line));
|
|
380
|
+
if (bulletLines.length === 0) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
const titleOnlyCount = bulletLines.filter((line) => isTitleOnlyBullet(line, contexts)).length;
|
|
384
|
+
return titleOnlyCount >= Math.max(2, Math.ceil(bulletLines.length / 3));
|
|
385
|
+
}
|
|
386
|
+
function isTitleOnlyBullet(line, contexts) {
|
|
387
|
+
const candidates = normalizeBulletForComparison(line);
|
|
388
|
+
if (candidates.length === 0) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return contexts.some((context) => {
|
|
392
|
+
const headlineForms = [
|
|
393
|
+
normalizeDigestText(context.headline),
|
|
394
|
+
normalizeDigestText(stripDigestPrefix(context.headline)),
|
|
395
|
+
].filter(Boolean);
|
|
396
|
+
return headlineForms.some((headline) => candidates.some((candidate) => candidate.includes(headline) && candidate.length <= headline.length + 6));
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function normalizeBulletForComparison(line) {
|
|
400
|
+
const cleaned = line
|
|
401
|
+
.replace(/^(?:•|-|\d+\.)\s*/, "")
|
|
402
|
+
.replace(/^\[[0-9:]+\]\s*/, "");
|
|
403
|
+
const normalizedVariants = [
|
|
404
|
+
cleaned,
|
|
405
|
+
cleaned.split(/[::]/).at(-1) ?? cleaned,
|
|
406
|
+
]
|
|
407
|
+
.map((item) => normalizeDigestText(item))
|
|
408
|
+
.filter(Boolean);
|
|
409
|
+
return [...new Set(normalizedVariants)];
|
|
410
|
+
}
|
|
411
|
+
function stripDigestPrefix(value) {
|
|
412
|
+
return value.replace(/^【?金十数据整理[::]\s*/, "").replace(/】$/, "").trim();
|
|
413
|
+
}
|
|
414
|
+
function normalizeDigestText(value) {
|
|
415
|
+
return value
|
|
416
|
+
.toLowerCase()
|
|
417
|
+
.replace(/[【】[\]()()"'“”‘’]/g, "")
|
|
418
|
+
.replace(/[::]/g, "")
|
|
419
|
+
.replace(/\s+/g, "");
|
|
420
|
+
}
|
|
267
421
|
function toFlashRecord(item) {
|
|
268
422
|
const published = new Date(item.time);
|
|
269
423
|
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;
|