feat(web): 批量评估页——股票池/因子集/时间窗三步提交进任务中心;CI门禁加tests/factor [nas]
This commit is contained in:
@@ -50,12 +50,13 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ 本次 push commit 标签齐全"
|
echo "✅ 本次 push commit 标签齐全"
|
||||||
- name: pytest 冒烟 (venv310 数据层+组合引擎)
|
- name: pytest 冒烟 (venv310 数据层+组合引擎+因子评估)
|
||||||
run: |
|
run: |
|
||||||
PY=/Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2/venv310/bin/python
|
PY=/Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2/venv310/bin/python
|
||||||
# 2026-08-22 起扩跑 tests/portfolio(issue#35 方案 B):原 13 个 Mac 环境失败
|
# 2026-08-22 起扩跑 tests/portfolio(issue#35 方案 B):原 13 个 Mac 环境失败
|
||||||
# 已被依赖治理顺带清零(实测 440 绿 1.74s);组合引擎 440 题进 CI 门禁
|
# 已被依赖治理顺带清零(实测 440 绿 1.74s);组合引擎 440 题进 CI 门禁
|
||||||
PYTHONPATH="$GITHUB_WORKSPACE" $PY -m pytest tests/data_platform tests/portfolio -q
|
# 2026-08-25 起加 tests/factor(因子评估层纯逻辑小测,本地62绿)
|
||||||
|
PYTHONPATH="$GITHUB_WORKSPACE" $PY -m pytest tests/data_platform tests/portfolio tests/factor -q
|
||||||
|
|
||||||
# ---------- NAS 部署(后端代码 + 前端 dist + restart 容器) ----------
|
# ---------- NAS 部署(后端代码 + 前端 dist + restart 容器) ----------
|
||||||
nas-deploy:
|
nas-deploy:
|
||||||
|
|||||||
@@ -1,18 +1,158 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getFactors, submitBatchEval, type FactorItem } from '@/api/factor'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pool = ref<string>('all')
|
const factors = ref<FactorItem[]>([])
|
||||||
const categories = ref<string[]>(['alpha101', 'alpha158'])
|
const customSymbolsText = ref('')
|
||||||
const startDate = ref('2018-01-01')
|
|
||||||
const endDate = ref('2026-06-30')
|
|
||||||
|
|
||||||
async function submit() {
|
// 01 股票池: 'all' = 全A, 'custom' = 自定义, 'csi800' = 中证800(未就绪)
|
||||||
|
const poolType = ref<string>('all')
|
||||||
|
|
||||||
|
// 02 因子集: 按类目分组勾选
|
||||||
|
const selectedCategories = ref<Set<string>>(new Set(['alpha101', 'alpha158']))
|
||||||
|
|
||||||
|
// 03 时间窗
|
||||||
|
const dateRange = ref<[string, string]>(['2018-01-01', '2026-06-30'])
|
||||||
|
|
||||||
|
// 按 category 分组并计数
|
||||||
|
const categoryGroups = computed(() => {
|
||||||
|
const map = new Map<string, { count: number; items: FactorItem[] }>()
|
||||||
|
for (const f of factors.value) {
|
||||||
|
if (!map.has(f.category)) {
|
||||||
|
map.set(f.category, { count: 0, items: [] })
|
||||||
|
}
|
||||||
|
const group = map.get(f.category)!
|
||||||
|
group.count++
|
||||||
|
group.items.push(f)
|
||||||
|
}
|
||||||
|
return Array.from(map, ([cat, data]) => ({ cat, count: data.count, items: data.items }))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 自动生成 label
|
||||||
|
const autoLabel = computed(() => {
|
||||||
|
const now = new Date()
|
||||||
|
const y = now.getFullYear()
|
||||||
|
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(now.getDate()).padStart(2, '0')
|
||||||
|
return `batch-${y}${m}${d}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 解析自定义股票代码
|
||||||
|
const parsedCustomSymbols = computed(() => {
|
||||||
|
return customSymbolsText.value
|
||||||
|
.split(/[\s,,\n]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 中证800是否置灰(未就绪)
|
||||||
|
const csi800Disabled = true
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
// TODO: Implement batch evaluation submission (Task 12)
|
try {
|
||||||
setTimeout(() => {
|
factors.value = await getFactors()
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('因子列表加载失败')
|
||||||
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}, 1000)
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换股票池类型
|
||||||
|
function selectPool(type: string): void {
|
||||||
|
if (type === 'csi800' && csi800Disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poolType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换类目勾选
|
||||||
|
function toggleCategory(cat: string): void {
|
||||||
|
if (selectedCategories.value.has(cat)) {
|
||||||
|
selectedCategories.value.delete(cat)
|
||||||
|
} else {
|
||||||
|
selectedCategories.value.add(cat)
|
||||||
|
}
|
||||||
|
// 强制响应式更新
|
||||||
|
selectedCategories.value = new Set(selectedCategories.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全选/清空类目
|
||||||
|
function setAllCategories(val: boolean): void {
|
||||||
|
if (val) {
|
||||||
|
// 只全选可用的类目(排除 gtja191 等未就绪的)
|
||||||
|
const available = categoryGroups.value
|
||||||
|
.filter((g) => g.cat !== 'gtja191')
|
||||||
|
.map((g) => g.cat)
|
||||||
|
selectedCategories.value = new Set(available)
|
||||||
|
} else {
|
||||||
|
selectedCategories.value.clear()
|
||||||
|
}
|
||||||
|
// 强制响应式更新
|
||||||
|
selectedCategories.value = new Set(selectedCategories.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交前校验
|
||||||
|
function validate(): boolean {
|
||||||
|
// 校验因子集: 至少选一类
|
||||||
|
if (selectedCategories.value.size === 0) {
|
||||||
|
ElMessage.warning('请至少选择一个因子类目')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验自定义股票池: 若选中自定义,必须填写代码
|
||||||
|
if (poolType.value === 'custom' && parsedCustomSymbols.value.length === 0) {
|
||||||
|
ElMessage.warning('请填写自定义股票代码(逗号或换行分隔)')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验日期
|
||||||
|
if (!dateRange.value[0] || !dateRange.value[1]) {
|
||||||
|
ElMessage.warning('请选择完整的起止日期')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交批量评估
|
||||||
|
async function submit(): Promise<void> {
|
||||||
|
if (!validate()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 构造 symbols: 全A 传空数组,自定义传解析后的列表
|
||||||
|
const symbols = poolType.value === 'all' ? [] : parsedCustomSymbols.value
|
||||||
|
|
||||||
|
// categories: 传选中的类目列表
|
||||||
|
const categories = Array.from(selectedCategories.value)
|
||||||
|
|
||||||
|
// factors: 空(让后端按 categories 全量)
|
||||||
|
const req = {
|
||||||
|
categories,
|
||||||
|
factors: [],
|
||||||
|
symbols,
|
||||||
|
start: dateRange.value[0],
|
||||||
|
end: dateRange.value[1],
|
||||||
|
label: autoLabel.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = await submitBatchEval(req)
|
||||||
|
ElMessage.success(`批量评估任务已提交(task_id: ${taskId.slice(0, 8)}...),完成后将自动生成排行榜批次`)
|
||||||
|
|
||||||
|
// 跳任务中心
|
||||||
|
router.push('/backtest/history')
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const msg = error instanceof Error ? error.message : '提交失败'
|
||||||
|
ElMessage.error(`提交失败: ${msg}`)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -29,39 +169,76 @@ async function submit() {
|
|||||||
<h3><span class="step">01</span>股票池</h3>
|
<h3><span class="step">01</span>股票池</h3>
|
||||||
<div class="cs">与实盘可交易域一致 · 退市股保留(无幸存者偏差)</div>
|
<div class="cs">与实盘可交易域一致 · 退市股保留(无幸存者偏差)</div>
|
||||||
<div class="poolgrid">
|
<div class="poolgrid">
|
||||||
<button class="pool on">
|
<button
|
||||||
|
class="pool"
|
||||||
|
:class="{ on: poolType === 'all' }"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="selectPool('all')"
|
||||||
|
>
|
||||||
<span class="pt"><span class="radio"></span>全A 主板+创业板</span>
|
<span class="pt"><span class="radio"></span>全A 主板+创业板</span>
|
||||||
<span class="pd">剔除科创板/北交所 · 剔 ST · 剔上市<120日</span>
|
<span class="pd">剔除科创板/北交所 · 剔 ST · 剔上市<120日</span>
|
||||||
<span class="pc">≈ 4,382 只</span>
|
<span class="pc">≈ 4,382 只</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="pool">
|
<button
|
||||||
|
class="pool"
|
||||||
|
:class="{ on: poolType === 'csi800' }"
|
||||||
|
:disabled="csi800Disabled || loading"
|
||||||
|
@click="selectPool('csi800')"
|
||||||
|
style="opacity: 0.5; cursor: not-allowed"
|
||||||
|
>
|
||||||
<span class="pt"><span class="radio"></span>中证 800</span>
|
<span class="pt"><span class="radio"></span>中证 800</span>
|
||||||
<span class="pd">成份股并集 · 按评估日动态取 · 快速迭代用</span>
|
<span class="pd">成份股并集 · 按评估日动态取 · 快速迭代用</span>
|
||||||
<span class="pc">≈ 800 只</span>
|
<span class="pc">≈ 800 只 · 未就绪</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="pool">
|
<button
|
||||||
|
class="pool"
|
||||||
|
:class="{ on: poolType === 'custom' }"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="selectPool('custom')"
|
||||||
|
>
|
||||||
<span class="pt"><span class="radio"></span>自定义列表</span>
|
<span class="pt"><span class="radio"></span>自定义列表</span>
|
||||||
<span class="pd">粘贴代码清单 · 沿用现有单因子分析入口</span>
|
<span class="pd">粘贴代码清单 · 沿用现有单因子分析入口</span>
|
||||||
<span class="pc">任意</span>
|
<span class="pc">{{ parsedCustomSymbols.length || '任意' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 自定义股票池输入框 -->
|
||||||
|
<div v-if="poolType === 'custom'" style="margin-top: 16px">
|
||||||
|
<textarea
|
||||||
|
v-model="customSymbolsText"
|
||||||
|
class="custom-input"
|
||||||
|
placeholder="输入股票代码,用逗号或换行分隔 例如: 600000, 000001, 300750"
|
||||||
|
:disabled="loading"
|
||||||
|
rows="4"
|
||||||
|
/>
|
||||||
|
<div class="cs" style="margin-top: 4px">
|
||||||
|
已识别 {{ parsedCustomSymbols.length }} 只股票
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="formcard">
|
<div class="formcard">
|
||||||
<h3><span class="step">02</span>因子集</h3>
|
<h3><span class="step">02</span>因子集</h3>
|
||||||
<div class="cs">按类批量勾选 · 未就绪的因子集显示占位</div>
|
<div class="cs">按类批量勾选 · 未就绪的因子集显示占位</div>
|
||||||
<div class="checkrow">
|
<div class="checkrow">
|
||||||
<button class="checkchip on">
|
<!-- 动态渲染因子类目 -->
|
||||||
<span class="box"></span>alpha101 <span class="cnt">×100</span>
|
<button
|
||||||
|
v-for="group in categoryGroups"
|
||||||
|
:key="group.cat"
|
||||||
|
class="checkchip"
|
||||||
|
:class="{ on: selectedCategories.has(group.cat) }"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="toggleCategory(group.cat)"
|
||||||
|
>
|
||||||
|
<span class="box"></span>{{ group.cat }} <span class="cnt">×{{ group.count }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="checkchip on">
|
</div>
|
||||||
<span class="box"></span>alpha158 <span class="cnt">×158</span>
|
<!-- 全选/清空按钮 -->
|
||||||
|
<div style="margin-top: 12px; display: flex; gap: 8px">
|
||||||
|
<button class="btn-sm" :disabled="loading" @click="setAllCategories(true)">
|
||||||
|
全选
|
||||||
</button>
|
</button>
|
||||||
<button class="checkchip">
|
<button class="btn-sm" :disabled="loading" @click="setAllCategories(false)">
|
||||||
<span class="box"></span>内置基础 <span class="cnt">×7</span>
|
清空
|
||||||
</button>
|
|
||||||
<button class="checkchip" style="opacity: 0.45">
|
|
||||||
<span class="box"></span>gtja191 <span class="cnt">×191 · 待移植</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,20 +247,25 @@ async function submit() {
|
|||||||
<h3><span class="step">03</span>时间窗与口径</h3>
|
<h3><span class="step">03</span>时间窗与口径</h3>
|
||||||
<div class="cs">日频因子值 · RankIC · 前瞻收益 1/5/10 日</div>
|
<div class="cs">日频因子值 · RankIC · 前瞻收益 1/5/10 日</div>
|
||||||
<div class="daterow">
|
<div class="daterow">
|
||||||
<input v-model="startDate" type="date" class="datebox" />
|
<input v-model="dateRange[0]" type="date" class="datebox" :disabled="loading" />
|
||||||
<span style="color: var(--text-3)">→</span>
|
<span style="color: var(--text-3)">→</span>
|
||||||
<input v-model="endDate" type="date" class="datebox" />
|
<input v-model="dateRange[1]" type="date" class="datebox" :disabled="loading" />
|
||||||
<span style="color: var(--text-3); font-family: var(--mono); font-size: 11px">
|
<span style="color: var(--text-3); font-family: var(--mono); font-size: 11px">
|
||||||
全量预估 ~2.5h (NAS) · 建议晚间错峰
|
全量约 4400 只 × 8.5 年,预计 1–3 小时,完成后结果进 IC 排行榜
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Label 预览 -->
|
||||||
|
<div class="formcard" style="padding: 12px 16px; background: var(--panel)">
|
||||||
|
<div class="cs">自动生成批次标签: {{ autoLabel }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="submitbar">
|
<div class="submitbar">
|
||||||
<button class="btn primary" :disabled="loading" @click="submit">
|
<button class="btn primary" :disabled="loading" @click="submit">
|
||||||
{{ loading ? '提交中…' : '提交评估任务' }}
|
{{ loading ? '提交中…' : '提交评估任务' }}
|
||||||
</button>
|
</button>
|
||||||
<span class="note">提交后进入任务中心排队 · 完成自动生成「2026-08-25 批次」排行榜</span>
|
<span class="note">提交后进入任务中心排队 · 完成自动生成「{{ autoLabel }}」排行榜</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -335,4 +517,38 @@ async function submit() {
|
|||||||
.btn.primary:hover:not(:disabled) {
|
.btn.primary:hover:not(:disabled) {
|
||||||
background: #33ecff;
|
background: #33ecff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-input {
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-2);
|
||||||
|
background: var(--bg-card);
|
||||||
|
transition: all 0.15s var(--ease);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:hover:not(:disabled) {
|
||||||
|
border-color: var(--cyan-dim);
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function renderMonthlyChart() {
|
|||||||
name: '月度 IC',
|
name: '月度 IC',
|
||||||
data: icValues,
|
data: icValues,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: (params) => (params.data as number) >= 0 ? 'var(--up)' : 'var(--down)',
|
color: (params: any) => (params.data as number) >= 0 ? 'var(--up)' : 'var(--down)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user