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.
Files changed (65) hide show
  1. package/README.md +11 -42
  2. package/dist/analysis/types/composite-analysis.d.ts +27 -0
  3. package/dist/background/realtime-monitor.worker.d.ts +1 -1
  4. package/dist/background/realtime-monitor.worker.js +3 -4
  5. package/dist/bootstrap.js +24 -4
  6. package/dist/config/tickflow-access.d.ts +2 -1
  7. package/dist/config/tickflow-access.js +10 -3
  8. package/dist/dev/run-monitor-loop.js +0 -1
  9. package/dist/dev/tickflow-assist-cli.js +4 -3
  10. package/dist/dev/validate-mx-search.js +10 -2
  11. package/dist/plugin-commands.js +27 -0
  12. package/dist/plugin.js +4 -6
  13. package/dist/prompts/analysis/kline-analysis-user-prompt.js +2 -1
  14. package/dist/prompts/analysis/post-close-review-user-prompt.js +40 -1
  15. package/dist/prompts/analysis/pre-market-brief-prompt.d.ts +3 -1
  16. package/dist/prompts/analysis/pre-market-brief-prompt.js +8 -3
  17. package/dist/services/industry-peer-service.d.ts +9 -0
  18. package/dist/services/industry-peer-service.js +152 -0
  19. package/dist/services/jin10-flash-monitor-service.js +2 -1
  20. package/dist/services/monitor-service.d.ts +1 -1
  21. package/dist/services/monitor-service.js +21 -26
  22. package/dist/services/mx-search-service.d.ts +8 -1
  23. package/dist/services/mx-search-service.js +400 -10
  24. package/dist/services/post-close-review-service.d.ts +11 -4
  25. package/dist/services/post-close-review-service.js +113 -10
  26. package/dist/services/pre-market-brief-service.js +500 -42
  27. package/dist/services/tickflow-client.d.ts +4 -1
  28. package/dist/services/tickflow-client.js +32 -0
  29. package/dist/services/tickflow-universe-service.d.ts +26 -0
  30. package/dist/services/tickflow-universe-service.js +213 -0
  31. package/dist/services/watchlist-profile-service.d.ts +4 -1
  32. package/dist/services/watchlist-profile-service.js +58 -29
  33. package/dist/services/watchlist-service.d.ts +5 -1
  34. package/dist/services/watchlist-service.js +9 -4
  35. package/dist/storage/repositories/universe-membership-repo.d.ts +11 -0
  36. package/dist/storage/repositories/universe-membership-repo.js +38 -0
  37. package/dist/storage/repositories/universe-repo.d.ts +17 -0
  38. package/dist/storage/repositories/universe-repo.js +62 -0
  39. package/dist/storage/schemas.d.ts +2 -0
  40. package/dist/storage/schemas.js +13 -0
  41. package/dist/tools/add-stock.tool.d.ts +2 -1
  42. package/dist/tools/add-stock.tool.js +10 -1
  43. package/dist/tools/eastmoney-watchlist.tool.d.ts +31 -0
  44. package/dist/tools/eastmoney-watchlist.tool.js +294 -0
  45. package/dist/tools/mx-data.tool.d.ts +8 -0
  46. package/dist/tools/mx-data.tool.js +94 -0
  47. package/dist/tools/mx-select-stock.tool.js +6 -2
  48. package/dist/tools/query-database.tool.js +6 -0
  49. package/dist/tools/refresh-watchlist-profiles.tool.d.ts +2 -1
  50. package/dist/tools/refresh-watchlist-profiles.tool.js +11 -1
  51. package/dist/tools/screen-stock-candidates.tool.d.ts +34 -0
  52. package/dist/tools/screen-stock-candidates.tool.js +477 -0
  53. package/dist/tools/test-alert.tool.js +56 -19
  54. package/dist/types/mx-data.d.ts +23 -0
  55. package/dist/types/mx-data.js +1 -0
  56. package/dist/types/mx-select-stock.d.ts +1 -0
  57. package/dist/types/mx-self-select.d.ts +30 -0
  58. package/dist/types/mx-self-select.js +1 -0
  59. package/dist/types/tickflow.d.ts +12 -0
  60. package/dist/utils/tickflow-quote.d.ts +5 -0
  61. package/dist/utils/tickflow-quote.js +31 -0
  62. package/openclaw.plugin.json +83 -6
  63. package/package.json +6 -6
  64. package/skills/stock-analysis/SKILL.md +39 -20
  65. package/skills/usage-help/SKILL.md +33 -0
@@ -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
  }
@@ -34,7 +34,7 @@ export declare class MonitorService {
34
34
  bindManagedServiceRuntime(): Promise<void>;
35
35
  markStopped(): Promise<void>;
36
36
  getStatusReport(): Promise<string>;
37
- runMonitorOnce(): Promise<number>;
37
+ runMonitorOnce(runtimeHost?: "plugin_service" | "fallback_process"): Promise<number>;
38
38
  private maybeSendSessionNotification;
39
39
  private buildQuoteLines;
40
40
  private buildKeyLevelsLines;
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { basenameOrUndefined, buildAlertMessageHash, truncateDiagnosticText, } from "../utils/alert-diagnostic-log.js";
4
4
  import { formatChinaDateTime } from "../utils/china-time.js";
5
5
  import { calculateProfitPct, formatCostPrice } from "../utils/cost-price.js";
6
+ import { resolveTickFlowQuoteChangePct } from "../utils/tickflow-quote.js";
6
7
  const DEFAULT_STATE = {
7
8
  running: false,
8
9
  startedAt: null,
@@ -23,6 +24,7 @@ const DEFAULT_STATE = {
23
24
  const INTRADAY_PERIOD = "1m";
24
25
  const MONITOR_RUN_LOCK_MIN_STALE_MS = 90_000;
25
26
  const ALERT_CLAIM_MIN_STALE_MS = 90_000;
27
+ const SYSTEM_SESSION_ALERT_SYMBOL = "__system_session__";
26
28
  export class MonitorService {
27
29
  baseDir;
28
30
  requestInterval;
@@ -182,12 +184,13 @@ export class MonitorService {
182
184
  lines.push(...(await this.buildKeyLevelsLines(watchlist)));
183
185
  return lines.join("\n");
184
186
  }
185
- async runMonitorOnce() {
187
+ async runMonitorOnce(runtimeHost) {
186
188
  const runLease = await this.tryAcquireRunLease();
187
189
  if (!runLease) {
188
190
  return 0;
189
191
  }
190
192
  try {
193
+ await this.recordHeartbeat(runtimeHost);
191
194
  await this.alertMediaService.maybeCleanupExpired();
192
195
  const phase = await this.tradingCalendarService.getTradingPhase();
193
196
  let alertCount = await this.maybeSendSessionNotification(phase);
@@ -258,12 +261,14 @@ export class MonitorService {
258
261
  return 0;
259
262
  }
260
263
  const watchlistCount = (await this.watchlistService.list()).length;
261
- const ok = await this.alertService.send(this.alertService.formatSystemNotification(event.title, [
264
+ const message = this.alertService.formatSystemNotification(event.title, [
262
265
  `时间: ${now}`,
263
266
  `阶段: ${event.phaseText}`,
264
267
  `关注列表: ${watchlistCount}只`,
265
- ]));
266
- if (ok) {
268
+ ]);
269
+ const ok = await this.trySendAlert(SYSTEM_SESSION_ALERT_SYMBOL, event.id, message);
270
+ if (ok
271
+ || await this.alertLogRepository.isSentThisSession(SYSTEM_SESSION_ALERT_SYMBOL, event.id, getSessionKey())) {
267
272
  nextState.sessionNotificationsSent.push(event.id);
268
273
  }
269
274
  await this.writeState(nextState);
@@ -309,7 +314,8 @@ export class MonitorService {
309
314
  }
310
315
  async buildAlertLine() {
311
316
  const today = formatChinaDateTime().slice(0, 10);
312
- const alerts = await this.alertLogRepository.listByNaturalDate(today);
317
+ const alerts = (await this.alertLogRepository.listByNaturalDate(today))
318
+ .filter((entry) => entry.symbol !== SYSTEM_SESSION_ALERT_SYMBOL);
313
319
  if (alerts.length === 0) {
314
320
  return "今日告警: 无";
315
321
  }
@@ -737,13 +743,7 @@ function formatTradingPhase(phase) {
737
743
  }
738
744
  function formatQuoteLine(item, quote) {
739
745
  const lastPrice = Number(quote.last_price ?? 0);
740
- const prevClose = Number(quote.prev_close ?? 0);
741
- const tickflowChangePct = quote.ext?.change_pct;
742
- const changePct = tickflowChangePct != null
743
- ? Number(tickflowChangePct) * 100
744
- : prevClose > 0
745
- ? ((lastPrice - prevClose) / prevClose) * 100
746
- : null;
746
+ const changePct = resolveTickFlowQuoteChangePct(quote);
747
747
  const quoteTime = formatQuoteTimestamp(quote.timestamp);
748
748
  const profitPct = calculateProfitPct(lastPrice, item.costPrice);
749
749
  let line = `• ${item.name}(${item.symbol}) ${lastPrice.toFixed(2)}`;
@@ -778,7 +778,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
778
778
  if (!hasSent("morning_start")
779
779
  && currentPhase === "trading"
780
780
  && hhmm <= "11:30"
781
- && ((previousPhase === "pre_market") || isWithinWindow(hhmm, "09:30", "09:40"))) {
781
+ && ((previousPhase === "pre_market")
782
+ || (previousPhase !== "trading" && isWithinWindow(hhmm, "09:30", "09:40")))) {
782
783
  return {
783
784
  id: "morning_start",
784
785
  title: "🔔 开始上午盯盘",
@@ -787,7 +788,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
787
788
  }
788
789
  if (!hasSent("morning_end")
789
790
  && currentPhase === "lunch_break"
790
- && ((previousPhase === "trading") || isWithinWindow(hhmm, "11:30", "11:40"))) {
791
+ && ((previousPhase === "trading")
792
+ || (previousPhase !== "lunch_break" && isWithinWindow(hhmm, "11:30", "11:40")))) {
791
793
  return {
792
794
  id: "morning_end",
793
795
  title: "🔔 上午盯盘结束",
@@ -797,7 +799,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
797
799
  if (!hasSent("afternoon_start")
798
800
  && currentPhase === "trading"
799
801
  && hhmm >= "13:00"
800
- && ((previousPhase === "lunch_break") || isWithinWindow(hhmm, "13:00", "13:10"))) {
802
+ && ((previousPhase === "lunch_break")
803
+ || (previousPhase !== "trading" && isWithinWindow(hhmm, "13:00", "13:10")))) {
801
804
  return {
802
805
  id: "afternoon_start",
803
806
  title: "🔔 开始下午盯盘",
@@ -806,7 +809,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
806
809
  }
807
810
  if (!hasSent("day_end")
808
811
  && currentPhase === "closed"
809
- && ((previousPhase === "trading") || isWithinWindow(hhmm, "15:00", "15:10"))) {
812
+ && ((previousPhase === "trading")
813
+ || (previousPhase !== "closed" && isWithinWindow(hhmm, "15:00", "15:10")))) {
810
814
  return {
811
815
  id: "day_end",
812
816
  title: "🔔 今日盯盘结束",
@@ -1007,14 +1011,5 @@ function resolveAlertImageLabel(ruleName, fallbackTitle) {
1007
1011
  }
1008
1012
  }
1009
1013
  function getQuoteChangePct(quote) {
1010
- const tickflowChangePct = quote.ext?.change_pct;
1011
- if (tickflowChangePct != null && Number.isFinite(Number(tickflowChangePct))) {
1012
- return Number(tickflowChangePct) * 100;
1013
- }
1014
- const lastPrice = Number(quote.last_price ?? 0);
1015
- const prevClose = Number(quote.prev_close ?? 0);
1016
- if (!(prevClose > 0)) {
1017
- return null;
1018
- }
1019
- return ((lastPrice - prevClose) / prevClose) * 100;
1014
+ return resolveTickFlowQuoteChangePct(quote);
1020
1015
  }
@@ -1,4 +1,6 @@
1
1
  import type { MxSearchDocument } from "../types/mx-search.js";
2
+ import type { MxDataResult } from "../types/mx-data.js";
3
+ import type { MxSelfSelectManageResult, MxSelfSelectResult } from "../types/mx-self-select.js";
2
4
  import type { MxSelectStockResult } from "../types/mx-select-stock.js";
3
5
  export declare class MxSearchServiceError extends Error {
4
6
  constructor(message: string);
@@ -8,15 +10,20 @@ export declare class MxApiService {
8
10
  private readonly apiKey;
9
11
  constructor(apiBaseUrl: string, apiKey: string);
10
12
  isConfigured(): boolean;
11
- getConfigurationError(): string | null;
13
+ getConfigurationError(featureName?: string): string | null;
12
14
  search(query: string): Promise<MxSearchDocument[]>;
13
15
  selectStocks(input: {
14
16
  keyword: string;
15
17
  pageNo?: number;
16
18
  pageSize?: number;
17
19
  }): Promise<MxSelectStockResult>;
20
+ queryData(toolQuery: string): Promise<MxDataResult>;
21
+ getSelfSelectWatchlist(): Promise<MxSelfSelectResult>;
22
+ manageSelfSelect(query: string): Promise<MxSelfSelectManageResult>;
18
23
  private postJson;
19
24
  }
20
25
  export declare class MxSearchService extends MxApiService {
21
26
  }
22
27
  export declare function normalizeMxSearchDocuments(value: unknown): MxSearchDocument[];
28
+ export declare function normalizeMxSelectStockResult(value: unknown): MxSelectStockResult;
29
+ export declare function normalizeMxDataResult(value: unknown): MxDataResult;