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
@@ -1,4 +1,5 @@
1
1
  import { formatConfigEnvFallback } from "../config/env.js";
2
+ import { normalizeSymbol } from "../utils/symbol.js";
2
3
  export class MxSearchServiceError extends Error {
3
4
  constructor(message) {
4
5
  super(message);
@@ -15,12 +16,12 @@ export class MxApiService {
15
16
  isConfigured() {
16
17
  return Boolean(this.apiBaseUrl.trim() && this.apiKey.trim());
17
18
  }
18
- getConfigurationError() {
19
+ getConfigurationError(featureName = "mx_search") {
19
20
  if (!this.apiBaseUrl.trim()) {
20
- return `mx_search 未配置接口地址,请设置 mxSearchApiUrl 或环境变量 ${formatConfigEnvFallback("mxSearchApiUrl")}`;
21
+ return `${featureName} 未配置接口地址,请设置 mxSearchApiUrl 或环境变量 ${formatConfigEnvFallback("mxSearchApiUrl")}`;
21
22
  }
22
23
  if (!this.apiKey.trim()) {
23
- return `mx_search 未配置 API Key,请设置插件配置 mxSearchApiKey 或环境变量 ${formatConfigEnvFallback("mxSearchApiKey")}`;
24
+ return `${featureName} 未配置 API Key,请设置插件配置 mxSearchApiKey 或环境变量 ${formatConfigEnvFallback("mxSearchApiKey")}`;
24
25
  }
25
26
  return null;
26
27
  }
@@ -40,8 +41,40 @@ export class MxApiService {
40
41
  }, "mx_select_stock");
41
42
  return normalizeMxSelectStockResult(json);
42
43
  }
44
+ async queryData(toolQuery) {
45
+ const normalizedQuery = toolQuery.trim();
46
+ if (!normalizedQuery) {
47
+ throw new MxSearchServiceError("mx_data requires query");
48
+ }
49
+ const json = await this.postJson("query", { toolQuery: normalizedQuery }, "mx_data");
50
+ const apiError = extractApiError(json);
51
+ if (apiError) {
52
+ throw new MxSearchServiceError(`mx_data 返回错误: ${apiError.code ?? "UNKNOWN"} ${apiError.message ?? ""}`.trim());
53
+ }
54
+ return normalizeMxDataResult(json);
55
+ }
56
+ async getSelfSelectWatchlist() {
57
+ const json = await this.postJson("self-select/get", {}, "mx_zixuan");
58
+ const apiError = extractApiError(json);
59
+ if (apiError) {
60
+ throw new MxSearchServiceError(`mx_zixuan 返回错误: ${apiError.code ?? "UNKNOWN"} ${apiError.message ?? ""}`.trim());
61
+ }
62
+ return normalizeMxSelfSelectResult(json);
63
+ }
64
+ async manageSelfSelect(query) {
65
+ const normalizedQuery = query.trim();
66
+ if (!normalizedQuery) {
67
+ throw new MxSearchServiceError("mx_zixuan requires query");
68
+ }
69
+ const json = await this.postJson("self-select/manage", { query: normalizedQuery }, "mx_zixuan");
70
+ const apiError = extractApiError(json);
71
+ if (apiError) {
72
+ throw new MxSearchServiceError(`mx_zixuan 返回错误: ${apiError.code ?? "UNKNOWN"} ${apiError.message ?? ""}`.trim());
73
+ }
74
+ return normalizeMxSelfSelectManageResult(json, normalizedQuery);
75
+ }
43
76
  async postJson(endpoint, body, toolName) {
44
- const configError = this.getConfigurationError();
77
+ const configError = this.getConfigurationError(toolName);
45
78
  if (configError) {
46
79
  throw new MxSearchServiceError(configError);
47
80
  }
@@ -65,7 +98,7 @@ export class MxSearchService extends MxApiService {
65
98
  function buildMxEndpointUrl(baseUrl, endpoint) {
66
99
  const trimmedBase = baseUrl.trim().replace(/\/+$/, "");
67
100
  const normalizedEndpoint = endpoint.replace(/^\/+/, "");
68
- const normalizedBase = trimmedBase.replace(/\/(news-search|stock-screen)$/i, "");
101
+ const normalizedBase = trimmedBase.replace(/\/(news-search|stock-screen|query|self-select\/get|self-select\/manage)$/i, "");
69
102
  if (trimmedBase.endsWith(`/${normalizedEndpoint}`)) {
70
103
  return trimmedBase;
71
104
  }
@@ -196,7 +229,7 @@ function toNullableString(value) {
196
229
  const text = value.trim();
197
230
  return text || null;
198
231
  }
199
- function normalizeMxSelectStockResult(value) {
232
+ export function normalizeMxSelectStockResult(value) {
200
233
  const root = asRecord(value);
201
234
  const data = asRecord(root.data);
202
235
  const nestedData = asRecord(data.data);
@@ -205,17 +238,42 @@ function normalizeMxSelectStockResult(value) {
205
238
  const businessCode = toNullableString(allResults.code) ?? toNullableString(nestedData.responseCode) ?? toNullableString(data.code);
206
239
  const rawTotalCondition = normalizeCondition(allResults.totalCondition);
207
240
  const flatTotalCondition = toNullableString(nestedData.totalCondition);
241
+ const structuredColumns = normalizeColumns(result.columns);
242
+ const structuredRows = normalizeDataList(result.dataList);
243
+ const partialTable = structuredRows.length === 0
244
+ ? parseMarkdownTable(toNullableString(nestedData.partialResults) ?? toNullableString(allResults.partialResults))
245
+ : null;
246
+ const fallbackColumns = partialTable
247
+ ? partialTable.fieldnames.map((fieldname) => ({
248
+ title: fieldname,
249
+ key: fieldname,
250
+ dateMsg: null,
251
+ sortable: false,
252
+ sortWay: null,
253
+ redGreenAble: false,
254
+ unit: null,
255
+ dataType: "String",
256
+ }))
257
+ : [];
258
+ const dataList = structuredRows.length > 0 ? structuredRows : (partialTable?.rows ?? []);
259
+ const columns = structuredColumns.length > 0 ? structuredColumns : fallbackColumns;
260
+ const dataSource = structuredRows.length > 0
261
+ ? "dataList"
262
+ : partialTable && partialTable.rows.length > 0
263
+ ? "partialResults"
264
+ : "none";
208
265
  return {
209
266
  status: toNullableNumber(root.status),
210
267
  message: toNullableString(root.message),
211
268
  code: businessCode,
212
269
  msg: toNullableString(data.message) ?? toNullableString(data.msg),
213
270
  resultType: toNullableNumber(result.resultType) ?? toNullableNumber(nestedData.resultType),
214
- total: toSafeNumber(result.total),
215
- totalRecordCount: toSafeNumber(result.totalRecordCount),
271
+ total: toSafeNumber(result.total) || dataList.length,
272
+ totalRecordCount: toSafeNumber(result.totalRecordCount) || dataList.length,
216
273
  parserText: toNullableString(nestedData.parserText),
217
- columns: normalizeColumns(result.columns),
218
- dataList: normalizeDataList(result.dataList),
274
+ dataSource,
275
+ columns,
276
+ dataList,
219
277
  responseConditionList: normalizeConditions(allResults.responseConditionList ?? nestedData.responseConditionList),
220
278
  totalCondition: rawTotalCondition ??
221
279
  (flatTotalCondition
@@ -226,6 +284,338 @@ function normalizeMxSelectStockResult(value) {
226
284
  : null),
227
285
  };
228
286
  }
287
+ export function normalizeMxDataResult(value) {
288
+ const root = asRecord(value);
289
+ const data = asRecord(root.data);
290
+ const nestedData = asRecord(data.data);
291
+ const searchResult = asRecord(nestedData.searchDataResultDTO ?? data.searchDataResultDTO);
292
+ const dtoList = Array.isArray(searchResult.dataTableDTOList)
293
+ ? searchResult.dataTableDTOList
294
+ : Array.isArray(searchResult.rawDataTableDTOList)
295
+ ? searchResult.rawDataTableDTOList
296
+ : [];
297
+ const tables = [];
298
+ const conditionParts = [];
299
+ for (const [index, item] of dtoList.entries()) {
300
+ if (typeof item !== "object" || item === null) {
301
+ continue;
302
+ }
303
+ const dto = item;
304
+ const condition = toNullableString(dto.condition);
305
+ const entityName = toNullableString(dto.entityName);
306
+ if (condition) {
307
+ conditionParts.push(`[${entityName ?? `表${index + 1}`}]\n${condition}`);
308
+ }
309
+ const table = normalizeMxDataTable(dto, index);
310
+ if (table.rows.length === 0) {
311
+ continue;
312
+ }
313
+ tables.push(table);
314
+ }
315
+ return {
316
+ status: toNullableNumber(root.status),
317
+ message: toNullableString(root.message),
318
+ questionId: toNullableString(searchResult.questionId),
319
+ entityTags: normalizeMxDataEntityTags(searchResult.entityTagDTOList ?? data.entityTagDTOList),
320
+ conditionParts,
321
+ tables,
322
+ totalRows: tables.reduce((sum, table) => sum + table.rows.length, 0),
323
+ };
324
+ }
325
+ function normalizeMxDataTable(dto, index) {
326
+ const title = toNullableString(dto.title) ??
327
+ toNullableString(dto.inputTitle) ??
328
+ toNullableString(dto.entityName) ??
329
+ `表${index + 1}`;
330
+ const tableValue = dto.table ?? dto.rawTable;
331
+ const { rows, fieldnames } = mxDataTableToRows(tableValue, dto.nameMap, dto.indicatorOrder, toNullableString(dto.entityName) ?? "指标", dto);
332
+ return {
333
+ title,
334
+ code: toNullableString(dto.code),
335
+ entityName: toNullableString(dto.entityName),
336
+ rows,
337
+ fieldnames,
338
+ };
339
+ }
340
+ function mxDataTableToRows(tableValue, nameMapValue, indicatorOrderValue, entityName, block) {
341
+ if (Array.isArray(tableValue)) {
342
+ return genericRowsToNamedRows(tableValue, nameMapValue);
343
+ }
344
+ if (typeof tableValue !== "object" || tableValue === null) {
345
+ return { rows: [], fieldnames: [] };
346
+ }
347
+ const table = tableValue;
348
+ const nameMap = normalizeStringMap(nameMapValue);
349
+ const headers = Array.isArray(table.headName) ? table.headName : [];
350
+ const order = orderedMxDataKeys(table, indicatorOrderValue);
351
+ const codeMap = normalizeStringMap(block.returnCodeMap ?? block.returnCodeNameMap ?? block.codeMap);
352
+ if (headers.length > 0) {
353
+ const dateColumn = nameMap.get("headNameSub") || nameMap.get("headName") || "date";
354
+ const fieldnames = [
355
+ dateColumn,
356
+ ...order
357
+ .map((key) => formatMxDataIndicatorLabel(key, nameMap, codeMap))
358
+ .filter(Boolean),
359
+ ];
360
+ const rows = headers.map((header, rowIndex) => {
361
+ const row = {
362
+ [dateColumn]: flattenMxValue(header),
363
+ };
364
+ for (const key of order) {
365
+ const label = formatMxDataIndicatorLabel(key, nameMap, codeMap);
366
+ if (!label) {
367
+ continue;
368
+ }
369
+ const values = table[key];
370
+ const cell = Array.isArray(values) ? values[rowIndex] : rowIndex === 0 ? values : "";
371
+ row[label] = flattenMxValue(cell);
372
+ }
373
+ return row;
374
+ });
375
+ return { rows, fieldnames };
376
+ }
377
+ const fieldnames = [entityName, "value"];
378
+ const rows = order
379
+ .map((key) => {
380
+ const label = formatMxDataIndicatorLabel(key, nameMap, codeMap);
381
+ if (!label) {
382
+ return null;
383
+ }
384
+ return {
385
+ [fieldnames[0]]: label,
386
+ [fieldnames[1]]: flattenMxValue(table[key]),
387
+ };
388
+ })
389
+ .filter((row) => row != null);
390
+ return { rows, fieldnames };
391
+ }
392
+ function genericRowsToNamedRows(value, nameMapValue) {
393
+ const records = value.filter((item) => typeof item === "object" && item !== null);
394
+ if (records.length === 0) {
395
+ return { rows: [], fieldnames: [] };
396
+ }
397
+ const nameMap = normalizeStringMap(nameMapValue);
398
+ const keys = Object.keys(records[0] ?? {});
399
+ const fieldnames = keys.map((key) => nameMap.get(key) || key);
400
+ const rows = records.map((record) => {
401
+ const row = {};
402
+ for (const key of keys) {
403
+ row[nameMap.get(key) || key] = flattenMxValue(record[key]);
404
+ }
405
+ return row;
406
+ });
407
+ return { rows, fieldnames };
408
+ }
409
+ function orderedMxDataKeys(table, indicatorOrderValue) {
410
+ const dataKeys = Object.keys(table).filter((key) => key !== "headName");
411
+ const ordered = [];
412
+ const seen = new Set();
413
+ const indicatorOrder = Array.isArray(indicatorOrderValue) ? indicatorOrderValue.map((item) => String(item)) : [];
414
+ for (const key of indicatorOrder) {
415
+ if (dataKeys.includes(key) && !seen.has(key)) {
416
+ ordered.push(key);
417
+ seen.add(key);
418
+ }
419
+ }
420
+ for (const key of dataKeys) {
421
+ if (!seen.has(key)) {
422
+ ordered.push(key);
423
+ seen.add(key);
424
+ }
425
+ }
426
+ return ordered;
427
+ }
428
+ function formatMxDataIndicatorLabel(key, nameMap, codeMap) {
429
+ const mapped = nameMap.get(key) ?? codeMap.get(key);
430
+ if (mapped) {
431
+ return mapped;
432
+ }
433
+ return /^\d+$/.test(key) ? "" : key;
434
+ }
435
+ function normalizeMxDataEntityTags(value) {
436
+ if (!Array.isArray(value)) {
437
+ return [];
438
+ }
439
+ return value
440
+ .filter((item) => typeof item === "object" && item !== null)
441
+ .map((item) => ({
442
+ fullName: toNullableString(item.fullName),
443
+ secuCode: toNullableString(item.secuCode),
444
+ marketChar: toNullableString(item.marketChar),
445
+ entityTypeName: toNullableString(item.entityTypeName),
446
+ className: toNullableString(item.className),
447
+ }));
448
+ }
449
+ function parseMarkdownTable(value) {
450
+ const text = String(value ?? "").trim();
451
+ if (!text) {
452
+ return null;
453
+ }
454
+ const lines = text
455
+ .split(/\r?\n/)
456
+ .map((line) => line.trim())
457
+ .filter(Boolean);
458
+ if (lines.length === 0) {
459
+ return null;
460
+ }
461
+ const header = splitMarkdownCells(lines[0] ?? "");
462
+ if (header.length === 0) {
463
+ return null;
464
+ }
465
+ const dataStart = lines[1] && /^[\s|:-]+$/.test(lines[1]) ? 2 : 1;
466
+ const rows = [];
467
+ for (const line of lines.slice(dataStart)) {
468
+ const cells = splitMarkdownCells(line);
469
+ if (cells.length === 0) {
470
+ continue;
471
+ }
472
+ const row = {};
473
+ for (const [index, fieldname] of header.entries()) {
474
+ row[fieldname] = cells[index] ?? "";
475
+ }
476
+ rows.push(row);
477
+ }
478
+ return rows.length > 0 ? { rows, fieldnames: header } : null;
479
+ }
480
+ function splitMarkdownCells(line) {
481
+ return line
482
+ .split("|")
483
+ .map((cell) => cell.trim())
484
+ .filter((cell, index, cells) => {
485
+ const isEdge = (index === 0 || index === cells.length - 1) && cell === "";
486
+ return !isEdge;
487
+ });
488
+ }
489
+ function normalizeStringMap(value) {
490
+ const map = new Map();
491
+ if (Array.isArray(value)) {
492
+ value.forEach((item, index) => {
493
+ const text = flattenMxValue(item);
494
+ if (text) {
495
+ map.set(String(index), text);
496
+ }
497
+ });
498
+ return map;
499
+ }
500
+ if (typeof value !== "object" || value === null) {
501
+ return map;
502
+ }
503
+ for (const [key, raw] of Object.entries(value)) {
504
+ const text = flattenMxValue(raw);
505
+ if (text) {
506
+ map.set(key, text);
507
+ }
508
+ }
509
+ return map;
510
+ }
511
+ function flattenMxValue(value) {
512
+ if (value == null) {
513
+ return "";
514
+ }
515
+ if (typeof value === "object") {
516
+ return JSON.stringify(value);
517
+ }
518
+ return String(value);
519
+ }
520
+ function normalizeMxSelfSelectResult(value) {
521
+ const root = asRecord(value);
522
+ const data = asRecord(root.data);
523
+ const nestedData = asRecord(data.data);
524
+ const allResults = asRecord(data.allResults ?? nestedData.allResults);
525
+ const result = asRecord(allResults.result ?? data.result ?? nestedData.result);
526
+ const rows = normalizeDataList(result.dataList ?? allResults.dataList ?? data.dataList ?? nestedData.dataList);
527
+ return {
528
+ status: toNullableNumber(root.status),
529
+ code: toNullableString(root.code) ?? toNullableString(allResults.code) ?? toNullableString(nestedData.responseCode),
530
+ message: toNullableString(root.message) ?? toNullableString(data.message) ?? toNullableString(data.msg),
531
+ columns: normalizeSelfSelectColumns(result.columns ?? allResults.columns ?? data.columns ?? nestedData.columns),
532
+ stocks: rows
533
+ .map((row) => normalizeSelfSelectStock(row))
534
+ .filter((item) => item != null),
535
+ raw: value,
536
+ };
537
+ }
538
+ function normalizeMxSelfSelectManageResult(value, query) {
539
+ const root = asRecord(value);
540
+ const data = asRecord(root.data);
541
+ const nestedData = asRecord(data.data);
542
+ const allResults = asRecord(data.allResults ?? nestedData.allResults);
543
+ return {
544
+ status: toNullableNumber(root.status),
545
+ code: toNullableString(root.code) ?? toNullableString(allResults.code) ?? toNullableString(nestedData.responseCode),
546
+ message: toNullableString(root.message) ??
547
+ toNullableString(data.message) ??
548
+ toNullableString(data.msg) ??
549
+ toNullableString(allResults.message) ??
550
+ "已完成",
551
+ query,
552
+ raw: value,
553
+ };
554
+ }
555
+ function normalizeSelfSelectColumns(value) {
556
+ if (!Array.isArray(value)) {
557
+ return [];
558
+ }
559
+ return value
560
+ .filter((item) => typeof item === "object" && item !== null)
561
+ .map((item) => ({
562
+ title: String(item.title ?? item.name ?? item.key ?? ""),
563
+ key: String(item.key ?? ""),
564
+ }))
565
+ .filter((item) => item.key);
566
+ }
567
+ function normalizeSelfSelectStock(value) {
568
+ const rawSymbol = pickFirstString(value, [
569
+ "SECURITY_CODE",
570
+ "SECUCODE",
571
+ "SECURITYCODE",
572
+ "secuCode",
573
+ "symbol",
574
+ "code",
575
+ ]) ?? null;
576
+ const symbol = normalizeSelfSelectSymbol(rawSymbol);
577
+ if (!symbol) {
578
+ return null;
579
+ }
580
+ return {
581
+ symbol,
582
+ rawSymbol,
583
+ name: pickFirstString(value, [
584
+ "SECURITY_SHORT_NAME",
585
+ "SECURITY_NAME_ABBR",
586
+ "SECURITY_NAME",
587
+ "secuName",
588
+ "name",
589
+ ]) ?? symbol,
590
+ latestPrice: pickFirstCell(value, ["NEWEST_PRICE", "LATEST_PRICE", "price"]),
591
+ changePercent: pickFirstCell(value, ["CHG", "CHANGE_PERCENT", "pctChg"]),
592
+ changeAmount: pickFirstCell(value, ["PCHG", "CHANGE", "change"]),
593
+ turnoverRate: pickFirstCell(value, ["010000_TURNOVER_RATE", "TURNOVER_RATE", "turnoverRate"]),
594
+ volumeRatio: pickFirstCell(value, ["010000_LIANGBI", "VOLUME_RATIO", "volumeRatio"]),
595
+ raw: value,
596
+ };
597
+ }
598
+ function normalizeSelfSelectSymbol(value) {
599
+ const text = String(value ?? "").trim().toUpperCase();
600
+ if (!text) {
601
+ return null;
602
+ }
603
+ const direct = text.match(/^\d{6}\.(SH|SZ|BJ)$/);
604
+ if (direct) {
605
+ return text;
606
+ }
607
+ const digits = text.match(/\d{6}/)?.[0];
608
+ return digits ? normalizeSymbol(digits) : null;
609
+ }
610
+ function pickFirstCell(value, keys) {
611
+ for (const key of keys) {
612
+ const candidate = value[key];
613
+ if (candidate != null && String(candidate).trim()) {
614
+ return String(candidate).trim();
615
+ }
616
+ }
617
+ return null;
618
+ }
229
619
  function normalizeColumns(value) {
230
620
  if (!Array.isArray(value)) {
231
621
  return [];
@@ -1,6 +1,6 @@
1
1
  import type { WatchlistItem } from "../types/domain.js";
2
2
  import { CompositeAnalysisOrchestrator } from "../analysis/orchestrators/composite-analysis.orchestrator.js";
3
- import type { CompositeAnalysisResult, PostCloseReviewResult, PriorKeyLevelValidationContext } from "../analysis/types/composite-analysis.js";
3
+ import type { CompositeAnalysisResult, IndustryPeerContext, PostCloseReviewResult, PriorKeyLevelValidationContext } from "../analysis/types/composite-analysis.js";
4
4
  import { PostCloseReviewTask } from "../analysis/tasks/post-close-review.task.js";
5
5
  import { WatchlistService } from "./watchlist-service.js";
6
6
  import { AnalysisService } from "./analysis-service.js";
@@ -10,6 +10,11 @@ import { KlinesRepository } from "../storage/repositories/klines-repo.js";
10
10
  import { IntradayKlinesRepository } from "../storage/repositories/intraday-klines-repo.js";
11
11
  import { Jin10FlashDeliveryRepository } from "../storage/repositories/jin10-flash-delivery-repo.js";
12
12
  import { Jin10FlashRepository } from "../storage/repositories/jin10-flash-repo.js";
13
+ import { IndustryPeerService } from "./industry-peer-service.js";
14
+ interface ReviewMarketSummary {
15
+ latestClose: number | null;
16
+ dailyChangePct: number | null;
17
+ }
13
18
  export interface PostCloseReviewRunResult {
14
19
  overviewMessage: string;
15
20
  detailMessages: string[];
@@ -26,7 +31,8 @@ export declare class PostCloseReviewService {
26
31
  private readonly intradayKlinesRepository;
27
32
  private readonly flashDeliveryRepository;
28
33
  private readonly flashRepository;
29
- constructor(watchlistService: WatchlistService, compositeAnalysisOrchestrator: CompositeAnalysisOrchestrator, analysisService: AnalysisService, postCloseReviewTask: PostCloseReviewTask, keyLevelsRepository: KeyLevelsRepository, keyLevelsHistoryRepository: KeyLevelsHistoryRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository, flashRepository: Jin10FlashRepository);
34
+ private readonly industryPeerService;
35
+ constructor(watchlistService: WatchlistService, compositeAnalysisOrchestrator: CompositeAnalysisOrchestrator, analysisService: AnalysisService, postCloseReviewTask: PostCloseReviewTask, keyLevelsRepository: KeyLevelsRepository, keyLevelsHistoryRepository: KeyLevelsHistoryRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository, flashRepository: Jin10FlashRepository, industryPeerService: IndustryPeerService);
30
36
  run(): Promise<PostCloseReviewRunResult>;
31
37
  private persistReview;
32
38
  private persistFallbackCompositeReview;
@@ -36,5 +42,6 @@ export declare class PostCloseReviewService {
36
42
  private formatDetailMessage;
37
43
  private formatFailureMessage;
38
44
  }
39
- export declare function formatPostCloseReviewDetailMessage(item: WatchlistItem, validation: PriorKeyLevelValidationContext, review: PostCloseReviewResult): string;
40
- export declare function formatPostCloseReviewFailureMessage(item: WatchlistItem, errorMessage: string, compositeResult: CompositeAnalysisResult | null): string;
45
+ export declare function formatPostCloseReviewDetailMessage(item: WatchlistItem, validation: PriorKeyLevelValidationContext, review: PostCloseReviewResult, marketSummary?: ReviewMarketSummary | null, peerContext?: IndustryPeerContext | null): string;
46
+ export declare function formatPostCloseReviewFailureMessage(item: WatchlistItem, errorMessage: string, compositeResult: CompositeAnalysisResult | null, marketSummary?: ReviewMarketSummary | null): string;
47
+ export {};