- 首页双入口(智能选股/策略回测):引入 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>
76 lines
3.2 KiB
Vue
76 lines
3.2 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from 'vue';
|
||
import DataTable from 'primevue/datatable';
|
||
import Column from 'primevue/column';
|
||
import type { ScreenerItemOut, ScreenerRunResponse } from '@/api/types';
|
||
|
||
const props = defineProps<{ result: ScreenerRunResponse }>();
|
||
|
||
const items = computed(() => props.result.items);
|
||
// 动态指标列:[{ key: 'kdj_j', label: 'KDJ J(9,3,3)' }]
|
||
const indCols = computed(() =>
|
||
Object.entries(props.result.indicator_labels).map(([key, label]) => ({ key, label })),
|
||
);
|
||
|
||
function pctClass(v: number | null) {
|
||
if (v == null) return '';
|
||
return v > 0 ? 'pos' : v < 0 ? 'neg' : '';
|
||
}
|
||
|
||
function fmt(v: number | null, digits = 2) {
|
||
return v == null ? '—' : v.toFixed(digits);
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="panel">
|
||
<div class="panel-title">
|
||
命中 {{ result.total }} 只{{ result.total > items.length ? `(仅显示前 ${items.length})` : '' }}
|
||
<span v-if="result.trade_date" style="color: var(--ink-3); margin-left: 8px;">· 数据基准 {{ result.trade_date.slice(0, 10) }}</span>
|
||
</div>
|
||
<DataTable :value="items" size="small" striped-rows :scrollable="true" scroll-height="560px" sort-mode="single">
|
||
<Column field="ts_code" header="代码" sortable style="min-width: 92px;">
|
||
<template #body="{ data }">
|
||
<span style="font-variant-numeric: tabular-nums; color: var(--ink);">{{ data.ts_code }}</span>
|
||
</template>
|
||
</Column>
|
||
<Column field="name" header="名称" sortable style="min-width: 96px;" />
|
||
<Column field="close" header="最新价" sortable style="min-width: 84px;">
|
||
<template #body="{ data }">{{ fmt(data.close) }}</template>
|
||
</Column>
|
||
<Column field="pct_chg" header="涨跌幅%" sortable style="min-width: 88px;">
|
||
<template #body="{ data }">
|
||
<span :class="pctClass(data.pct_chg)">{{ data.pct_chg == null ? '—' : (data.pct_chg > 0 ? '+' : '') + data.pct_chg.toFixed(2) }}</span>
|
||
</template>
|
||
</Column>
|
||
<Column field="total_mv" header="总市值(亿)" sortable style="min-width: 100px;">
|
||
<template #body="{ data }">{{ fmt(data.total_mv) }}</template>
|
||
</Column>
|
||
<Column field="circ_mv" header="流通市值(亿)" sortable style="min-width: 106px;">
|
||
<template #body="{ data }">{{ fmt(data.circ_mv) }}</template>
|
||
</Column>
|
||
<Column field="pe_ttm" header="PE-TTM" sortable style="min-width: 84px;">
|
||
<template #body="{ data }">{{ fmt(data.pe_ttm) }}</template>
|
||
</Column>
|
||
<Column field="pb" header="PB" sortable style="min-width: 70px;">
|
||
<template #body="{ data }">{{ fmt(data.pb) }}</template>
|
||
</Column>
|
||
<Column field="turnover_rate" header="换手率%" sortable style="min-width: 88px;">
|
||
<template #body="{ data }">{{ fmt(data.turnover_rate) }}</template>
|
||
</Column>
|
||
<Column
|
||
v-for="col in indCols"
|
||
:key="col.key"
|
||
:field="`indicators.${col.key}`"
|
||
:header="col.label"
|
||
sortable
|
||
style="min-width: 110px;"
|
||
>
|
||
<template #body="{ data }">
|
||
<span style="font-variant-numeric: tabular-nums;">{{ data.indicators?.[col.key] == null ? '—' : data.indicators[col.key].toFixed(2) }}</span>
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
</div>
|
||
</template>
|