tickflow-assist 0.2.0

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 (246) hide show
  1. package/README.md +151 -0
  2. package/day_future.txt +8797 -0
  3. package/dist/analysis/orchestrators/composite-analysis.orchestrator.d.ts +35 -0
  4. package/dist/analysis/orchestrators/composite-analysis.orchestrator.js +232 -0
  5. package/dist/analysis/parsers/json-block.parser.d.ts +1 -0
  6. package/dist/analysis/parsers/json-block.parser.js +13 -0
  7. package/dist/analysis/parsers/key-levels.parser.d.ts +5 -0
  8. package/dist/analysis/parsers/key-levels.parser.js +61 -0
  9. package/dist/analysis/parsers/post-close-review.parser.d.ts +2 -0
  10. package/dist/analysis/parsers/post-close-review.parser.js +79 -0
  11. package/dist/analysis/parsers/watchlist-profile.parser.d.ts +6 -0
  12. package/dist/analysis/parsers/watchlist-profile.parser.js +93 -0
  13. package/dist/analysis/providers/financial-analysis.provider.d.ts +12 -0
  14. package/dist/analysis/providers/financial-analysis.provider.js +85 -0
  15. package/dist/analysis/providers/market-analysis.provider.d.ts +27 -0
  16. package/dist/analysis/providers/market-analysis.provider.js +187 -0
  17. package/dist/analysis/providers/news-analysis.provider.d.ts +9 -0
  18. package/dist/analysis/providers/news-analysis.provider.js +60 -0
  19. package/dist/analysis/tasks/analysis-step-task.d.ts +11 -0
  20. package/dist/analysis/tasks/analysis-step-task.js +1 -0
  21. package/dist/analysis/tasks/analysis-task.d.ts +13 -0
  22. package/dist/analysis/tasks/analysis-task.js +1 -0
  23. package/dist/analysis/tasks/composite-stock-analysis.task.d.ts +17 -0
  24. package/dist/analysis/tasks/composite-stock-analysis.task.js +47 -0
  25. package/dist/analysis/tasks/financial-fundamental-lite.task.d.ts +10 -0
  26. package/dist/analysis/tasks/financial-fundamental-lite.task.js +21 -0
  27. package/dist/analysis/tasks/financial-fundamental.task.d.ts +12 -0
  28. package/dist/analysis/tasks/financial-fundamental.task.js +64 -0
  29. package/dist/analysis/tasks/kline-technical-signal.task.d.ts +10 -0
  30. package/dist/analysis/tasks/kline-technical-signal.task.js +41 -0
  31. package/dist/analysis/tasks/kline-technical.task.d.ts +32 -0
  32. package/dist/analysis/tasks/kline-technical.task.js +61 -0
  33. package/dist/analysis/tasks/news-catalyst.task.d.ts +11 -0
  34. package/dist/analysis/tasks/news-catalyst.task.js +62 -0
  35. package/dist/analysis/tasks/post-close-review.task.d.ts +12 -0
  36. package/dist/analysis/tasks/post-close-review.task.js +35 -0
  37. package/dist/analysis/types/composite-analysis.d.ts +123 -0
  38. package/dist/analysis/types/composite-analysis.js +1 -0
  39. package/dist/background/daily-update.worker.d.ts +50 -0
  40. package/dist/background/daily-update.worker.js +546 -0
  41. package/dist/background/realtime-monitor.worker.d.ts +8 -0
  42. package/dist/background/realtime-monitor.worker.js +28 -0
  43. package/dist/bootstrap.d.ts +45 -0
  44. package/dist/bootstrap.js +214 -0
  45. package/dist/config/normalize.d.ts +4 -0
  46. package/dist/config/normalize.js +99 -0
  47. package/dist/config/schema.d.ts +23 -0
  48. package/dist/config/schema.js +18 -0
  49. package/dist/config/tickflow-access.d.ts +4 -0
  50. package/dist/config/tickflow-access.js +28 -0
  51. package/dist/constants/market-indexes.d.ts +5 -0
  52. package/dist/constants/market-indexes.js +4 -0
  53. package/dist/dev/run-daily-update-loop.d.ts +1 -0
  54. package/dist/dev/run-daily-update-loop.js +48 -0
  55. package/dist/dev/run-monitor-loop.d.ts +1 -0
  56. package/dist/dev/run-monitor-loop.js +60 -0
  57. package/dist/dev/run-tool.d.ts +1 -0
  58. package/dist/dev/run-tool.js +49 -0
  59. package/dist/dev/tickflow-assist-cli.d.ts +2 -0
  60. package/dist/dev/tickflow-assist-cli.js +525 -0
  61. package/dist/dev/validate-mx-search.d.ts +1 -0
  62. package/dist/dev/validate-mx-search.js +212 -0
  63. package/dist/plugin-commands.d.ts +3 -0
  64. package/dist/plugin-commands.js +229 -0
  65. package/dist/plugin.d.ts +8 -0
  66. package/dist/plugin.js +151 -0
  67. package/dist/prompts/analysis/common-system-prompt.d.ts +1 -0
  68. package/dist/prompts/analysis/common-system-prompt.js +44 -0
  69. package/dist/prompts/analysis/composite-analysis-user-prompt.d.ts +11 -0
  70. package/dist/prompts/analysis/composite-analysis-user-prompt.js +102 -0
  71. package/dist/prompts/analysis/financial-analysis-user-prompt.d.ts +7 -0
  72. package/dist/prompts/analysis/financial-analysis-user-prompt.js +106 -0
  73. package/dist/prompts/analysis/financial-lite-analysis-user-prompt.d.ts +7 -0
  74. package/dist/prompts/analysis/financial-lite-analysis-user-prompt.js +59 -0
  75. package/dist/prompts/analysis/index.d.ts +8 -0
  76. package/dist/prompts/analysis/index.js +8 -0
  77. package/dist/prompts/analysis/kline-analysis-user-prompt.d.ts +13 -0
  78. package/dist/prompts/analysis/kline-analysis-user-prompt.js +215 -0
  79. package/dist/prompts/analysis/news-analysis-user-prompt.d.ts +8 -0
  80. package/dist/prompts/analysis/news-analysis-user-prompt.js +57 -0
  81. package/dist/prompts/analysis/post-close-review-user-prompt.d.ts +3 -0
  82. package/dist/prompts/analysis/post-close-review-user-prompt.js +129 -0
  83. package/dist/prompts/analysis/watchlist-profile-extraction-prompt.d.ts +7 -0
  84. package/dist/prompts/analysis/watchlist-profile-extraction-prompt.js +52 -0
  85. package/dist/prompts/analysis-system-prompt.d.ts +1 -0
  86. package/dist/prompts/analysis-system-prompt.js +1 -0
  87. package/dist/prompts/analysis-user-prompt.d.ts +1 -0
  88. package/dist/prompts/analysis-user-prompt.js +1 -0
  89. package/dist/runtime/daily-update-process.d.ts +3 -0
  90. package/dist/runtime/daily-update-process.js +24 -0
  91. package/dist/runtime/monitor-process.d.ts +3 -0
  92. package/dist/runtime/monitor-process.js +24 -0
  93. package/dist/runtime/plugin-api.d.ts +22 -0
  94. package/dist/runtime/plugin-api.js +2 -0
  95. package/dist/runtime/process-config.d.ts +8 -0
  96. package/dist/runtime/process-config.js +35 -0
  97. package/dist/services/alert-service.d.ts +45 -0
  98. package/dist/services/alert-service.js +198 -0
  99. package/dist/services/analysis-service.d.ts +20 -0
  100. package/dist/services/analysis-service.js +73 -0
  101. package/dist/services/analysis-view-service.d.ts +23 -0
  102. package/dist/services/analysis-view-service.js +343 -0
  103. package/dist/services/financial-lite-service.d.ts +23 -0
  104. package/dist/services/financial-lite-service.js +201 -0
  105. package/dist/services/financial-service.d.ts +20 -0
  106. package/dist/services/financial-service.js +47 -0
  107. package/dist/services/indicator-service.d.ts +9 -0
  108. package/dist/services/indicator-service.js +67 -0
  109. package/dist/services/instrument-service.d.ts +6 -0
  110. package/dist/services/instrument-service.js +21 -0
  111. package/dist/services/key-level-service.d.ts +2 -0
  112. package/dist/services/key-level-service.js +2 -0
  113. package/dist/services/key-levels-backtest-service.d.ts +71 -0
  114. package/dist/services/key-levels-backtest-service.js +427 -0
  115. package/dist/services/kline-service.d.ts +19 -0
  116. package/dist/services/kline-service.js +91 -0
  117. package/dist/services/monitor-service.d.ts +44 -0
  118. package/dist/services/monitor-service.js +598 -0
  119. package/dist/services/mx-search-service.d.ts +22 -0
  120. package/dist/services/mx-search-service.js +286 -0
  121. package/dist/services/post-close-review-service.d.ts +31 -0
  122. package/dist/services/post-close-review-service.js +402 -0
  123. package/dist/services/quote-service.d.ts +7 -0
  124. package/dist/services/quote-service.js +9 -0
  125. package/dist/services/review-memory-service.d.ts +7 -0
  126. package/dist/services/review-memory-service.js +76 -0
  127. package/dist/services/tickflow-client.d.ts +43 -0
  128. package/dist/services/tickflow-client.js +126 -0
  129. package/dist/services/trading-calendar-service.d.ts +20 -0
  130. package/dist/services/trading-calendar-service.js +102 -0
  131. package/dist/services/update-service.d.ts +21 -0
  132. package/dist/services/update-service.js +130 -0
  133. package/dist/services/watchlist-profile-service.d.ts +20 -0
  134. package/dist/services/watchlist-profile-service.js +76 -0
  135. package/dist/services/watchlist-service.d.ts +43 -0
  136. package/dist/services/watchlist-service.js +204 -0
  137. package/dist/storage/db.d.ts +23 -0
  138. package/dist/storage/db.js +70 -0
  139. package/dist/storage/repositories/alert-log-repo.d.ts +15 -0
  140. package/dist/storage/repositories/alert-log-repo.js +54 -0
  141. package/dist/storage/repositories/analysis-log-repo.d.ts +8 -0
  142. package/dist/storage/repositories/analysis-log-repo.js +48 -0
  143. package/dist/storage/repositories/composite-analysis-repo.d.ts +9 -0
  144. package/dist/storage/repositories/composite-analysis-repo.js +116 -0
  145. package/dist/storage/repositories/financial-analysis-repo.d.ts +9 -0
  146. package/dist/storage/repositories/financial-analysis-repo.js +107 -0
  147. package/dist/storage/repositories/indicators-repo.d.ts +8 -0
  148. package/dist/storage/repositories/indicators-repo.js +98 -0
  149. package/dist/storage/repositories/intraday-klines-repo.d.ts +9 -0
  150. package/dist/storage/repositories/intraday-klines-repo.js +102 -0
  151. package/dist/storage/repositories/key-levels-history-repo.d.ts +9 -0
  152. package/dist/storage/repositories/key-levels-history-repo.js +91 -0
  153. package/dist/storage/repositories/key-levels-repo.d.ts +9 -0
  154. package/dist/storage/repositories/key-levels-repo.js +83 -0
  155. package/dist/storage/repositories/klines-repo.d.ts +8 -0
  156. package/dist/storage/repositories/klines-repo.js +60 -0
  157. package/dist/storage/repositories/news-analysis-repo.d.ts +9 -0
  158. package/dist/storage/repositories/news-analysis-repo.js +107 -0
  159. package/dist/storage/repositories/technical-analysis-repo.d.ts +9 -0
  160. package/dist/storage/repositories/technical-analysis-repo.js +80 -0
  161. package/dist/storage/repositories/watchlist-repo.d.ts +10 -0
  162. package/dist/storage/repositories/watchlist-repo.js +124 -0
  163. package/dist/storage/schemas.d.ts +13 -0
  164. package/dist/storage/schemas.js +177 -0
  165. package/dist/tools/add-stock.tool.d.ts +13 -0
  166. package/dist/tools/add-stock.tool.js +123 -0
  167. package/dist/tools/analyze.tool.d.ts +8 -0
  168. package/dist/tools/analyze.tool.js +24 -0
  169. package/dist/tools/backtest-key-levels.tool.d.ts +8 -0
  170. package/dist/tools/backtest-key-levels.tool.js +43 -0
  171. package/dist/tools/daily-update-status.tool.d.ts +6 -0
  172. package/dist/tools/daily-update-status.tool.js +9 -0
  173. package/dist/tools/fetch-financials.tool.d.ts +8 -0
  174. package/dist/tools/fetch-financials.tool.js +224 -0
  175. package/dist/tools/fetch-intraday-klines.tool.d.ts +11 -0
  176. package/dist/tools/fetch-intraday-klines.tool.js +58 -0
  177. package/dist/tools/fetch-klines.tool.d.ts +11 -0
  178. package/dist/tools/fetch-klines.tool.js +61 -0
  179. package/dist/tools/list-watchlist.tool.d.ts +6 -0
  180. package/dist/tools/list-watchlist.tool.js +22 -0
  181. package/dist/tools/monitor-status.tool.d.ts +6 -0
  182. package/dist/tools/monitor-status.tool.js +9 -0
  183. package/dist/tools/mx-search.tool.d.ts +8 -0
  184. package/dist/tools/mx-search.tool.js +77 -0
  185. package/dist/tools/mx-select-stock.tool.d.ts +8 -0
  186. package/dist/tools/mx-select-stock.tool.js +118 -0
  187. package/dist/tools/query-database.tool.d.ts +8 -0
  188. package/dist/tools/query-database.tool.js +283 -0
  189. package/dist/tools/refresh-watchlist-names.tool.d.ts +7 -0
  190. package/dist/tools/refresh-watchlist-names.tool.js +17 -0
  191. package/dist/tools/refresh-watchlist-profiles.tool.d.ts +9 -0
  192. package/dist/tools/refresh-watchlist-profiles.tool.js +67 -0
  193. package/dist/tools/remove-stock.tool.d.ts +9 -0
  194. package/dist/tools/remove-stock.tool.js +27 -0
  195. package/dist/tools/start-daily-update.tool.d.ts +10 -0
  196. package/dist/tools/start-daily-update.tool.js +23 -0
  197. package/dist/tools/start-monitor.tool.d.ts +9 -0
  198. package/dist/tools/start-monitor.tool.js +52 -0
  199. package/dist/tools/stop-daily-update.tool.d.ts +9 -0
  200. package/dist/tools/stop-daily-update.tool.js +20 -0
  201. package/dist/tools/stop-monitor.tool.d.ts +9 -0
  202. package/dist/tools/stop-monitor.tool.js +44 -0
  203. package/dist/tools/test-alert.tool.d.ts +7 -0
  204. package/dist/tools/test-alert.tool.js +21 -0
  205. package/dist/tools/update-all.tool.d.ts +9 -0
  206. package/dist/tools/update-all.tool.js +17 -0
  207. package/dist/tools/view-analysis.tool.d.ts +8 -0
  208. package/dist/tools/view-analysis.tool.js +95 -0
  209. package/dist/types/daily-update.d.ts +26 -0
  210. package/dist/types/daily-update.js +1 -0
  211. package/dist/types/domain.d.ts +140 -0
  212. package/dist/types/domain.js +1 -0
  213. package/dist/types/indicator.d.ts +43 -0
  214. package/dist/types/indicator.js +1 -0
  215. package/dist/types/monitor.d.ts +17 -0
  216. package/dist/types/monitor.js +1 -0
  217. package/dist/types/mx-search.d.ts +14 -0
  218. package/dist/types/mx-search.js +1 -0
  219. package/dist/types/mx-select-stock.d.ts +28 -0
  220. package/dist/types/mx-select-stock.js +1 -0
  221. package/dist/types/tickflow.d.ts +133 -0
  222. package/dist/types/tickflow.js +1 -0
  223. package/dist/utils/abortable-sleep.d.ts +1 -0
  224. package/dist/utils/abortable-sleep.js +20 -0
  225. package/dist/utils/china-time.d.ts +3 -0
  226. package/dist/utils/china-time.js +18 -0
  227. package/dist/utils/cost-price.d.ts +3 -0
  228. package/dist/utils/cost-price.js +18 -0
  229. package/dist/utils/format.d.ts +1 -0
  230. package/dist/utils/format.js +3 -0
  231. package/dist/utils/process.d.ts +5 -0
  232. package/dist/utils/process.js +1 -0
  233. package/dist/utils/symbol.d.ts +1 -0
  234. package/dist/utils/symbol.js +13 -0
  235. package/docs/installation.md +391 -0
  236. package/docs/usage.md +244 -0
  237. package/openclaw.plugin.json +116 -0
  238. package/package.json +57 -0
  239. package/python/indicator_runner.py +31 -0
  240. package/python/indicators.py +148 -0
  241. package/python/pyproject.toml +9 -0
  242. package/python/requirements.txt +3 -0
  243. package/python/uv.lock +366 -0
  244. package/skills/database-query/SKILL.md +58 -0
  245. package/skills/stock-analysis/SKILL.md +106 -0
  246. package/skills/usage-help/SKILL.md +102 -0
@@ -0,0 +1,20 @@
1
+ export type TradingPhase = "non_trading_day" | "pre_market" | "trading" | "lunch_break" | "closed";
2
+ export declare class TradingCalendarService {
3
+ private readonly calendarFile;
4
+ private cache;
5
+ constructor(calendarFile: string);
6
+ isTradingDay(date?: Date): Promise<boolean>;
7
+ getTradingPhase(date?: Date): Promise<TradingPhase>;
8
+ canRunDailyUpdate(date?: Date): Promise<{
9
+ ok: boolean;
10
+ reason: string;
11
+ }>;
12
+ canRunPostCloseReview(date?: Date): Promise<{
13
+ ok: boolean;
14
+ reason: string;
15
+ }>;
16
+ getRecentTradingDays(limit: number, endDate?: Date): Promise<string[]>;
17
+ private loadDays;
18
+ private toChinaDate;
19
+ private toChinaParts;
20
+ }
@@ -0,0 +1,102 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export class TradingCalendarService {
3
+ calendarFile;
4
+ cache = null;
5
+ constructor(calendarFile) {
6
+ this.calendarFile = calendarFile;
7
+ }
8
+ async isTradingDay(date = new Date()) {
9
+ const { daySet } = await this.loadDays();
10
+ return daySet.has(this.toChinaDate(date));
11
+ }
12
+ async getTradingPhase(date = new Date()) {
13
+ if (!(await this.isTradingDay(date))) {
14
+ return "non_trading_day";
15
+ }
16
+ const china = this.toChinaParts(date);
17
+ const hhmm = `${china.hour}:${china.minute}`;
18
+ if (hhmm < "09:30") {
19
+ return "pre_market";
20
+ }
21
+ if (hhmm <= "11:30") {
22
+ return "trading";
23
+ }
24
+ if (hhmm < "13:00") {
25
+ return "lunch_break";
26
+ }
27
+ if (hhmm <= "15:00") {
28
+ return "trading";
29
+ }
30
+ return "closed";
31
+ }
32
+ async canRunDailyUpdate(date = new Date()) {
33
+ if (!(await this.isTradingDay(date))) {
34
+ return { ok: false, reason: `${this.toChinaDate(date)} 非交易日` };
35
+ }
36
+ const china = this.toChinaParts(date);
37
+ const hhmm = `${china.hour}:${china.minute}`;
38
+ if (hhmm < "15:25") {
39
+ return { ok: false, reason: `当前 ${hhmm},须等到 15:25 后执行` };
40
+ }
41
+ return { ok: true, reason: "交易日已收盘" };
42
+ }
43
+ async canRunPostCloseReview(date = new Date()) {
44
+ if (!(await this.isTradingDay(date))) {
45
+ return { ok: false, reason: `${this.toChinaDate(date)} 非交易日` };
46
+ }
47
+ const china = this.toChinaParts(date);
48
+ const hhmm = `${china.hour}:${china.minute}`;
49
+ if (hhmm < "20:00") {
50
+ return { ok: false, reason: `当前 ${hhmm},须等到 20:00 后执行复盘` };
51
+ }
52
+ return { ok: true, reason: "交易日已完成收盘且达到复盘时间" };
53
+ }
54
+ async getRecentTradingDays(limit, endDate = new Date()) {
55
+ if (!Number.isInteger(limit) || limit <= 0) {
56
+ throw new Error(`limit must be a positive integer, got ${limit}`);
57
+ }
58
+ const { days } = await this.loadDays();
59
+ const end = this.toChinaDate(endDate);
60
+ const eligible = days.filter((day) => day <= end);
61
+ const source = eligible.length > 0 ? eligible : days;
62
+ return source.slice(-limit);
63
+ }
64
+ async loadDays() {
65
+ if (this.cache) {
66
+ return this.cache;
67
+ }
68
+ const raw = await readFile(this.calendarFile, "utf-8");
69
+ const days = raw
70
+ .split(/\r?\n/)
71
+ .map((line) => line.trim())
72
+ .filter(Boolean);
73
+ this.cache = { days, daySet: new Set(days) };
74
+ return this.cache;
75
+ }
76
+ toChinaDate(date) {
77
+ const parts = this.toChinaParts(date);
78
+ return `${parts.year}-${parts.month}-${parts.day}`;
79
+ }
80
+ toChinaParts(date) {
81
+ const formatter = new Intl.DateTimeFormat("zh-CN", {
82
+ timeZone: "Asia/Shanghai",
83
+ year: "numeric",
84
+ month: "2-digit",
85
+ day: "2-digit",
86
+ hour: "2-digit",
87
+ minute: "2-digit",
88
+ hour12: false,
89
+ });
90
+ const map = Object.fromEntries(formatter
91
+ .formatToParts(date)
92
+ .filter((part) => part.type !== "literal")
93
+ .map((part) => [part.type, part.value]));
94
+ return {
95
+ year: map.year,
96
+ month: map.month,
97
+ day: map.day,
98
+ hour: map.hour,
99
+ minute: map.minute,
100
+ };
101
+ }
102
+ }
@@ -0,0 +1,21 @@
1
+ import { type TickflowApiKeyLevel } from "../config/tickflow-access.js";
2
+ import { KlineService } from "./kline-service.js";
3
+ import { IndicatorService } from "./indicator-service.js";
4
+ import { KlinesRepository } from "../storage/repositories/klines-repo.js";
5
+ import { IndicatorsRepository } from "../storage/repositories/indicators-repo.js";
6
+ import { IntradayKlinesRepository } from "../storage/repositories/intraday-klines-repo.js";
7
+ import { WatchlistService } from "./watchlist-service.js";
8
+ import { TradingCalendarService } from "./trading-calendar-service.js";
9
+ export declare class UpdateService {
10
+ private readonly klineService;
11
+ private readonly tickflowApiKeyLevel;
12
+ private readonly indicatorService;
13
+ private readonly klinesRepository;
14
+ private readonly indicatorsRepository;
15
+ private readonly intradayKlinesRepository;
16
+ private readonly watchlistService;
17
+ private readonly tradingCalendarService;
18
+ constructor(klineService: KlineService, tickflowApiKeyLevel: TickflowApiKeyLevel, indicatorService: IndicatorService, klinesRepository: KlinesRepository, indicatorsRepository: IndicatorsRepository, intradayKlinesRepository: IntradayKlinesRepository, watchlistService: WatchlistService, tradingCalendarService: TradingCalendarService);
19
+ updateAll(force?: boolean, days?: number, adjust?: string): Promise<string>;
20
+ private updateTarget;
21
+ }
@@ -0,0 +1,130 @@
1
+ import { DEFAULT_MARKET_INDEXES } from "../constants/market-indexes.js";
2
+ import { formatTickflowApiKeyLevel, supportsIntradayKlines } from "../config/tickflow-access.js";
3
+ const INTRADAY_PERIOD = "1m";
4
+ const INTRADAY_RETENTION_DAYS = 30;
5
+ export class UpdateService {
6
+ klineService;
7
+ tickflowApiKeyLevel;
8
+ indicatorService;
9
+ klinesRepository;
10
+ indicatorsRepository;
11
+ intradayKlinesRepository;
12
+ watchlistService;
13
+ tradingCalendarService;
14
+ constructor(klineService, tickflowApiKeyLevel, indicatorService, klinesRepository, indicatorsRepository, intradayKlinesRepository, watchlistService, tradingCalendarService) {
15
+ this.klineService = klineService;
16
+ this.tickflowApiKeyLevel = tickflowApiKeyLevel;
17
+ this.indicatorService = indicatorService;
18
+ this.klinesRepository = klinesRepository;
19
+ this.indicatorsRepository = indicatorsRepository;
20
+ this.intradayKlinesRepository = intradayKlinesRepository;
21
+ this.watchlistService = watchlistService;
22
+ this.tradingCalendarService = tradingCalendarService;
23
+ }
24
+ async updateAll(force = false, days = 90, adjust = "forward") {
25
+ if (!force) {
26
+ const check = await this.tradingCalendarService.canRunDailyUpdate();
27
+ if (!check.ok) {
28
+ return `🚫 ${check.reason}`;
29
+ }
30
+ }
31
+ const watchlist = await this.watchlistService.list();
32
+ const lines = [
33
+ `📊 收盘更新: ${watchlist.length} 只股票 + ${DEFAULT_MARKET_INDEXES.length} 个指数, 获取 ${days} 天日K与当日分钟K (个股复权: ${adjust})`,
34
+ `🔑 TickFlow API Key Level: ${formatTickflowApiKeyLevel(this.tickflowApiKeyLevel)}`,
35
+ "",
36
+ "📈 指数更新:",
37
+ ];
38
+ let indexSuccess = 0;
39
+ let indexFailed = 0;
40
+ for (const target of DEFAULT_MARKET_INDEXES) {
41
+ const result = await this.updateTarget({
42
+ symbol: target.symbol,
43
+ name: target.name,
44
+ kind: "index",
45
+ }, days, "none");
46
+ lines.push(result.line);
47
+ if (result.ok) {
48
+ indexSuccess += 1;
49
+ }
50
+ else {
51
+ indexFailed += 1;
52
+ }
53
+ }
54
+ let success = 0;
55
+ let failed = 0;
56
+ if (watchlist.length === 0) {
57
+ lines.push("", "📋 关注列表为空,已跳过个股更新");
58
+ }
59
+ else {
60
+ lines.push("", "📋 个股更新:");
61
+ for (const item of watchlist) {
62
+ const result = await this.updateTarget({
63
+ symbol: item.symbol,
64
+ name: item.name,
65
+ kind: "stock",
66
+ }, days, adjust);
67
+ lines.push(result.line);
68
+ if (result.ok) {
69
+ success += 1;
70
+ }
71
+ else {
72
+ failed += 1;
73
+ }
74
+ }
75
+ }
76
+ lines.push(`🏁 完成: 指数 ${indexSuccess} 成功, ${indexFailed} 失败 | 个股 ${success} 成功, ${failed} 失败 (共 ${watchlist.length} 只)`);
77
+ return lines.join("\n");
78
+ }
79
+ async updateTarget(target, days, adjust) {
80
+ try {
81
+ const rows = await this.klineService.fetchKlines(target.symbol, {
82
+ count: days,
83
+ adjust,
84
+ });
85
+ if (rows.length === 0) {
86
+ return {
87
+ ok: false,
88
+ line: `❌ ${target.name}(${target.symbol}): 返回数据为空`,
89
+ };
90
+ }
91
+ await this.klinesRepository.saveAll(target.symbol, rows);
92
+ const indicators = await this.indicatorService.calculate(rows);
93
+ await this.indicatorsRepository.saveAll(target.symbol, indicators);
94
+ let intradaySummary = `分钟K 已跳过(API Key Level=${formatTickflowApiKeyLevel(this.tickflowApiKeyLevel)})`;
95
+ if (supportsIntradayKlines(this.tickflowApiKeyLevel)) {
96
+ try {
97
+ const intradayRows = await this.klineService.fetchIntradayKlines(target.symbol, {
98
+ period: INTRADAY_PERIOD,
99
+ });
100
+ if (intradayRows.length > 0) {
101
+ await this.intradayKlinesRepository.saveAll(target.symbol, INTRADAY_PERIOD, intradayRows);
102
+ const keepTradeDates = await this.tradingCalendarService.getRecentTradingDays(INTRADAY_RETENTION_DAYS, new Date(intradayRows[intradayRows.length - 1].timestamp));
103
+ await this.intradayKlinesRepository.pruneToTradeDates(target.symbol, INTRADAY_PERIOD, keepTradeDates);
104
+ intradaySummary = `分钟K ${intradayRows.length} 根`;
105
+ }
106
+ else {
107
+ intradaySummary = "分钟K 0 根";
108
+ }
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ intradaySummary = `分钟K 更新失败,已跳过(${message})`;
113
+ }
114
+ }
115
+ const latest = rows[rows.length - 1];
116
+ const scope = target.kind === "index" ? "指数" : "个股";
117
+ return {
118
+ ok: true,
119
+ line: `✅ ${target.name}(${target.symbol}): ${scope}日K ${rows.length} 根, ${intradaySummary}, 最新 ${latest.trade_date} 收盘 ${latest.close.toFixed(2)}`,
120
+ };
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ return {
125
+ ok: false,
126
+ line: `❌ ${target.name}(${target.symbol}): ${message}`,
127
+ };
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,20 @@
1
+ import { AnalysisService } from "./analysis-service.js";
2
+ import type { MxSearchDocument } from "../types/mx-search.js";
3
+ import { MxApiService } from "./mx-search-service.js";
4
+ export interface WatchlistProfile {
5
+ sector: string | null;
6
+ themes: string[];
7
+ themeQuery: string | null;
8
+ themeUpdatedAt: string | null;
9
+ }
10
+ export declare class WatchlistProfileService {
11
+ private readonly mxApiService;
12
+ private readonly analysisService;
13
+ constructor(mxApiService: MxApiService, analysisService: AnalysisService);
14
+ resolve(symbol: string, companyName: string, updatedAt: string): Promise<WatchlistProfile>;
15
+ }
16
+ export declare function buildBoardNewsQuery(profile: {
17
+ sector: string | null;
18
+ themes: string[];
19
+ }): string | null;
20
+ export declare function formatWatchlistProfileDocuments(documents: MxSearchDocument[]): string;
@@ -0,0 +1,76 @@
1
+ import { parseWatchlistProfileExtraction } from "../analysis/parsers/watchlist-profile.parser.js";
2
+ import { buildWatchlistProfileExtractionUserPrompt, WATCHLIST_PROFILE_EXTRACTION_SYSTEM_PROMPT, } from "../prompts/analysis/index.js";
3
+ const MAX_PROFILE_DOCUMENTS = 8;
4
+ export class WatchlistProfileService {
5
+ mxApiService;
6
+ analysisService;
7
+ constructor(mxApiService, analysisService) {
8
+ this.mxApiService = mxApiService;
9
+ this.analysisService = analysisService;
10
+ }
11
+ async resolve(symbol, companyName, updatedAt) {
12
+ const themeQuery = buildThemeQuery(companyName, symbol);
13
+ const mxConfigError = this.mxApiService.getConfigurationError();
14
+ if (mxConfigError) {
15
+ throw new Error(mxConfigError);
16
+ }
17
+ const llmConfigError = this.analysisService.getConfigurationError();
18
+ if (llmConfigError) {
19
+ throw new Error(llmConfigError);
20
+ }
21
+ const documents = (await this.mxApiService.search(themeQuery)).slice(0, MAX_PROFILE_DOCUMENTS);
22
+ if (documents.length === 0) {
23
+ return {
24
+ sector: null,
25
+ themes: [],
26
+ themeQuery,
27
+ themeUpdatedAt: updatedAt,
28
+ };
29
+ }
30
+ const responseText = await this.analysisService.generateText(WATCHLIST_PROFILE_EXTRACTION_SYSTEM_PROMPT, buildWatchlistProfileExtractionUserPrompt({
31
+ symbol,
32
+ companyName,
33
+ documents,
34
+ }), {
35
+ maxTokens: 1200,
36
+ temperature: 0.1,
37
+ });
38
+ const profile = parseWatchlistProfileExtraction(responseText);
39
+ if (!profile) {
40
+ throw new Error(`watchlist profile extraction returned invalid JSON for ${symbol}`);
41
+ }
42
+ return {
43
+ sector: profile.sector,
44
+ themes: profile.themes,
45
+ themeQuery,
46
+ themeUpdatedAt: updatedAt,
47
+ };
48
+ }
49
+ }
50
+ export function buildBoardNewsQuery(profile) {
51
+ const keywords = [
52
+ String(profile.sector ?? "").trim(),
53
+ ...profile.themes
54
+ .map((item) => String(item ?? "").trim())
55
+ .filter(Boolean)
56
+ .slice(0, 3),
57
+ ].filter(Boolean);
58
+ if (keywords.length === 0) {
59
+ return null;
60
+ }
61
+ return `${keywords.join(" ")} 板块 题材 最新新闻 政策 资金`;
62
+ }
63
+ function buildThemeQuery(companyName, symbol) {
64
+ return `${companyName} ${symbol} 所属行业 板块 题材 概念`;
65
+ }
66
+ export function formatWatchlistProfileDocuments(documents) {
67
+ return documents
68
+ .slice(0, MAX_PROFILE_DOCUMENTS)
69
+ .map((document, index) => [
70
+ `${index + 1}. ${document.title}`,
71
+ `来源: ${document.source ?? "未知"}`,
72
+ `时间: ${document.publishedAt ?? "未知"}`,
73
+ `正文: ${document.trunk || "无"}`,
74
+ ].join("\n"))
75
+ .join("\n\n");
76
+ }
@@ -0,0 +1,43 @@
1
+ import type { WatchlistItem } from "../types/domain.js";
2
+ import { InstrumentService } from "./instrument-service.js";
3
+ import { WatchlistRepository } from "../storage/repositories/watchlist-repo.js";
4
+ import { WatchlistProfileService } from "./watchlist-profile-service.js";
5
+ interface GetWatchlistItemOptions {
6
+ refreshConceptBoards?: boolean;
7
+ }
8
+ export interface AddWatchlistResult {
9
+ item: WatchlistItem;
10
+ profileError: string | null;
11
+ }
12
+ export interface RefreshWatchlistProfilesOptions {
13
+ symbol?: string;
14
+ force?: boolean;
15
+ }
16
+ export interface WatchlistProfileRefreshFailure {
17
+ symbol: string;
18
+ name: string;
19
+ error: string;
20
+ }
21
+ export interface RefreshWatchlistProfilesResult {
22
+ targetCount: number;
23
+ updated: WatchlistItem[];
24
+ rechecked: WatchlistItem[];
25
+ failed: WatchlistProfileRefreshFailure[];
26
+ }
27
+ export declare class WatchlistService {
28
+ private readonly repository;
29
+ private readonly instrumentService;
30
+ private readonly watchlistProfileService;
31
+ constructor(repository: WatchlistRepository, instrumentService: InstrumentService, watchlistProfileService?: WatchlistProfileService | null);
32
+ add(symbolInput: string, costPrice?: number | null): Promise<AddWatchlistResult>;
33
+ list(): Promise<WatchlistItem[]>;
34
+ remove(symbolInput: string): Promise<boolean>;
35
+ getBySymbol(symbolInput: string, options?: GetWatchlistItemOptions): Promise<WatchlistItem | null>;
36
+ refreshProfiles(options?: RefreshWatchlistProfilesOptions): Promise<RefreshWatchlistProfilesResult>;
37
+ refreshNames(): Promise<{
38
+ updated: WatchlistItem[];
39
+ unchanged: WatchlistItem[];
40
+ }>;
41
+ private refreshConceptProfile;
42
+ }
43
+ export {};
@@ -0,0 +1,204 @@
1
+ import { formatChinaDateTime } from "../utils/china-time.js";
2
+ import { normalizeSymbol } from "../utils/symbol.js";
3
+ import { normalizeCostPrice } from "../utils/cost-price.js";
4
+ const CONCEPT_BOARD_REFRESH_DAYS = 5;
5
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
6
+ export class WatchlistService {
7
+ repository;
8
+ instrumentService;
9
+ watchlistProfileService;
10
+ constructor(repository, instrumentService, watchlistProfileService = null) {
11
+ this.repository = repository;
12
+ this.instrumentService = instrumentService;
13
+ this.watchlistProfileService = watchlistProfileService;
14
+ }
15
+ async add(symbolInput, costPrice = null) {
16
+ const symbol = normalizeSymbol(symbolInput);
17
+ const existing = await this.getBySymbol(symbol);
18
+ const normalizedCostPrice = normalizeCostPrice(costPrice);
19
+ const name = await this.instrumentService.resolveName(symbol);
20
+ const addedAt = formatChinaDateTime();
21
+ const fallbackProfile = {
22
+ sector: existing?.sector ?? null,
23
+ themes: existing?.themes ?? [],
24
+ themeQuery: existing?.themeQuery ?? null,
25
+ themeUpdatedAt: existing?.themeUpdatedAt ?? null,
26
+ };
27
+ let profile = fallbackProfile;
28
+ let profileError = null;
29
+ if (this.watchlistProfileService) {
30
+ try {
31
+ const resolved = await this.watchlistProfileService.resolve(symbol, name, addedAt);
32
+ profile = {
33
+ sector: resolved.sector ?? fallbackProfile.sector,
34
+ themes: resolved.themes.length > 0 ? resolved.themes : fallbackProfile.themes,
35
+ themeQuery: resolved.themeQuery ?? fallbackProfile.themeQuery,
36
+ themeUpdatedAt: resolved.themeUpdatedAt ?? fallbackProfile.themeUpdatedAt,
37
+ };
38
+ }
39
+ catch (error) {
40
+ profileError = toErrorMessage(error);
41
+ }
42
+ }
43
+ const item = {
44
+ symbol,
45
+ name,
46
+ costPrice: normalizedCostPrice ?? existing?.costPrice ?? null,
47
+ addedAt,
48
+ sector: profile.sector,
49
+ themes: profile.themes,
50
+ themeQuery: profile.themeQuery,
51
+ themeUpdatedAt: profile.themeUpdatedAt,
52
+ };
53
+ await this.repository.upsert(item);
54
+ return { item, profileError };
55
+ }
56
+ async list() {
57
+ return this.repository.list();
58
+ }
59
+ async remove(symbolInput) {
60
+ const symbol = normalizeSymbol(symbolInput);
61
+ return this.repository.remove(symbol);
62
+ }
63
+ async getBySymbol(symbolInput, options = {}) {
64
+ const symbol = normalizeSymbol(symbolInput);
65
+ const items = await this.repository.list();
66
+ const item = items.find((entry) => entry.symbol === symbol) ?? null;
67
+ if (!item || !options.refreshConceptBoards) {
68
+ return item;
69
+ }
70
+ const result = await this.refreshConceptProfile(item, { force: false });
71
+ return result.item;
72
+ }
73
+ async refreshProfiles(options = {}) {
74
+ const items = await this.repository.list();
75
+ const targetSymbol = options.symbol ? normalizeSymbol(options.symbol) : null;
76
+ const targets = targetSymbol
77
+ ? items.filter((item) => item.symbol === targetSymbol)
78
+ : items;
79
+ const result = {
80
+ targetCount: targets.length,
81
+ updated: [],
82
+ rechecked: [],
83
+ failed: [],
84
+ };
85
+ if (targets.length === 0) {
86
+ return result;
87
+ }
88
+ if (!this.watchlistProfileService) {
89
+ result.failed = targets.map((item) => ({
90
+ symbol: item.symbol,
91
+ name: item.name,
92
+ error: "watchlist profile service unavailable",
93
+ }));
94
+ return result;
95
+ }
96
+ const force = options.force ?? true;
97
+ for (const item of targets) {
98
+ try {
99
+ const refreshed = await this.refreshConceptProfile(item, { force });
100
+ if (refreshed.status === "updated") {
101
+ result.updated.push(refreshed.item);
102
+ continue;
103
+ }
104
+ if (refreshed.status === "rechecked") {
105
+ result.rechecked.push(refreshed.item);
106
+ }
107
+ }
108
+ catch (error) {
109
+ result.failed.push({
110
+ symbol: item.symbol,
111
+ name: item.name,
112
+ error: toErrorMessage(error),
113
+ });
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ async refreshNames() {
119
+ const items = await this.repository.list();
120
+ const updated = [];
121
+ const unchanged = [];
122
+ for (const item of items) {
123
+ const nextName = await this.instrumentService.resolveName(item.symbol);
124
+ const normalizedCurrent = sanitizeName(item.name, item.symbol);
125
+ if (normalizedCurrent === nextName) {
126
+ unchanged.push({ ...item, name: normalizedCurrent });
127
+ continue;
128
+ }
129
+ const nextItem = { ...item, name: nextName };
130
+ await this.repository.upsert(nextItem);
131
+ updated.push(nextItem);
132
+ }
133
+ return { updated, unchanged };
134
+ }
135
+ async refreshConceptProfile(item, options) {
136
+ if (!this.watchlistProfileService) {
137
+ return { item, status: "skipped" };
138
+ }
139
+ if (!options.force && !shouldRefreshConceptBoards(item)) {
140
+ return { item, status: "skipped" };
141
+ }
142
+ const refreshedAt = formatChinaDateTime();
143
+ const profile = await this.watchlistProfileService.resolve(item.symbol, item.name, refreshedAt);
144
+ const nextItem = {
145
+ ...item,
146
+ sector: profile.sector ?? item.sector ?? null,
147
+ themes: profile.themes.length > 0 ? profile.themes : item.themes,
148
+ themeQuery: profile.themeQuery ?? item.themeQuery ?? null,
149
+ themeUpdatedAt: profile.themeUpdatedAt ?? item.themeUpdatedAt ?? null,
150
+ };
151
+ const profileChanged = !hasSameConceptProfileContent(item, nextItem);
152
+ const timestampChanged = item.themeUpdatedAt !== nextItem.themeUpdatedAt;
153
+ if (profileChanged || timestampChanged) {
154
+ await this.repository.upsert(nextItem);
155
+ }
156
+ return {
157
+ item: profileChanged || timestampChanged ? nextItem : item,
158
+ status: profileChanged ? "updated" : "rechecked",
159
+ };
160
+ }
161
+ }
162
+ function shouldRefreshConceptBoards(item) {
163
+ if (!item.sector || item.themes.length === 0 || !item.themeUpdatedAt) {
164
+ return true;
165
+ }
166
+ const updatedAt = parseChinaTimestamp(item.themeUpdatedAt);
167
+ if (updatedAt == null) {
168
+ return true;
169
+ }
170
+ return Date.now() - updatedAt >= CONCEPT_BOARD_REFRESH_DAYS * ONE_DAY_MS;
171
+ }
172
+ function parseChinaTimestamp(value) {
173
+ const text = value.trim();
174
+ if (!text) {
175
+ return null;
176
+ }
177
+ const isoLike = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(text)
178
+ ? `${text.replace(" ", "T")}+08:00`
179
+ : text;
180
+ const timestamp = Date.parse(isoLike);
181
+ return Number.isFinite(timestamp) ? timestamp : null;
182
+ }
183
+ function hasSameConceptProfileContent(left, right) {
184
+ return left.sector === right.sector
185
+ && left.themeQuery === right.themeQuery
186
+ && left.themes.length === right.themes.length
187
+ && left.themes.every((value, index) => value === right.themes[index]);
188
+ }
189
+ function sanitizeName(name, symbol) {
190
+ const trimmed = (name || "").trim();
191
+ if (!trimmed) {
192
+ return symbol;
193
+ }
194
+ if (trimmed === symbol) {
195
+ return symbol;
196
+ }
197
+ if (trimmed.includes("http://") || trimmed.includes("https://")) {
198
+ return symbol;
199
+ }
200
+ return trimmed;
201
+ }
202
+ function toErrorMessage(error) {
203
+ return error instanceof Error ? error.message : String(error);
204
+ }
@@ -0,0 +1,23 @@
1
+ type LanceDbModule = typeof import("@lancedb/lancedb");
2
+ type LanceConnection = Awaited<ReturnType<LanceDbModule["connect"]>>;
3
+ type LanceTable = Awaited<ReturnType<LanceConnection["openTable"]>>;
4
+ export type DbRow = Record<string, unknown>;
5
+ export type DbSchema = Awaited<ReturnType<LanceTable["schema"]>>;
6
+ export declare class Database {
7
+ private readonly baseDir;
8
+ constructor(baseDir: string);
9
+ getConnection(): Promise<LanceConnection>;
10
+ hasTable(name: string): Promise<boolean>;
11
+ listTables(): Promise<string[]>;
12
+ openTable(name: string): Promise<LanceTable>;
13
+ createTable(name: string, rows: DbRow[], schema?: DbSchema): Promise<LanceTable>;
14
+ tableToArray<T>(name: string): Promise<T[]>;
15
+ describeTable(name: string): Promise<Array<{
16
+ name: string;
17
+ type: string;
18
+ nullable: boolean;
19
+ }>>;
20
+ private connect;
21
+ private listTableNames;
22
+ }
23
+ export {};