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,47 @@
1
+ export const ALL_FINANCIAL_SECTIONS = [
2
+ "income",
3
+ "metrics",
4
+ "cash_flow",
5
+ "balance_sheet",
6
+ ];
7
+ export class FinancialService {
8
+ client;
9
+ constructor(client) {
10
+ this.client = client;
11
+ }
12
+ async fetchIncome(symbol, options = {}) {
13
+ const response = await this.client.fetchIncome([symbol], options);
14
+ return sortFinancialRecords(response.data?.[symbol] ?? []);
15
+ }
16
+ async fetchMetrics(symbol, options = {}) {
17
+ const response = await this.client.fetchFinancialMetrics([symbol], options);
18
+ return sortFinancialRecords(response.data?.[symbol] ?? []);
19
+ }
20
+ async fetchCashFlow(symbol, options = {}) {
21
+ const response = await this.client.fetchCashFlow([symbol], options);
22
+ return sortFinancialRecords(response.data?.[symbol] ?? []);
23
+ }
24
+ async fetchBalanceSheet(symbol, options = {}) {
25
+ const response = await this.client.fetchBalanceSheet([symbol], options);
26
+ return sortFinancialRecords(response.data?.[symbol] ?? []);
27
+ }
28
+ async fetchSnapshot(symbol, options = {}, sections = ALL_FINANCIAL_SECTIONS) {
29
+ const requested = new Set(sections);
30
+ const [income, metrics, cashFlow, balanceSheet] = await Promise.all([
31
+ requested.has("income") ? this.fetchIncome(symbol, options) : Promise.resolve([]),
32
+ requested.has("metrics") ? this.fetchMetrics(symbol, options) : Promise.resolve([]),
33
+ requested.has("cash_flow") ? this.fetchCashFlow(symbol, options) : Promise.resolve([]),
34
+ requested.has("balance_sheet") ? this.fetchBalanceSheet(symbol, options) : Promise.resolve([]),
35
+ ]);
36
+ return {
37
+ symbol,
38
+ income,
39
+ metrics,
40
+ cashFlow,
41
+ balanceSheet,
42
+ };
43
+ }
44
+ }
45
+ function sortFinancialRecords(rows) {
46
+ return [...rows].sort((left, right) => right.period_end.localeCompare(left.period_end));
47
+ }
@@ -0,0 +1,9 @@
1
+ import type { IndicatorInputRow, IndicatorRow } from "../types/indicator.js";
2
+ export declare class IndicatorService {
3
+ private readonly pythonBin;
4
+ private readonly pythonArgs;
5
+ private readonly pythonWorkdir;
6
+ constructor(pythonBin: string, pythonArgs: string[], pythonWorkdir: string);
7
+ calculate(rows: IndicatorInputRow[]): Promise<IndicatorRow[]>;
8
+ private runPythonJson;
9
+ }
@@ -0,0 +1,67 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ export class IndicatorService {
4
+ pythonBin;
5
+ pythonArgs;
6
+ pythonWorkdir;
7
+ constructor(pythonBin, pythonArgs, pythonWorkdir) {
8
+ this.pythonBin = pythonBin;
9
+ this.pythonArgs = pythonArgs;
10
+ this.pythonWorkdir = pythonWorkdir;
11
+ }
12
+ async calculate(rows) {
13
+ if (rows.length === 0) {
14
+ throw new Error("K-line data must contain at least 1 row to calculate indicators");
15
+ }
16
+ const payload = rows.map((row) => ({
17
+ trade_date: row.trade_date,
18
+ trade_time: row.trade_time,
19
+ period: row.period,
20
+ timestamp: row.timestamp,
21
+ open: row.open,
22
+ high: row.high,
23
+ low: row.low,
24
+ close: row.close,
25
+ volume: row.volume,
26
+ amount: row.amount,
27
+ prev_close: row.prev_close,
28
+ }));
29
+ const raw = await this.runPythonJson(payload);
30
+ const parsed = JSON.parse(raw);
31
+ return parsed.map((row) => ({
32
+ ...row,
33
+ trade_date: String(row.trade_date),
34
+ trade_time: row.trade_time == null ? undefined : String(row.trade_time),
35
+ period: row.period == null ? undefined : String(row.period),
36
+ timestamp: row.timestamp == null ? undefined : Number(row.timestamp),
37
+ }));
38
+ }
39
+ runPythonJson(payload) {
40
+ const scriptPath = path.join(this.pythonWorkdir, "indicator_runner.py");
41
+ const child = spawn(this.pythonBin, [...this.pythonArgs, scriptPath], {
42
+ cwd: path.dirname(scriptPath),
43
+ stdio: ["pipe", "pipe", "pipe"],
44
+ });
45
+ return new Promise((resolve, reject) => {
46
+ let stdout = "";
47
+ let stderr = "";
48
+ child.stdout.on("data", (chunk) => {
49
+ stdout += chunk.toString();
50
+ });
51
+ child.stderr.on("data", (chunk) => {
52
+ stderr += chunk.toString();
53
+ });
54
+ child.on("error", reject);
55
+ child.on("close", (code) => {
56
+ if (code === 0) {
57
+ resolve(stdout);
58
+ return;
59
+ }
60
+ reject(new Error(`indicator_runner failed with code ${code}: ${stderr || stdout}` +
61
+ `\npython command: ${this.pythonBin} ${[...this.pythonArgs, scriptPath].join(" ")}`));
62
+ });
63
+ child.stdin.write(JSON.stringify(payload));
64
+ child.stdin.end();
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ import { TickFlowClient } from "./tickflow-client.js";
2
+ export declare class InstrumentService {
3
+ private readonly client;
4
+ constructor(client: TickFlowClient);
5
+ resolveName(symbol: string): Promise<string>;
6
+ }
@@ -0,0 +1,21 @@
1
+ export class InstrumentService {
2
+ client;
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ async resolveName(symbol) {
7
+ const instruments = await this.client.fetchInstruments([symbol]);
8
+ const matched = findBestInstrumentMatch(instruments, symbol);
9
+ const resolvedName = matched?.name?.trim();
10
+ return resolvedName || symbol;
11
+ }
12
+ }
13
+ function findBestInstrumentMatch(instruments, symbol) {
14
+ const normalizedSymbol = symbol.trim().toUpperCase();
15
+ const [targetCode, targetExchange = ""] = normalizedSymbol.split(".");
16
+ return (instruments.find((item) => item.symbol?.trim().toUpperCase() === normalizedSymbol) ??
17
+ instruments.find((item) => item.code?.trim().toUpperCase() === targetCode &&
18
+ item.exchange?.trim().toUpperCase() === targetExchange) ??
19
+ instruments.find((item) => item.code?.trim().toUpperCase() === targetCode) ??
20
+ instruments[0]);
21
+ }
@@ -0,0 +1,2 @@
1
+ export declare class KeyLevelService {
2
+ }
@@ -0,0 +1,2 @@
1
+ export class KeyLevelService {
2
+ }
@@ -0,0 +1,71 @@
1
+ import type { KeyLevelsHistoryEntry } from "../types/domain.js";
2
+ import { IntradayKlinesRepository } from "../storage/repositories/intraday-klines-repo.js";
3
+ import { KlinesRepository } from "../storage/repositories/klines-repo.js";
4
+ import { KeyLevelsHistoryRepository } from "../storage/repositories/key-levels-history-repo.js";
5
+ import { WatchlistService } from "./watchlist-service.js";
6
+ declare const HORIZONS: readonly [1, 3, 5];
7
+ type Horizon = (typeof HORIZONS)[number];
8
+ interface ReactionStats {
9
+ sampleCount: number;
10
+ touchCount: number;
11
+ validCount: number;
12
+ }
13
+ interface ThresholdStats {
14
+ sampleCount: number;
15
+ hitCount: number;
16
+ }
17
+ interface BreakthroughStats {
18
+ sampleCount: number;
19
+ hitCount: number;
20
+ confirmCount: number;
21
+ }
22
+ interface TradePathStats {
23
+ sampleCount: number;
24
+ stopFirstCount: number;
25
+ takeProfitFirstCount: number;
26
+ unresolvedCount: number;
27
+ sameDayConflictCount: number;
28
+ intradayResolvedCount: number;
29
+ intradayMissingCount: number;
30
+ intradayAmbiguousCount: number;
31
+ }
32
+ interface HorizonStats {
33
+ horizon: Horizon;
34
+ support: ReactionStats;
35
+ resistance: ReactionStats;
36
+ stopLoss: ThresholdStats;
37
+ takeProfit: ThresholdStats;
38
+ breakthrough: BreakthroughStats;
39
+ tradePath: TradePathStats;
40
+ }
41
+ export interface KeyLevelsBacktestReport {
42
+ scopeLabel: string;
43
+ snapshots: KeyLevelsHistoryEntry[];
44
+ recentSnapshots: KeyLevelsHistoryEntry[];
45
+ horizons: HorizonStats[];
46
+ conclusion: string;
47
+ }
48
+ export declare class KeyLevelsBacktestService {
49
+ private readonly keyLevelsHistoryRepository;
50
+ private readonly klinesRepository;
51
+ private readonly intradayKlinesRepository;
52
+ private readonly watchlistService;
53
+ constructor(keyLevelsHistoryRepository: KeyLevelsHistoryRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, watchlistService: WatchlistService);
54
+ buildReport(input?: {
55
+ symbol?: string;
56
+ recentLimit?: number;
57
+ }): Promise<KeyLevelsBacktestReport>;
58
+ render(input?: {
59
+ symbol?: string;
60
+ recentLimit?: number;
61
+ }): Promise<string>;
62
+ buildSummaryLines(input?: {
63
+ symbol?: string;
64
+ recentLimit?: number;
65
+ }): Promise<string[]>;
66
+ private listWatchlistSnapshots;
67
+ private loadKlinesMap;
68
+ private loadIntradayMap;
69
+ private buildHorizonStats;
70
+ }
71
+ export {};
@@ -0,0 +1,427 @@
1
+ import { normalizeSymbol } from "../utils/symbol.js";
2
+ const HORIZONS = [1, 3, 5];
3
+ const INTRADAY_PERIOD = "1m";
4
+ const LEVEL_BUFFER = 0.005;
5
+ const DEFAULT_RECENT_LIMIT = 5;
6
+ export class KeyLevelsBacktestService {
7
+ keyLevelsHistoryRepository;
8
+ klinesRepository;
9
+ intradayKlinesRepository;
10
+ watchlistService;
11
+ constructor(keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, watchlistService) {
12
+ this.keyLevelsHistoryRepository = keyLevelsHistoryRepository;
13
+ this.klinesRepository = klinesRepository;
14
+ this.intradayKlinesRepository = intradayKlinesRepository;
15
+ this.watchlistService = watchlistService;
16
+ }
17
+ async buildReport(input = {}) {
18
+ const symbol = input.symbol ? normalizeSymbol(input.symbol) : undefined;
19
+ const snapshots = symbol
20
+ ? await this.keyLevelsHistoryRepository.listBySymbol(symbol)
21
+ : await this.listWatchlistSnapshots();
22
+ const symbols = uniqueSymbols(snapshots);
23
+ const [klinesMap, intradayMap] = await Promise.all([
24
+ this.loadKlinesMap(symbols),
25
+ this.loadIntradayMap(symbols),
26
+ ]);
27
+ const horizons = HORIZONS.map((horizon) => this.buildHorizonStats(snapshots, klinesMap, intradayMap, horizon));
28
+ return {
29
+ scopeLabel: symbol ? `活动价位回测: ${symbol}` : "活动价位回测: 全部关注股",
30
+ snapshots,
31
+ recentSnapshots: snapshots.slice(0, input.recentLimit ?? DEFAULT_RECENT_LIMIT),
32
+ horizons,
33
+ conclusion: buildConclusion(horizons),
34
+ };
35
+ }
36
+ async render(input = {}) {
37
+ const report = await this.buildReport(input);
38
+ const lines = [`🧪 ${report.scopeLabel}`, `活动快照: ${report.snapshots.length} 条`];
39
+ if (report.snapshots.length === 0) {
40
+ lines.push("⚠️ 暂无活动价位历史快照,需先完成至少一次收盘后分析。", report.conclusion);
41
+ return lines.join("\n");
42
+ }
43
+ for (const horizon of report.horizons) {
44
+ lines.push("", `${horizon.horizon}日窗口:`, `• 支撑: 样本 ${horizon.support.sampleCount} | 触达 ${horizon.support.touchCount} (${formatRate(horizon.support.touchCount, horizon.support.sampleCount)}) | 守住 ${horizon.support.validCount} (${formatRate(horizon.support.validCount, horizon.support.touchCount)})`, `• 压力: 样本 ${horizon.resistance.sampleCount} | 触达 ${horizon.resistance.touchCount} (${formatRate(horizon.resistance.touchCount, horizon.resistance.sampleCount)}) | 压制 ${horizon.resistance.validCount} (${formatRate(horizon.resistance.validCount, horizon.resistance.touchCount)})`, `• 止损: 样本 ${horizon.stopLoss.sampleCount} | 触发 ${horizon.stopLoss.hitCount} (${formatRate(horizon.stopLoss.hitCount, horizon.stopLoss.sampleCount)}) | 未触发 ${horizon.stopLoss.sampleCount - horizon.stopLoss.hitCount} (${formatRate(horizon.stopLoss.sampleCount - horizon.stopLoss.hitCount, horizon.stopLoss.sampleCount)})`, `• 止盈: 样本 ${horizon.takeProfit.sampleCount} | 触发 ${horizon.takeProfit.hitCount} (${formatRate(horizon.takeProfit.hitCount, horizon.takeProfit.sampleCount)}) | 未触发 ${horizon.takeProfit.sampleCount - horizon.takeProfit.hitCount} (${formatRate(horizon.takeProfit.sampleCount - horizon.takeProfit.hitCount, horizon.takeProfit.sampleCount)})`, `• 突破: 样本 ${horizon.breakthrough.sampleCount} | 触达 ${horizon.breakthrough.hitCount} (${formatRate(horizon.breakthrough.hitCount, horizon.breakthrough.sampleCount)}) | 确认 ${horizon.breakthrough.confirmCount} (${formatRate(horizon.breakthrough.confirmCount, horizon.breakthrough.hitCount)})`, `• 路径: 双目标样本 ${horizon.tradePath.sampleCount} | 先止损 ${horizon.tradePath.stopFirstCount} (${formatRate(horizon.tradePath.stopFirstCount, horizon.tradePath.sampleCount)}) | 先止盈 ${horizon.tradePath.takeProfitFirstCount} (${formatRate(horizon.tradePath.takeProfitFirstCount, horizon.tradePath.sampleCount)}) | 未决 ${horizon.tradePath.unresolvedCount} (${formatRate(horizon.tradePath.unresolvedCount, horizon.tradePath.sampleCount)})`, `• 分钟判定: 同日冲突 ${horizon.tradePath.sameDayConflictCount} | 分钟解开 ${horizon.tradePath.intradayResolvedCount} | 缺分钟线 ${horizon.tradePath.intradayMissingCount} | 同分钟未决 ${horizon.tradePath.intradayAmbiguousCount}`);
45
+ }
46
+ if (input.symbol && report.recentSnapshots.length > 0) {
47
+ lines.push("", "最近活动价位快照:");
48
+ for (const snapshot of report.recentSnapshots) {
49
+ lines.push(`• ${snapshot.analysis_date} | 评分 ${formatMaybeInt(snapshot.score)} | 支撑 ${formatMaybePrice(snapshot.support)} | 压力 ${formatMaybePrice(snapshot.resistance)} | 突破 ${formatMaybePrice(snapshot.breakthrough)} | 止损 ${formatMaybePrice(snapshot.stop_loss)} | 止盈 ${formatMaybePrice(snapshot.take_profit)}`);
50
+ }
51
+ }
52
+ lines.push("", report.conclusion);
53
+ return lines.join("\n");
54
+ }
55
+ async buildSummaryLines(input = {}) {
56
+ const report = await this.buildReport(input);
57
+ const lines = [`🧪 ${report.scopeLabel} | 活动快照 ${report.snapshots.length} 条`];
58
+ for (const horizon of report.horizons) {
59
+ lines.push(`🧪 ${horizon.horizon}日: 支撑守住 ${formatRate(horizon.support.validCount, horizon.support.touchCount)} | 压力压制 ${formatRate(horizon.resistance.validCount, horizon.resistance.touchCount)} | 止损触发 ${formatRate(horizon.stopLoss.hitCount, horizon.stopLoss.sampleCount)} | 止盈触发 ${formatRate(horizon.takeProfit.hitCount, horizon.takeProfit.sampleCount)} | 突破确认 ${formatRate(horizon.breakthrough.confirmCount, horizon.breakthrough.hitCount)}`);
60
+ lines.push(`🧪 ${horizon.horizon}日先到: 止损 ${formatRate(horizon.tradePath.stopFirstCount, horizon.tradePath.sampleCount)} | 止盈 ${formatRate(horizon.tradePath.takeProfitFirstCount, horizon.tradePath.sampleCount)} | 未决 ${formatRate(horizon.tradePath.unresolvedCount, horizon.tradePath.sampleCount)} | 分钟判定 ${horizon.tradePath.intradayResolvedCount}/${horizon.tradePath.sameDayConflictCount}`);
61
+ }
62
+ lines.push(report.conclusion);
63
+ return lines;
64
+ }
65
+ async listWatchlistSnapshots() {
66
+ const watchlist = await this.watchlistService.list();
67
+ if (watchlist.length === 0) {
68
+ return [];
69
+ }
70
+ const symbols = new Set(watchlist.map((item) => item.symbol));
71
+ const snapshots = await this.keyLevelsHistoryRepository.listLatest();
72
+ return snapshots.filter((snapshot) => symbols.has(snapshot.symbol));
73
+ }
74
+ async loadKlinesMap(symbols) {
75
+ const entries = await Promise.all(symbols.map(async (symbol) => [symbol, await this.klinesRepository.listBySymbol(symbol)]));
76
+ return new Map(entries);
77
+ }
78
+ async loadIntradayMap(symbols) {
79
+ const entries = await Promise.all(symbols.map(async (symbol) => {
80
+ const rows = await this.intradayKlinesRepository.listBySymbol(symbol, INTRADAY_PERIOD);
81
+ return [symbol, groupIntradayByTradeDate(rows)];
82
+ }));
83
+ return new Map(entries);
84
+ }
85
+ buildHorizonStats(snapshots, klinesMap, intradayMap, horizon) {
86
+ const support = createReactionStats();
87
+ const resistance = createReactionStats();
88
+ const stopLoss = createThresholdStats();
89
+ const takeProfit = createThresholdStats();
90
+ const breakthrough = createBreakthroughStats();
91
+ const tradePath = createTradePathStats();
92
+ for (const snapshot of snapshots) {
93
+ const klines = klinesMap.get(snapshot.symbol) ?? [];
94
+ const intradayByDate = intradayMap.get(snapshot.symbol) ?? new Map();
95
+ const futureRows = getFutureRows(klines, snapshot.analysis_date, horizon);
96
+ if (futureRows.length < horizon) {
97
+ continue;
98
+ }
99
+ updateSupportStats(support, snapshot, futureRows);
100
+ updateResistanceStats(resistance, snapshot, futureRows);
101
+ updateStopLossStats(stopLoss, snapshot, futureRows);
102
+ updateTakeProfitStats(takeProfit, snapshot, futureRows);
103
+ updateBreakthroughStats(breakthrough, snapshot, futureRows);
104
+ updateTradePathStats(tradePath, snapshot, futureRows, intradayByDate);
105
+ }
106
+ return {
107
+ horizon,
108
+ support,
109
+ resistance,
110
+ stopLoss,
111
+ takeProfit,
112
+ breakthrough,
113
+ tradePath,
114
+ };
115
+ }
116
+ }
117
+ function createReactionStats() {
118
+ return {
119
+ sampleCount: 0,
120
+ touchCount: 0,
121
+ validCount: 0,
122
+ };
123
+ }
124
+ function createThresholdStats() {
125
+ return {
126
+ sampleCount: 0,
127
+ hitCount: 0,
128
+ };
129
+ }
130
+ function createBreakthroughStats() {
131
+ return {
132
+ sampleCount: 0,
133
+ hitCount: 0,
134
+ confirmCount: 0,
135
+ };
136
+ }
137
+ function createTradePathStats() {
138
+ return {
139
+ sampleCount: 0,
140
+ stopFirstCount: 0,
141
+ takeProfitFirstCount: 0,
142
+ unresolvedCount: 0,
143
+ sameDayConflictCount: 0,
144
+ intradayResolvedCount: 0,
145
+ intradayMissingCount: 0,
146
+ intradayAmbiguousCount: 0,
147
+ };
148
+ }
149
+ function updateSupportStats(stats, snapshot, futureRows) {
150
+ if (!(snapshot.support != null && snapshot.support > 0)) {
151
+ return;
152
+ }
153
+ stats.sampleCount += 1;
154
+ const touchUpper = snapshot.support * (1 + LEVEL_BUFFER);
155
+ const holdLower = snapshot.support * (1 - LEVEL_BUFFER);
156
+ const touchIndex = futureRows.findIndex((row) => row.low <= touchUpper);
157
+ if (touchIndex < 0) {
158
+ return;
159
+ }
160
+ stats.touchCount += 1;
161
+ const broken = futureRows.slice(touchIndex).some((row) => row.close < holdLower);
162
+ if (!broken) {
163
+ stats.validCount += 1;
164
+ }
165
+ }
166
+ function updateResistanceStats(stats, snapshot, futureRows) {
167
+ if (!(snapshot.resistance != null && snapshot.resistance > 0)) {
168
+ return;
169
+ }
170
+ stats.sampleCount += 1;
171
+ const touchLower = snapshot.resistance * (1 - LEVEL_BUFFER);
172
+ const holdUpper = snapshot.resistance * (1 + LEVEL_BUFFER);
173
+ const touchIndex = futureRows.findIndex((row) => row.high >= touchLower);
174
+ if (touchIndex < 0) {
175
+ return;
176
+ }
177
+ stats.touchCount += 1;
178
+ const broken = futureRows.slice(touchIndex).some((row) => row.close > holdUpper);
179
+ if (!broken) {
180
+ stats.validCount += 1;
181
+ }
182
+ }
183
+ function updateStopLossStats(stats, snapshot, futureRows) {
184
+ if (!(snapshot.stop_loss != null && snapshot.stop_loss > 0)) {
185
+ return;
186
+ }
187
+ stats.sampleCount += 1;
188
+ if (futureRows.some((row) => row.low <= snapshot.stop_loss)) {
189
+ stats.hitCount += 1;
190
+ }
191
+ }
192
+ function updateTakeProfitStats(stats, snapshot, futureRows) {
193
+ if (!(snapshot.take_profit != null && snapshot.take_profit > 0)) {
194
+ return;
195
+ }
196
+ stats.sampleCount += 1;
197
+ if (futureRows.some((row) => row.high >= snapshot.take_profit)) {
198
+ stats.hitCount += 1;
199
+ }
200
+ }
201
+ function updateBreakthroughStats(stats, snapshot, futureRows) {
202
+ if (!(snapshot.breakthrough != null && snapshot.breakthrough > 0)) {
203
+ return;
204
+ }
205
+ stats.sampleCount += 1;
206
+ const touchIndex = futureRows.findIndex((row) => row.high >= snapshot.breakthrough);
207
+ if (touchIndex < 0) {
208
+ return;
209
+ }
210
+ stats.hitCount += 1;
211
+ const confirmed = futureRows
212
+ .slice(touchIndex)
213
+ .some((row) => row.close >= snapshot.breakthrough * (1 + LEVEL_BUFFER));
214
+ if (confirmed) {
215
+ stats.confirmCount += 1;
216
+ }
217
+ }
218
+ function updateTradePathStats(stats, snapshot, futureRows, intradayByDate) {
219
+ if (!(snapshot.stop_loss != null && snapshot.stop_loss > 0)) {
220
+ return;
221
+ }
222
+ if (!(snapshot.take_profit != null && snapshot.take_profit > 0)) {
223
+ return;
224
+ }
225
+ stats.sampleCount += 1;
226
+ const stopIndex = futureRows.findIndex((row) => row.low <= snapshot.stop_loss);
227
+ const takeProfitIndex = futureRows.findIndex((row) => row.high >= snapshot.take_profit);
228
+ if (stopIndex < 0 && takeProfitIndex < 0) {
229
+ stats.unresolvedCount += 1;
230
+ return;
231
+ }
232
+ if (stopIndex >= 0 && takeProfitIndex < 0) {
233
+ stats.stopFirstCount += 1;
234
+ return;
235
+ }
236
+ if (takeProfitIndex >= 0 && stopIndex < 0) {
237
+ stats.takeProfitFirstCount += 1;
238
+ return;
239
+ }
240
+ if (stopIndex < takeProfitIndex) {
241
+ stats.stopFirstCount += 1;
242
+ return;
243
+ }
244
+ if (takeProfitIndex < stopIndex) {
245
+ stats.takeProfitFirstCount += 1;
246
+ return;
247
+ }
248
+ stats.sameDayConflictCount += 1;
249
+ const tradeDate = futureRows[stopIndex]?.trade_date;
250
+ const resolution = resolveSameDayTradePath(snapshot.stop_loss, snapshot.take_profit, tradeDate ? intradayByDate.get(tradeDate) ?? [] : []);
251
+ if (resolution === "stop_first") {
252
+ stats.stopFirstCount += 1;
253
+ stats.intradayResolvedCount += 1;
254
+ return;
255
+ }
256
+ if (resolution === "take_profit_first") {
257
+ stats.takeProfitFirstCount += 1;
258
+ stats.intradayResolvedCount += 1;
259
+ return;
260
+ }
261
+ if (resolution === "missing_intraday") {
262
+ stats.intradayMissingCount += 1;
263
+ stats.unresolvedCount += 1;
264
+ return;
265
+ }
266
+ stats.intradayAmbiguousCount += 1;
267
+ stats.unresolvedCount += 1;
268
+ }
269
+ function resolveSameDayTradePath(stopLoss, takeProfit, intradayRows) {
270
+ if (intradayRows.length === 0) {
271
+ return "missing_intraday";
272
+ }
273
+ for (const row of intradayRows) {
274
+ const hitsStop = row.low <= stopLoss;
275
+ const hitsTakeProfit = row.high >= takeProfit;
276
+ if (!hitsStop && !hitsTakeProfit) {
277
+ continue;
278
+ }
279
+ if (hitsStop && !hitsTakeProfit) {
280
+ return "stop_first";
281
+ }
282
+ if (hitsTakeProfit && !hitsStop) {
283
+ return "take_profit_first";
284
+ }
285
+ if (row.open <= stopLoss) {
286
+ return "stop_first";
287
+ }
288
+ if (row.open >= takeProfit) {
289
+ return "take_profit_first";
290
+ }
291
+ return "ambiguous";
292
+ }
293
+ return "ambiguous";
294
+ }
295
+ function groupIntradayByTradeDate(rows) {
296
+ const grouped = new Map();
297
+ for (const row of rows) {
298
+ const list = grouped.get(row.trade_date);
299
+ if (list) {
300
+ list.push(row);
301
+ continue;
302
+ }
303
+ grouped.set(row.trade_date, [row]);
304
+ }
305
+ return grouped;
306
+ }
307
+ function getFutureRows(klines, analysisDate, horizon) {
308
+ const index = klines.findIndex((row) => row.trade_date === analysisDate);
309
+ if (index < 0) {
310
+ return [];
311
+ }
312
+ return klines.slice(index + 1, index + 1 + horizon);
313
+ }
314
+ function uniqueSymbols(snapshots) {
315
+ return [...new Set(snapshots.map((snapshot) => snapshot.symbol))];
316
+ }
317
+ function buildConclusion(horizons) {
318
+ const primary = [...horizons].reverse().find((item) => hasSamples(item));
319
+ if (!primary) {
320
+ return "💡 结论: 历史样本不足,先持续积累收盘后活动价位快照,再观察是否需要调整刷新频率。";
321
+ }
322
+ const supportHold = ratio(primary.support.validCount, primary.support.touchCount);
323
+ const resistanceHold = ratio(primary.resistance.validCount, primary.resistance.touchCount);
324
+ const touchAverage = average([
325
+ ratio(primary.support.touchCount, primary.support.sampleCount),
326
+ ratio(primary.resistance.touchCount, primary.resistance.sampleCount),
327
+ ]);
328
+ const holdAverage = average([supportHold, resistanceHold]);
329
+ const stopHit = ratio(primary.stopLoss.hitCount, primary.stopLoss.sampleCount);
330
+ const takeProfitHit = ratio(primary.takeProfit.hitCount, primary.takeProfit.sampleCount);
331
+ const stopFirst = ratio(primary.tradePath.stopFirstCount, primary.tradePath.sampleCount);
332
+ const takeProfitFirst = ratio(primary.tradePath.takeProfitFirstCount, primary.tradePath.sampleCount);
333
+ const breakthroughConfirm = ratio(primary.breakthrough.confirmCount, primary.breakthrough.hitCount);
334
+ const intradayResolved = ratio(primary.tradePath.intradayResolvedCount, primary.tradePath.sameDayConflictCount);
335
+ const intradayMissing = ratio(primary.tradePath.intradayMissingCount, primary.tradePath.sameDayConflictCount);
336
+ const parts = [];
337
+ if (countSamples(primary) < 12) {
338
+ parts.push("当前可评估样本仍偏少,结论只可作方向参考");
339
+ }
340
+ if (holdAverage != null && holdAverage >= 0.65) {
341
+ parts.push("支撑压力整体稳定,维持收盘后日更刷新即可,无需盘中频繁重算");
342
+ }
343
+ else if (holdAverage != null && holdAverage >= 0.45) {
344
+ parts.push("支撑压力有效性中性,建议继续日更刷新,并在临近触达时手动复核");
345
+ }
346
+ else if (holdAverage != null) {
347
+ parts.push("支撑压力失效偏快,建议缩短有效期,并在波动日额外复核");
348
+ }
349
+ if (touchAverage != null && touchAverage < 0.2) {
350
+ parts.push("支撑压力触达率偏低,说明部分关键位可能离现价偏远,可适度收窄区间");
351
+ }
352
+ if (supportHold != null && resistanceHold != null) {
353
+ if (supportHold >= resistanceHold + 0.15) {
354
+ parts.push("下沿支撑的稳定性明显强于上沿压力");
355
+ }
356
+ else if (resistanceHold >= supportHold + 0.15) {
357
+ parts.push("上沿压力的压制性明显强于下沿支撑");
358
+ }
359
+ }
360
+ if (takeProfitFirst != null && stopFirst != null) {
361
+ if (takeProfitFirst >= stopFirst + 0.15) {
362
+ parts.push("双目标样本里止盈先到明显多于止损先到,盈亏比结构偏正");
363
+ }
364
+ else if (stopFirst >= takeProfitFirst + 0.15) {
365
+ parts.push("双目标样本里止损先到偏多,当前活动价位的风险收益结构偏弱");
366
+ }
367
+ }
368
+ if (takeProfitHit != null && stopHit != null) {
369
+ if (takeProfitHit >= stopHit + 0.15) {
370
+ parts.push("止盈触发率高于止损触发率,关键价位具备一定兑现能力");
371
+ }
372
+ else if (stopHit >= takeProfitHit + 0.15) {
373
+ parts.push("止损触发率高于止盈触发率,关键价位偏保守或偏离真实波动结构");
374
+ }
375
+ }
376
+ if (primary.breakthrough.hitCount > 0 && breakthroughConfirm != null) {
377
+ if (breakthroughConfirm >= 0.6) {
378
+ parts.push("突破位在被触发后具备较高确认率");
379
+ }
380
+ else if (breakthroughConfirm < 0.35) {
381
+ parts.push("突破位经常被试探但确认率偏低,需警惕假突破");
382
+ }
383
+ }
384
+ if (primary.tradePath.sameDayConflictCount > 0 && intradayResolved != null && intradayResolved < 0.5) {
385
+ parts.push("同日双触发样本里,分钟线可判定比例仍偏低,路径结论置信度一般");
386
+ }
387
+ if (primary.tradePath.sameDayConflictCount > 0 && intradayMissing != null && intradayMissing >= 0.35) {
388
+ parts.push("历史分钟线覆盖仍有缺口,部分同日双触发样本只能保留为未决");
389
+ }
390
+ return `💡 结论: ${parts.join(";") || "当前回测统计没有形成明显倾向,先按收盘后日更节奏持续观察。"}`;
391
+ }
392
+ function hasSamples(stats) {
393
+ return countSamples(stats) > 0;
394
+ }
395
+ function countSamples(stats) {
396
+ return (stats.support.sampleCount +
397
+ stats.resistance.sampleCount +
398
+ stats.stopLoss.sampleCount +
399
+ stats.takeProfit.sampleCount +
400
+ stats.breakthrough.sampleCount +
401
+ stats.tradePath.sampleCount);
402
+ }
403
+ function ratio(numerator, denominator) {
404
+ if (!(denominator > 0)) {
405
+ return null;
406
+ }
407
+ return numerator / denominator;
408
+ }
409
+ function average(values) {
410
+ const normalized = values.filter((value) => value != null);
411
+ if (normalized.length === 0) {
412
+ return null;
413
+ }
414
+ return normalized.reduce((sum, value) => sum + value, 0) / normalized.length;
415
+ }
416
+ function formatRate(numerator, denominator) {
417
+ if (!(denominator > 0)) {
418
+ return "样本不足";
419
+ }
420
+ return `${((numerator / denominator) * 100).toFixed(1)}%`;
421
+ }
422
+ function formatMaybePrice(value) {
423
+ return value == null ? "-" : value.toFixed(2);
424
+ }
425
+ function formatMaybeInt(value) {
426
+ return value == null ? "-" : String(Math.trunc(value));
427
+ }
@@ -0,0 +1,19 @@
1
+ import type { TickFlowIntradayKlineRow, TickFlowKlineRow } from "../types/tickflow.js";
2
+ import { TickFlowClient } from "./tickflow-client.js";
3
+ export declare class KlineService {
4
+ private readonly client;
5
+ constructor(client: TickFlowClient);
6
+ fetchKlines(symbol: string, options?: {
7
+ period?: string;
8
+ count?: number;
9
+ adjust?: string;
10
+ startTime?: number;
11
+ endTime?: number;
12
+ }): Promise<TickFlowKlineRow[]>;
13
+ fetchIntradayKlines(symbol: string, options?: {
14
+ period?: string;
15
+ count?: number;
16
+ }): Promise<TickFlowIntradayKlineRow[]>;
17
+ private toDailyRows;
18
+ private toIntradayRows;
19
+ }