tickflow-assist 0.3.7 → 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.
- package/README.md +6 -5
- 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/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 +7 -7
- package/package.json +6 -6
- 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 {};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "tickflow-assist",
|
|
3
3
|
"name": "TickFlow Assist",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.8",
|
|
5
5
|
"description": "A-share watchlist analysis, monitoring, and alert delivery powered by TickFlow and OpenClaw.",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills"
|
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
"provider": "mx-search",
|
|
62
62
|
"method": "api-key",
|
|
63
63
|
"choiceId": "mx-search-api-key",
|
|
64
|
-
"choiceLabel": "MX
|
|
64
|
+
"choiceLabel": "MX Skills API key",
|
|
65
65
|
"groupId": "tickflow-assist",
|
|
66
66
|
"groupLabel": "TickFlow Assist",
|
|
67
67
|
"groupHint": "TickFlow, analysis LLM, and optional data-service credentials.",
|
|
68
68
|
"optionKey": "mxSearchApiKey",
|
|
69
69
|
"cliFlag": "--mx-search-api-key",
|
|
70
70
|
"cliOption": "--mx-search-api-key <key>",
|
|
71
|
-
"cliDescription": "MX
|
|
71
|
+
"cliDescription": "MX Skills API key",
|
|
72
72
|
"assistantVisibility": "manual-only"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
@@ -163,12 +163,12 @@
|
|
|
163
163
|
"mxSearchApiUrl": {
|
|
164
164
|
"type": "string",
|
|
165
165
|
"default": "https://mkapi2.dfcfs.com/finskillshub/api/claw",
|
|
166
|
-
"description": "MX
|
|
166
|
+
"description": "MX Skills API base URL. Leave empty to use TICKFLOW_ASSIST_MX_SEARCH_API_URL or MX_SEARCH_API_URL."
|
|
167
167
|
},
|
|
168
168
|
"mxSearchApiKey": {
|
|
169
169
|
"type": "string",
|
|
170
170
|
"default": "",
|
|
171
|
-
"description": "Configure this during setup if you want mx_search, mx_select_stock, and non-Expert financial fallback. Leave empty to use TICKFLOW_ASSIST_MX_SEARCH_API_KEY, MX_SEARCH_API_KEY, or MX_APIKEY."
|
|
171
|
+
"description": "Configure this during setup if you want mx_search, mx_data, mx_select_stock, Eastmoney self-select sync, and non-Expert financial fallback. Leave empty to use TICKFLOW_ASSIST_MX_SEARCH_API_KEY, MX_SEARCH_API_KEY, or MX_APIKEY."
|
|
172
172
|
},
|
|
173
173
|
"jin10McpUrl": {
|
|
174
174
|
"type": "string",
|
|
@@ -282,8 +282,8 @@
|
|
|
282
282
|
"help": "Set this to your actual TickFlow subscription level, or provide it via TICKFLOW_ASSIST_TICKFLOW_API_KEY_LEVEL / TICKFLOW_API_KEY_LEVEL."
|
|
283
283
|
},
|
|
284
284
|
"mxSearchApiKey": {
|
|
285
|
-
"label": "MX
|
|
286
|
-
"help": "Configure this during setup if you want mx_search, mx_select_stock, and lite financial fallback, or use TICKFLOW_ASSIST_MX_SEARCH_API_KEY / MX_SEARCH_API_KEY / MX_APIKEY.",
|
|
285
|
+
"label": "MX Skills API Key",
|
|
286
|
+
"help": "Configure this during setup if you want mx_search, mx_data, mx_select_stock, Eastmoney self-select sync, and lite financial fallback, or use TICKFLOW_ASSIST_MX_SEARCH_API_KEY / MX_SEARCH_API_KEY / MX_APIKEY.",
|
|
287
287
|
"sensitive": true
|
|
288
288
|
},
|
|
289
289
|
"jin10McpUrl": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickflow-assist",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "面向 A 股投资与盯盘场景的 OpenClaw 智能股票插件,基于 TickFlow API 提供实时监控、收盘后复盘、多维综合分析、关键价位跟踪与告警能力。OpenClaw smart stock plugin for A-share investing and watchlist workflows, powered by TickFlow API for realtime monitoring, post-close review, multi-dimensional analysis, key level tracking, and alerts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dev": "tsc -p tsconfig.json --watch",
|
|
38
38
|
"prepack": "npm run build && node ./scripts/prepare-package-assets.mjs",
|
|
39
39
|
"postpack": "node ./scripts/restore-package-assets.mjs",
|
|
40
|
-
"test": "npm run build && node --test dist/plugin-registration.test.js dist/tools/test-alert.tool.test.js dist/services/jin10-mcp-service.test.js dist/services/jin10-flash-monitor-service.test.js dist/services/pre-market-brief-service.test.js dist/services/monitor-service.test.js",
|
|
40
|
+
"test": "npm run build && node --test dist/plugin-registration.test.js dist/tools/test-alert.tool.test.js dist/tools/eastmoney-watchlist.tool.test.js dist/tools/screen-stock-candidates.tool.test.js dist/services/mx-search-service.test.js dist/services/jin10-mcp-service.test.js dist/services/jin10-flash-monitor-service.test.js dist/services/pre-market-brief-service.test.js dist/services/monitor-service.test.js",
|
|
41
41
|
"community-setup": "node dist/dev/tickflow-assist-cli.js configure-openclaw",
|
|
42
42
|
"tool": "node dist/dev/run-tool.js",
|
|
43
43
|
"monitor-loop": "node dist/dev/run-monitor-loop.js",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"openclaw": {
|
|
48
48
|
"build": {
|
|
49
|
-
"openclawVersion": "2026.4.
|
|
49
|
+
"openclawVersion": "2026.4.22"
|
|
50
50
|
},
|
|
51
51
|
"compat": {
|
|
52
52
|
"pluginApi": ">=2026.3.31",
|
|
53
53
|
"minGatewayVersion": "2026.3.31",
|
|
54
|
-
"builtWithOpenClawVersion": "2026.4.
|
|
54
|
+
"builtWithOpenClawVersion": "2026.4.22"
|
|
55
55
|
},
|
|
56
56
|
"install": {
|
|
57
57
|
"npmSpec": "tickflow-assist",
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/node": "^22.13.11",
|
|
77
|
-
"openclaw": "^2026.4.
|
|
77
|
+
"openclaw": "^2026.4.22",
|
|
78
78
|
"typescript": "^5.8.2"
|
|
79
79
|
},
|
|
80
|
-
"readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。\n\n最近更新:`v0.3.
|
|
80
|
+
"readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。\n\n最近更新:`v0.3.8` 接入东方财富自选同步、妙想官方数据/智能选股兼容与小规模候选池联动,新增 `/ta_screenstocks` / `/ta_screenstocks_llm`,并优化盘前简报提炼和监控阶段提醒去重。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。\n\n当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.22` 上兼容。\n\n## 安装前准备\n\n在执行社区安装前,建议先确认你已经准备好以下配置:\n\n- 核心必需:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`\n- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`\n- 可选增强:`mxSearchApiKey`、`jin10ApiToken`\n\n其中,`configure-openclaw` 会把上述配置写入 `~/.openclaw/openclaw.json` 的 `plugins.entries[\"tickflow-assist\"].config`,插件启用后会在本地 `databasePath` 下持久化 LanceDB 数据,并运行监控 / 日更等后台服务。\n如果你不想把密钥写进配置文件,运行时也支持环境变量回退,优先级是 `openclaw.json / local.config.json` > 环境变量 > 默认值。\n常用环境变量:`TICKFLOW_ASSIST_TICKFLOW_API_KEY` / `TICKFLOW_API_KEY`、`TICKFLOW_ASSIST_LLM_API_KEY` / `LLM_API_KEY`、`TICKFLOW_ASSIST_LLM_BASE_URL` / `LLM_BASE_URL`、`TICKFLOW_ASSIST_LLM_MODEL` / `LLM_MODEL`、`TICKFLOW_ASSIST_MX_SEARCH_API_KEY` / `MX_SEARCH_API_KEY` / `MX_APIKEY`、`TICKFLOW_ASSIST_JIN10_API_TOKEN` / `JIN10_API_TOKEN`。\n如果你希望尽量避免把密钥落盘,推荐先把这些变量写进 `~/.openclaw/.env`,再运行配置向导补齐非密钥项。\n\n## 安装\n\n社区安装:\n\n```bash\nopenclaw plugins install tickflow-assist\nnode ~/.openclaw/extensions/tickflow-assist/dist/dev/tickflow-assist-cli.js configure-openclaw\ncd ~/.openclaw/extensions/tickflow-assist/python && uv sync\nopenclaw plugins enable tickflow-assist\nopenclaw config validate\nopenclaw gateway restart\n```\n\n- `configure-openclaw` 会把配置写入 `~/.openclaw/openclaw.json` 的 `plugins.entries[\"tickflow-assist\"].config`。\n- 核心必填建议先准备:`tickflowApiKey`、`tickflowApiKeyLevel`、`llmApiKey`、`llmBaseUrl`、`llmModel`;告警场景再补 `alertChannel`、`alertTarget`、`alertAccount`。\n- 如果你不想把密钥落盘,优先把环境变量写进 `~/.openclaw/.env`,再运行配置向导补齐非密钥项;如需 PNG 告警卡正常显示中文,请自行安装 `fontconfig` 与 Noto CJK 字体。\n\n社区安装后的升级方式:\n\n```bash\nopenclaw plugins update tickflow-assist\nopenclaw gateway restart\n```\n\n## 配置\n\n插件正式运行读取:\n\n```text\n~/.openclaw/openclaw.json\n```\n\n配置路径:\n\n```text\nplugins.entries[\"tickflow-assist\"].config\n```\n\n建议按完整功能显式填写以下字段,不要只填 API Key:\n\n- 核心运行:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`\n- 本地数据:`databasePath`、`calendarFile`\n- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`\n- 能力补充:`mxSearchApiKey`、`jin10ApiToken`\n\n其中,`mxSearchApiKey` 用于 `mx_search`、`mx_data`、`mx_select_stock`、东方财富自选同步以及非 `Expert` 财务链路的 lite 补充;东方财富自选管理接口每日额度 200 次;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。\n如果你使用环境变量,运行时支持以下回退:\n\n- `tickflowApiUrl`:`TICKFLOW_ASSIST_TICKFLOW_API_URL` / `TICKFLOW_API_URL`\n- `tickflowApiKey`:`TICKFLOW_ASSIST_TICKFLOW_API_KEY` / `TICKFLOW_API_KEY`\n- `tickflowApiKeyLevel`:`TICKFLOW_ASSIST_TICKFLOW_API_KEY_LEVEL` / `TICKFLOW_API_KEY_LEVEL`\n- `llmBaseUrl`:`TICKFLOW_ASSIST_LLM_BASE_URL` / `LLM_BASE_URL`\n- `llmApiKey`:`TICKFLOW_ASSIST_LLM_API_KEY` / `LLM_API_KEY`\n- `llmModel`:`TICKFLOW_ASSIST_LLM_MODEL` / `LLM_MODEL`\n- `mxSearchApiUrl`:`TICKFLOW_ASSIST_MX_SEARCH_API_URL` / `MX_SEARCH_API_URL`\n- `mxSearchApiKey`:`TICKFLOW_ASSIST_MX_SEARCH_API_KEY` / `MX_SEARCH_API_KEY` / `MX_APIKEY`\n- `jin10McpUrl`:`TICKFLOW_ASSIST_JIN10_MCP_URL` / `JIN10_MCP_URL`\n- `jin10ApiToken`:`TICKFLOW_ASSIST_JIN10_API_TOKEN` / `JIN10_API_TOKEN`\n\n## 功能\n\n- 自选股管理、东方财富自选同步、日 K / 分钟 K 抓取与指标计算\n- 妙想资讯搜索、官方金融数据查询、智能选股,以及限量候选池 + TickFlow 补数据联动\n- 技术面、财务面、资讯面的综合分析\n- 实时监控、定时日更、收盘后复盘\n- 金十数据 24 小时快讯监控与自选关联提醒\n- 本地 LanceDB 数据留痕与分析结果查看\n\n## 运行说明\n\n- 插件会在本地 `databasePath` 下持久化 LanceDB 数据。\n- 后台服务会按配置执行定时日更、实时监控与金十数据快讯监控。\n- Python 子模块仅用于技术指标计算,不承担主业务流程。\n\n## 依赖与可选能力\n\n- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):`Free` 可用日线与实时行情;`Starter` 起可用标的池,插件会用来做申万行业映射与申万 3 级同业表现;`Pro` 起可用分钟K;`Expert` 才走 TickFlow 财务数据,非 `Expert` 默认回退妙想 lite。\n- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。独立的金十数据 Skill 详见 [OpenClaw Skill](https://clawhub.ai/robinspt/jin10) / [Hermes Skill](https://github.com/robinspt/hermes-skills)。\n- [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_data`、`mx_select_stock`、东方财富自选同步与非 `Expert` 财务链路的 lite 补充。\n\n## 仓库\n\n- GitHub: [robinspt/tickflow-assist](https://github.com/robinspt/tickflow-assist)\n",
|
|
81
81
|
"readmeFilename": "README.md"
|
|
82
82
|
}
|