tickflow-assist 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/dist/background/realtime-monitor.worker.d.ts +1 -1
- package/dist/background/realtime-monitor.worker.js +3 -4
- package/dist/bootstrap.js +9 -0
- package/dist/dev/run-monitor-loop.js +0 -1
- package/dist/plugin-commands.js +27 -0
- package/dist/prompts/analysis/pre-market-brief-prompt.d.ts +1 -1
- package/dist/prompts/analysis/pre-market-brief-prompt.js +4 -3
- package/dist/services/alert-service.js +34 -4
- package/dist/services/monitor-service.d.ts +1 -1
- package/dist/services/monitor-service.js +18 -9
- package/dist/services/mx-search-service.d.ts +8 -1
- package/dist/services/mx-search-service.js +400 -10
- package/dist/services/pre-market-brief-service.js +343 -39
- package/dist/services/watchlist-service.d.ts +5 -1
- package/dist/services/watchlist-service.js +8 -3
- package/dist/tools/eastmoney-watchlist.tool.d.ts +31 -0
- package/dist/tools/eastmoney-watchlist.tool.js +294 -0
- package/dist/tools/mx-data.tool.d.ts +8 -0
- package/dist/tools/mx-data.tool.js +94 -0
- package/dist/tools/mx-select-stock.tool.js +6 -2
- package/dist/tools/screen-stock-candidates.tool.d.ts +34 -0
- package/dist/tools/screen-stock-candidates.tool.js +477 -0
- package/dist/types/mx-data.d.ts +23 -0
- package/dist/types/mx-data.js +1 -0
- package/dist/types/mx-select-stock.d.ts +1 -0
- package/dist/types/mx-self-select.d.ts +30 -0
- package/dist/types/mx-self-select.js +1 -0
- package/openclaw.plugin.json +143 -24
- package/package.json +9 -9
- package/skills/stock-analysis/SKILL.md +31 -2
- package/skills/usage-help/SKILL.md +33 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { formatTickflowApiKeyLevel, supportsIntradayKlines, } from "../config/tickflow-access.js";
|
|
2
|
+
import { formatChinaDateTime } from "../utils/china-time.js";
|
|
3
|
+
import { normalizeSymbol } from "../utils/symbol.js";
|
|
4
|
+
import { resolveTickFlowQuoteChangePct } from "../utils/tickflow-quote.js";
|
|
5
|
+
const DEFAULT_CANDIDATE_LIMIT = 3;
|
|
6
|
+
const MAX_CANDIDATE_LIMIT = 8;
|
|
7
|
+
const DEFAULT_DAILY_KLINE_COUNT = 20;
|
|
8
|
+
const MAX_DAILY_KLINE_COUNT = 60;
|
|
9
|
+
const DEFAULT_INTRADAY_COUNT = 20;
|
|
10
|
+
const MAX_INTRADAY_COUNT = 60;
|
|
11
|
+
const MAX_INTRADAY_CANDIDATES = 3;
|
|
12
|
+
const MAX_FINANCIAL_CANDIDATES = 2;
|
|
13
|
+
function parseInput(rawInput) {
|
|
14
|
+
if (typeof rawInput === "string" && rawInput.trim()) {
|
|
15
|
+
return buildInput({ keyword: rawInput.trim() });
|
|
16
|
+
}
|
|
17
|
+
if (typeof rawInput === "object" && rawInput !== null) {
|
|
18
|
+
const input = rawInput;
|
|
19
|
+
const keyword = String(input.keyword ?? input.query ?? input.q ?? "").trim();
|
|
20
|
+
if (!keyword) {
|
|
21
|
+
throw new Error("screen_stock_candidates requires keyword");
|
|
22
|
+
}
|
|
23
|
+
return buildInput({
|
|
24
|
+
keyword,
|
|
25
|
+
limit: input.limit,
|
|
26
|
+
dailyKlineCount: input.dailyKlineCount ?? input.klineCount,
|
|
27
|
+
includeDailyKlines: input.includeDailyKlines,
|
|
28
|
+
includeIntraday: input.includeIntraday,
|
|
29
|
+
intradayCount: input.intradayCount,
|
|
30
|
+
includeFinancial: input.includeFinancial,
|
|
31
|
+
summarize: input.summarize ?? input.llm,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
throw new Error("invalid screen_stock_candidates input");
|
|
35
|
+
}
|
|
36
|
+
function buildInput(input) {
|
|
37
|
+
return {
|
|
38
|
+
keyword: input.keyword,
|
|
39
|
+
limit: parsePositiveInteger(input.limit, DEFAULT_CANDIDATE_LIMIT, MAX_CANDIDATE_LIMIT),
|
|
40
|
+
dailyKlineCount: parsePositiveInteger(input.dailyKlineCount, DEFAULT_DAILY_KLINE_COUNT, MAX_DAILY_KLINE_COUNT),
|
|
41
|
+
includeDailyKlines: parseOptionalBoolean(input.includeDailyKlines, true),
|
|
42
|
+
includeIntraday: parseOptionalBoolean(input.includeIntraday, false),
|
|
43
|
+
intradayCount: parsePositiveInteger(input.intradayCount, DEFAULT_INTRADAY_COUNT, MAX_INTRADAY_COUNT),
|
|
44
|
+
includeFinancial: parseOptionalBoolean(input.includeFinancial, false),
|
|
45
|
+
summarize: parseOptionalBoolean(input.summarize, false),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function screenStockCandidatesTool(tickflowApiKeyLevel, mxApiService, quoteService, klineService, financialService, watchlistService, analysisService) {
|
|
49
|
+
return {
|
|
50
|
+
name: "screen_stock_candidates",
|
|
51
|
+
description: "Build a small enriched stock candidate pool from MX smart screening plus TickFlow quote/daily K-line data, with strict candidate limits by design.",
|
|
52
|
+
async run({ rawInput }) {
|
|
53
|
+
let input;
|
|
54
|
+
try {
|
|
55
|
+
input = parseInput(rawInput);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return `智能选股联动失败😔 ${formatError(error)}`;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const mxResult = await mxApiService.selectStocks({
|
|
62
|
+
keyword: input.keyword,
|
|
63
|
+
pageNo: 1,
|
|
64
|
+
pageSize: Math.max(20, input.limit),
|
|
65
|
+
});
|
|
66
|
+
const candidates = extractStockCandidatesFromMxResult(mxResult, input.limit);
|
|
67
|
+
if (candidates.length === 0) {
|
|
68
|
+
return [
|
|
69
|
+
`🧭 智能选股候选池: ${input.keyword}`,
|
|
70
|
+
renderMxSummary(mxResult),
|
|
71
|
+
"⚠️ 未解析到可用于 TickFlow 补数据的股票代码。",
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
const symbols = candidates.map((candidate) => candidate.symbol);
|
|
75
|
+
const notes = [];
|
|
76
|
+
const [watchlistSymbols, quotesBySymbol, dailyBySymbol, intradayBySymbol, financialBySymbol] = await Promise.all([
|
|
77
|
+
loadWatchlistSymbols(watchlistService, notes),
|
|
78
|
+
loadQuotes(quoteService, symbols, notes),
|
|
79
|
+
input.includeDailyKlines
|
|
80
|
+
? loadDailyKlines(klineService, candidates, input.dailyKlineCount, notes)
|
|
81
|
+
: Promise.resolve(new Map()),
|
|
82
|
+
input.includeIntraday && supportsIntradayKlines(tickflowApiKeyLevel)
|
|
83
|
+
? loadIntradayKlines(klineService, candidates.slice(0, MAX_INTRADAY_CANDIDATES), input.intradayCount, notes)
|
|
84
|
+
: Promise.resolve(new Map()),
|
|
85
|
+
input.includeFinancial && tickflowApiKeyLevel === "expert"
|
|
86
|
+
? loadFinancialMetrics(financialService, candidates.slice(0, MAX_FINANCIAL_CANDIDATES), notes)
|
|
87
|
+
: Promise.resolve(new Map()),
|
|
88
|
+
]);
|
|
89
|
+
const deterministicText = renderCandidatePool({
|
|
90
|
+
input,
|
|
91
|
+
tickflowApiKeyLevel,
|
|
92
|
+
mxResult,
|
|
93
|
+
candidates,
|
|
94
|
+
watchlistSymbols,
|
|
95
|
+
quotesBySymbol,
|
|
96
|
+
dailyBySymbol,
|
|
97
|
+
intradayBySymbol,
|
|
98
|
+
financialBySymbol,
|
|
99
|
+
notes,
|
|
100
|
+
});
|
|
101
|
+
if (!input.summarize) {
|
|
102
|
+
return deterministicText;
|
|
103
|
+
}
|
|
104
|
+
return appendLlmSummary(deterministicText, analysisService);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return `智能选股联动失败😔 ${formatError(error)}`;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function appendLlmSummary(deterministicText, analysisService) {
|
|
113
|
+
const configError = analysisService.getConfigurationError();
|
|
114
|
+
if (configError) {
|
|
115
|
+
return [
|
|
116
|
+
deterministicText,
|
|
117
|
+
"",
|
|
118
|
+
"LLM整理:",
|
|
119
|
+
`⚠️ ${configError}`,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const summary = await analysisService.generateText([
|
|
124
|
+
"你是 A 股候选池整理助手。",
|
|
125
|
+
"只能基于用户提供的候选池文本进行整理,不得引入外部事实、不得改写或臆造数值。",
|
|
126
|
+
"候选池文本未提供的字段必须写“未提供”或“需另查”,禁止按股票名称推断主营业务、概念归属、产业链位置或公告事件。",
|
|
127
|
+
"输出中文,简洁,突出优先级、主要看点、风险点和下一步验证动作。",
|
|
128
|
+
"这不是投资建议,不要给买卖指令。",
|
|
129
|
+
].join("\n"), [
|
|
130
|
+
"请整理下面的智能选股候选池结果。",
|
|
131
|
+
"要求:",
|
|
132
|
+
"1. 给出候选优先级排序和理由。",
|
|
133
|
+
"2. 标出需要排除或谨慎的点。",
|
|
134
|
+
"3. 给出下一步最多 3 个验证动作。",
|
|
135
|
+
"4. 不要重复大段原始表格。",
|
|
136
|
+
"5. 不要提及候选公司主营业务或具体概念归属,除非原文候选明细已提供该字段。",
|
|
137
|
+
"",
|
|
138
|
+
deterministicText,
|
|
139
|
+
].join("\n"), {
|
|
140
|
+
maxTokens: 900,
|
|
141
|
+
temperature: 0.2,
|
|
142
|
+
});
|
|
143
|
+
return [
|
|
144
|
+
deterministicText,
|
|
145
|
+
"",
|
|
146
|
+
"LLM整理:",
|
|
147
|
+
summary,
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return [
|
|
152
|
+
deterministicText,
|
|
153
|
+
"",
|
|
154
|
+
"LLM整理:",
|
|
155
|
+
`⚠️ LLM整理失败: ${formatError(error)}`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function extractStockCandidatesFromMxResult(result, limit) {
|
|
160
|
+
const columns = result.columns;
|
|
161
|
+
const keyMap = buildCandidateColumnKeyMap(columns);
|
|
162
|
+
const candidates = [];
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (const row of result.dataList) {
|
|
165
|
+
const code = readCell(row, keyMap.codeKeys);
|
|
166
|
+
if (!code) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const market = readCell(row, keyMap.marketKeys);
|
|
170
|
+
const symbol = normalizeCandidateSymbol(code, market);
|
|
171
|
+
if (!symbol || seen.has(symbol)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
seen.add(symbol);
|
|
175
|
+
candidates.push({
|
|
176
|
+
rank: candidates.length + 1,
|
|
177
|
+
symbol,
|
|
178
|
+
code,
|
|
179
|
+
market,
|
|
180
|
+
name: readCell(row, keyMap.nameKeys) ?? symbol,
|
|
181
|
+
mx: {
|
|
182
|
+
latestPrice: readCell(row, keyMap.latestPriceKeys),
|
|
183
|
+
changePct: readCell(row, keyMap.changePctKeys),
|
|
184
|
+
pe: readCell(row, keyMap.peKeys),
|
|
185
|
+
pb: readCell(row, keyMap.pbKeys),
|
|
186
|
+
turnoverRate: readCell(row, keyMap.turnoverRateKeys),
|
|
187
|
+
volumeRatio: readCell(row, keyMap.volumeRatioKeys),
|
|
188
|
+
amount: readCell(row, keyMap.amountKeys),
|
|
189
|
+
marketValue: readCell(row, keyMap.marketValueKeys),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
if (candidates.length >= limit) {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return candidates;
|
|
197
|
+
}
|
|
198
|
+
function renderCandidatePool(input) {
|
|
199
|
+
const lines = [
|
|
200
|
+
`🧭 智能选股候选池: ${input.input.keyword}`,
|
|
201
|
+
`TickFlow等级: ${formatTickflowApiKeyLevel(input.tickflowApiKeyLevel)} | 候选展示: ${input.candidates.length}/${input.input.limit} | 硬上限: ${MAX_CANDIDATE_LIMIT}`,
|
|
202
|
+
renderMxSummary(input.mxResult),
|
|
203
|
+
renderCapabilityPolicy(input.input, input.tickflowApiKeyLevel, input.candidates.length),
|
|
204
|
+
];
|
|
205
|
+
const conditionLines = renderConditions(input.mxResult);
|
|
206
|
+
if (conditionLines.length > 0) {
|
|
207
|
+
lines.push("", "条件拆解:", ...conditionLines);
|
|
208
|
+
}
|
|
209
|
+
lines.push("", "候选明细:");
|
|
210
|
+
for (const candidate of input.candidates) {
|
|
211
|
+
const quote = input.quotesBySymbol.get(candidate.symbol) ?? null;
|
|
212
|
+
const dailyRows = input.dailyBySymbol.get(candidate.symbol) ?? [];
|
|
213
|
+
const intradayRows = input.intradayBySymbol.get(candidate.symbol) ?? [];
|
|
214
|
+
const financial = input.financialBySymbol.get(candidate.symbol) ?? null;
|
|
215
|
+
lines.push(`${candidate.rank}. ${candidate.name}(${candidate.symbol})${input.watchlistSymbols.has(candidate.symbol) ? " | 已在本地自选" : ""}`);
|
|
216
|
+
lines.push(` - 妙想: ${renderMxCandidateMetrics(candidate)}`);
|
|
217
|
+
lines.push(` - TickFlow行情: ${renderQuote(quote)}`);
|
|
218
|
+
lines.push(` - 日线: ${renderDailySummary(dailyRows)}`);
|
|
219
|
+
if (input.input.includeIntraday) {
|
|
220
|
+
lines.push(` - 分钟K: ${supportsIntradayKlines(input.tickflowApiKeyLevel) ? renderIntradaySummary(intradayRows) : "当前等级不支持,已跳过"}`);
|
|
221
|
+
}
|
|
222
|
+
if (input.input.includeFinancial) {
|
|
223
|
+
lines.push(` - 财务: ${input.tickflowApiKeyLevel === "expert" ? renderFinancialMetrics(financial) : "当前等级不是 Expert,已跳过 TickFlow 财务"}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
lines.push("", "后续联动建议:");
|
|
227
|
+
lines.push("- 需要加入本地观察时,先确认具体股票,再调用 `add_stock`;本工具不会自动写入自选。");
|
|
228
|
+
lines.push("- 需要同步到东方财富自选时,先加入本地自选,再调用 `push_eastmoney_watchlist`。");
|
|
229
|
+
lines.push("- 需要单股深度分析时,对候选中的 1-2 只再调用 `analyze`,避免一次性拉取过多数据。");
|
|
230
|
+
if (input.notes.length > 0) {
|
|
231
|
+
lines.push("", "补数据提示:", ...input.notes.map((note) => `- ${note}`));
|
|
232
|
+
}
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
function renderMxSummary(result) {
|
|
236
|
+
return `妙想选股: 状态 ${result.status ?? "-"} | 业务码 ${result.code ?? "-"} | 总数 ${result.total} | 接口返回 ${result.dataList.length} | 数据来源 ${result.dataSource}`;
|
|
237
|
+
}
|
|
238
|
+
function renderCapabilityPolicy(input, level, candidateCount) {
|
|
239
|
+
const parts = [
|
|
240
|
+
"补数据策略: TickFlow行情 1次批量请求",
|
|
241
|
+
input.includeDailyKlines ? `日K ${candidateCount}只 x ${input.dailyKlineCount}天` : "日K已关闭",
|
|
242
|
+
input.includeIntraday
|
|
243
|
+
? supportsIntradayKlines(level)
|
|
244
|
+
? `分钟K最多 ${Math.min(candidateCount, MAX_INTRADAY_CANDIDATES)}只 x ${input.intradayCount}根`
|
|
245
|
+
: "分钟K需 Pro/Expert,已跳过"
|
|
246
|
+
: "分钟K默认关闭",
|
|
247
|
+
input.includeFinancial
|
|
248
|
+
? level === "expert"
|
|
249
|
+
? `财务最多 ${Math.min(candidateCount, MAX_FINANCIAL_CANDIDATES)}只`
|
|
250
|
+
: "财务需 Expert,已跳过"
|
|
251
|
+
: "财务默认关闭",
|
|
252
|
+
];
|
|
253
|
+
return parts.join(";");
|
|
254
|
+
}
|
|
255
|
+
function renderConditions(result) {
|
|
256
|
+
const lines = [];
|
|
257
|
+
if (result.totalCondition) {
|
|
258
|
+
lines.push(`- 组合条件: ${result.totalCondition.describe}(${result.totalCondition.stockCount ?? "-"} 只)`);
|
|
259
|
+
}
|
|
260
|
+
for (const condition of result.responseConditionList.slice(0, 5)) {
|
|
261
|
+
lines.push(`- ${condition.describe}(${condition.stockCount ?? "-"} 只)`);
|
|
262
|
+
}
|
|
263
|
+
return lines;
|
|
264
|
+
}
|
|
265
|
+
function renderMxCandidateMetrics(candidate) {
|
|
266
|
+
return [
|
|
267
|
+
candidate.mx.latestPrice ? `最新价 ${candidate.mx.latestPrice}` : null,
|
|
268
|
+
candidate.mx.changePct ? `涨跌幅 ${candidate.mx.changePct}%` : null,
|
|
269
|
+
candidate.mx.pe ? `PE ${candidate.mx.pe}` : null,
|
|
270
|
+
candidate.mx.pb ? `PB ${candidate.mx.pb}` : null,
|
|
271
|
+
candidate.mx.turnoverRate ? `换手 ${candidate.mx.turnoverRate}%` : null,
|
|
272
|
+
candidate.mx.amount ? `成交额 ${candidate.mx.amount}` : null,
|
|
273
|
+
candidate.mx.marketValue ? `总市值 ${candidate.mx.marketValue}` : null,
|
|
274
|
+
].filter(Boolean).join(";") || "无核心字段";
|
|
275
|
+
}
|
|
276
|
+
async function loadWatchlistSymbols(watchlistService, notes) {
|
|
277
|
+
try {
|
|
278
|
+
return new Set((await watchlistService.list()).map((item) => item.symbol));
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
notes.push(`本地自选状态读取失败: ${formatError(error)}`);
|
|
282
|
+
return new Set();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function loadQuotes(quoteService, symbols, notes) {
|
|
286
|
+
try {
|
|
287
|
+
const quotes = await quoteService.fetchQuotes(symbols);
|
|
288
|
+
return new Map(quotes.map((quote) => [quote.symbol, quote]));
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
notes.push(`TickFlow批量行情获取失败: ${formatError(error)}`);
|
|
292
|
+
return new Map();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function loadDailyKlines(klineService, candidates, count, notes) {
|
|
296
|
+
const result = new Map();
|
|
297
|
+
for (const candidate of candidates) {
|
|
298
|
+
try {
|
|
299
|
+
result.set(candidate.symbol, await klineService.fetchKlines(candidate.symbol, {
|
|
300
|
+
count,
|
|
301
|
+
adjust: "forward",
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
notes.push(`${candidate.symbol} 日K获取失败: ${formatError(error)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
async function loadIntradayKlines(klineService, candidates, count, notes) {
|
|
311
|
+
const result = new Map();
|
|
312
|
+
for (const candidate of candidates) {
|
|
313
|
+
try {
|
|
314
|
+
result.set(candidate.symbol, await klineService.fetchIntradayKlines(candidate.symbol, {
|
|
315
|
+
period: "1m",
|
|
316
|
+
count,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
notes.push(`${candidate.symbol} 分钟K获取失败: ${formatError(error)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
async function loadFinancialMetrics(financialService, candidates, notes) {
|
|
326
|
+
const result = new Map();
|
|
327
|
+
for (const candidate of candidates) {
|
|
328
|
+
try {
|
|
329
|
+
const rows = await financialService.fetchMetrics(candidate.symbol, { latest: true });
|
|
330
|
+
if (rows[0]) {
|
|
331
|
+
result.set(candidate.symbol, rows[0]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
notes.push(`${candidate.symbol} 财务指标获取失败: ${formatError(error)}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
function renderQuote(quote) {
|
|
341
|
+
if (!quote) {
|
|
342
|
+
return "未返回";
|
|
343
|
+
}
|
|
344
|
+
const changePct = resolveTickFlowQuoteChangePct(quote);
|
|
345
|
+
return [
|
|
346
|
+
`最新 ${formatNumber(quote.last_price)}`,
|
|
347
|
+
changePct == null ? null : `涨跌幅 ${formatSignedPct(changePct)}`,
|
|
348
|
+
quote.timestamp ? `时间 ${formatChinaDateTime(new Date(quote.timestamp))}` : null,
|
|
349
|
+
].filter(Boolean).join(";");
|
|
350
|
+
}
|
|
351
|
+
function renderDailySummary(rows) {
|
|
352
|
+
if (rows.length === 0) {
|
|
353
|
+
return "未返回";
|
|
354
|
+
}
|
|
355
|
+
const last = rows[rows.length - 1];
|
|
356
|
+
const change5 = calculateWindowChange(rows, 5);
|
|
357
|
+
const change20 = calculateWindowChange(rows, 20);
|
|
358
|
+
return [
|
|
359
|
+
`${rows.length}根`,
|
|
360
|
+
`${last.trade_date} 收盘 ${formatNumber(last.close)}`,
|
|
361
|
+
change5 == null ? null : `5日 ${formatSignedPct(change5)}`,
|
|
362
|
+
change20 == null ? null : `20日 ${formatSignedPct(change20)}`,
|
|
363
|
+
].filter(Boolean).join(";");
|
|
364
|
+
}
|
|
365
|
+
function renderIntradaySummary(rows) {
|
|
366
|
+
if (rows.length === 0) {
|
|
367
|
+
return "未返回";
|
|
368
|
+
}
|
|
369
|
+
const last = rows[rows.length - 1];
|
|
370
|
+
return `${rows.length}根;${last.trade_date} ${last.trade_time} 收盘 ${formatNumber(last.close)}`;
|
|
371
|
+
}
|
|
372
|
+
function renderFinancialMetrics(row) {
|
|
373
|
+
if (!row) {
|
|
374
|
+
return "未返回";
|
|
375
|
+
}
|
|
376
|
+
return [
|
|
377
|
+
`期末 ${row.period_end}`,
|
|
378
|
+
row.roe == null ? null : `ROE ${formatPercentLike(row.roe)}`,
|
|
379
|
+
row.gross_margin == null ? null : `毛利率 ${formatPercentLike(row.gross_margin)}`,
|
|
380
|
+
row.net_income_yoy == null ? null : `净利同比 ${formatPercentLike(row.net_income_yoy)}`,
|
|
381
|
+
row.debt_to_asset_ratio == null ? null : `资产负债率 ${formatPercentLike(row.debt_to_asset_ratio)}`,
|
|
382
|
+
].filter(Boolean).join(";");
|
|
383
|
+
}
|
|
384
|
+
function buildCandidateColumnKeyMap(columns) {
|
|
385
|
+
return {
|
|
386
|
+
codeKeys: ["SECURITY_CODE", ...findColumnKeys(columns, [/代码/, /security.*code/i])],
|
|
387
|
+
nameKeys: ["SECURITY_SHORT_NAME", ...findColumnKeys(columns, [/名称/, /简称/, /security.*name/i])],
|
|
388
|
+
marketKeys: ["MARKET_SHORT_NAME", ...findColumnKeys(columns, [/市场代码简称/, /market/i])],
|
|
389
|
+
latestPriceKeys: ["NEWEST_PRICE", ...findColumnKeys(columns, [/最新价/])],
|
|
390
|
+
changePctKeys: ["CHG", ...findColumnKeys(columns, [/涨跌幅/])],
|
|
391
|
+
peKeys: findColumnKeys(columns, [/市盈率/]),
|
|
392
|
+
pbKeys: findColumnKeys(columns, [/市净率/]),
|
|
393
|
+
turnoverRateKeys: findColumnKeys(columns, [/换手率/]),
|
|
394
|
+
volumeRatioKeys: findColumnKeys(columns, [/量比/]),
|
|
395
|
+
amountKeys: findColumnKeys(columns, [/成交额/]),
|
|
396
|
+
marketValueKeys: findColumnKeys(columns, [/总市值/]),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function findColumnKeys(columns, patterns) {
|
|
400
|
+
return columns
|
|
401
|
+
.filter((column) => {
|
|
402
|
+
const text = `${column.title}\n${column.key}`;
|
|
403
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
404
|
+
})
|
|
405
|
+
.map((column) => column.key);
|
|
406
|
+
}
|
|
407
|
+
function readCell(row, keys) {
|
|
408
|
+
for (const key of keys) {
|
|
409
|
+
const value = row[key];
|
|
410
|
+
if (value != null && String(value).trim()) {
|
|
411
|
+
return String(value).trim();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function normalizeCandidateSymbol(code, market) {
|
|
417
|
+
const digits = code.match(/\d{6}/)?.[0] ?? code.trim();
|
|
418
|
+
const normalizedMarket = String(market ?? "").trim().toUpperCase();
|
|
419
|
+
if (/^\d{6}$/.test(digits) && ["SH", "SZ", "BJ"].includes(normalizedMarket)) {
|
|
420
|
+
return `${digits}.${normalizedMarket}`;
|
|
421
|
+
}
|
|
422
|
+
return normalizeSymbol(digits);
|
|
423
|
+
}
|
|
424
|
+
function calculateWindowChange(rows, windowSize) {
|
|
425
|
+
if (rows.length <= windowSize) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const latest = rows[rows.length - 1]?.close;
|
|
429
|
+
const previous = rows[rows.length - 1 - windowSize]?.close;
|
|
430
|
+
return calculateChangePct(latest, previous);
|
|
431
|
+
}
|
|
432
|
+
function calculateChangePct(current, previous) {
|
|
433
|
+
if (current == null || previous == null || !Number.isFinite(current) || !Number.isFinite(previous) || previous === 0) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
return ((current - previous) / previous) * 100;
|
|
437
|
+
}
|
|
438
|
+
function parsePositiveInteger(value, fallback, max) {
|
|
439
|
+
if (value == null || String(value).trim() === "") {
|
|
440
|
+
return fallback;
|
|
441
|
+
}
|
|
442
|
+
const numeric = Number(value);
|
|
443
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
444
|
+
throw new Error("number must be greater than 0");
|
|
445
|
+
}
|
|
446
|
+
return Math.min(Math.trunc(numeric), max);
|
|
447
|
+
}
|
|
448
|
+
function parseOptionalBoolean(value, fallback) {
|
|
449
|
+
if (typeof value === "boolean") {
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
if (typeof value === "string") {
|
|
453
|
+
const normalized = value.trim().toLowerCase();
|
|
454
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return fallback;
|
|
462
|
+
}
|
|
463
|
+
function formatNumber(value) {
|
|
464
|
+
if (!Number.isFinite(value)) {
|
|
465
|
+
return "-";
|
|
466
|
+
}
|
|
467
|
+
return value.toFixed(2);
|
|
468
|
+
}
|
|
469
|
+
function formatSignedPct(value) {
|
|
470
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(2)}%`;
|
|
471
|
+
}
|
|
472
|
+
function formatPercentLike(value) {
|
|
473
|
+
return `${value.toFixed(2)}%`;
|
|
474
|
+
}
|
|
475
|
+
function formatError(error) {
|
|
476
|
+
return error instanceof Error ? error.message : String(error);
|
|
477
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface MxDataEntityTag {
|
|
2
|
+
fullName: string | null;
|
|
3
|
+
secuCode: string | null;
|
|
4
|
+
marketChar: string | null;
|
|
5
|
+
entityTypeName: string | null;
|
|
6
|
+
className: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface MxDataTable {
|
|
9
|
+
title: string;
|
|
10
|
+
code: string | null;
|
|
11
|
+
entityName: string | null;
|
|
12
|
+
rows: Array<Record<string, string>>;
|
|
13
|
+
fieldnames: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface MxDataResult {
|
|
16
|
+
status: number | null;
|
|
17
|
+
message: string | null;
|
|
18
|
+
questionId: string | null;
|
|
19
|
+
entityTags: MxDataEntityTag[];
|
|
20
|
+
conditionParts: string[];
|
|
21
|
+
tables: MxDataTable[];
|
|
22
|
+
totalRows: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -21,6 +21,7 @@ export interface MxSelectStockResult {
|
|
|
21
21
|
total: number;
|
|
22
22
|
totalRecordCount: number;
|
|
23
23
|
parserText: string | null;
|
|
24
|
+
dataSource: "dataList" | "partialResults" | "none";
|
|
24
25
|
columns: MxSelectStockColumn[];
|
|
25
26
|
dataList: Array<Record<string, unknown>>;
|
|
26
27
|
responseConditionList: MxSelectStockCondition[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface MxSelfSelectColumn {
|
|
2
|
+
title: string;
|
|
3
|
+
key: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MxSelfSelectStock {
|
|
6
|
+
symbol: string;
|
|
7
|
+
rawSymbol: string | null;
|
|
8
|
+
name: string;
|
|
9
|
+
latestPrice: string | null;
|
|
10
|
+
changePercent: string | null;
|
|
11
|
+
changeAmount: string | null;
|
|
12
|
+
turnoverRate: string | null;
|
|
13
|
+
volumeRatio: string | null;
|
|
14
|
+
raw: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface MxSelfSelectResult {
|
|
17
|
+
status: number | null;
|
|
18
|
+
code: string | null;
|
|
19
|
+
message: string | null;
|
|
20
|
+
columns: MxSelfSelectColumn[];
|
|
21
|
+
stocks: MxSelfSelectStock[];
|
|
22
|
+
raw: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface MxSelfSelectManageResult {
|
|
25
|
+
status: number | null;
|
|
26
|
+
code: string | null;
|
|
27
|
+
message: string | null;
|
|
28
|
+
query: string;
|
|
29
|
+
raw: unknown;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|