- 首页双入口(智能选股/策略回测):引入 vue-router,顶部导航 - 智能选股:自然语言 -> LLM 解析结构化条件(智谱 GLM,OpenAI 兼容,/v4 兼容)-> SQL 快照预筛 + pandas 指标过滤(复用 indicators 单一事实源) - 条件模型:指标 vs 常数/指标(value_indicator,如 DIF>DEA、close<布林下轨)、lookback+match 表达连续N天/近N天任一天、市值/PE/PB/换手率快照条件、默认排除 ST/退市/北交所 - 全市场数据同步:按 trade_date 批量拉取未复权日线(与回测 candles qfq 隔离),交易日历/股票列表本地缓存,daily_basic 仅最新截面,Tushare 限频兜底(分钟级重试/小时级降级) - 存储:DATABASE_URL 切远程 PostgreSQL(cirry.cn/stock),本地 SQLite 已移除 - .env 入库(私有仓库);smoke_test 扩展选股链路 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import { ref } from 'vue';
|
|
import { getScreenerSyncStatus, runScreener, startScreenerSync } from '@/api/client';
|
|
import type { ScreenerRunResponse, ScreenerSyncStatus } from '@/api/types';
|
|
|
|
export const useScreenerStore = defineStore('screener', () => {
|
|
const loading = ref(false);
|
|
const stage = ref<'idle' | 'parsing' | 'screening'>('idle');
|
|
const note = ref<string | null>(null);
|
|
const error = ref<string | null>(null);
|
|
const result = ref<ScreenerRunResponse | null>(null);
|
|
const syncStatus = ref<ScreenerSyncStatus | null>(null);
|
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
async function run(text: string) {
|
|
loading.value = true;
|
|
error.value = null;
|
|
result.value = null;
|
|
note.value = 'AI 解析条件中…';
|
|
stage.value = 'parsing';
|
|
try {
|
|
// 条件解析与全市场筛选在后端一气呵成;切到筛选阶段给个过渡提示
|
|
setTimeout(() => {
|
|
if (loading.value && stage.value === 'parsing') {
|
|
stage.value = 'screening';
|
|
note.value = '全市场筛选中…';
|
|
}
|
|
}, 1200);
|
|
result.value = await runScreener({ text });
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '选股失败';
|
|
} finally {
|
|
loading.value = false;
|
|
stage.value = 'idle';
|
|
note.value = null;
|
|
}
|
|
}
|
|
|
|
async function fetchStatus() {
|
|
try {
|
|
syncStatus.value = await getScreenerSyncStatus();
|
|
} catch {
|
|
/* 静默:状态拉取失败不阻塞页面 */
|
|
}
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (pollTimer) {
|
|
clearInterval(pollTimer);
|
|
pollTimer = null;
|
|
}
|
|
}
|
|
|
|
async function startSync(days = 90) {
|
|
error.value = null;
|
|
try {
|
|
await startScreenerSync({ days });
|
|
await fetchStatus();
|
|
stopPolling();
|
|
pollTimer = setInterval(async () => {
|
|
await fetchStatus();
|
|
if (syncStatus.value && !syncStatus.value.running) stopPolling();
|
|
}, 2000);
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '启动同步失败';
|
|
}
|
|
}
|
|
|
|
return { loading, stage, note, error, result, syncStatus, run, fetchStatus, startSync, stopPolling };
|
|
});
|