feat: AI 自然语言选股(GLM)+ 全市场数据管道 + 远程 PostgreSQL

- 首页双入口(智能选股/策略回测):引入 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>
This commit is contained in:
2026-08-14 14:49:53 +08:00
parent e0b5228008
commit 528357c3f5
29 changed files with 1765 additions and 12 deletions

View File

@@ -0,0 +1,70 @@
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 };
});