|
|
|
|
@@ -1,47 +1,79 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
|
|
|
import { nextTick, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { listContracts } from '@/api/scores'
|
|
|
|
|
import { runPipeline, type RunResponse } from '@/api/run'
|
|
|
|
|
import {
|
|
|
|
|
runPipeline,
|
|
|
|
|
getActiveContract,
|
|
|
|
|
type ActiveContract,
|
|
|
|
|
type RunResponse,
|
|
|
|
|
} from '@/api/run'
|
|
|
|
|
import { parseTsCode } from '@/utils/contract'
|
|
|
|
|
|
|
|
|
|
const SYMBOLS = ['FG', 'SA', 'RB', 'MA', 'CF', 'M']
|
|
|
|
|
|
|
|
|
|
const form = reactive<{
|
|
|
|
|
ts_code: string
|
|
|
|
|
symbol: string
|
|
|
|
|
trade_date: string
|
|
|
|
|
}>({
|
|
|
|
|
ts_code: '',
|
|
|
|
|
symbol: 'FG',
|
|
|
|
|
trade_date: '',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const contracts = ref<string[]>([])
|
|
|
|
|
const active = ref<ActiveContract | null>(null)
|
|
|
|
|
const activeLoading = ref(false)
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const result = ref<RunResponse | null>(null)
|
|
|
|
|
const resultRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
|
|
async function loadActive() {
|
|
|
|
|
activeLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
active.value = await getActiveContract(form.symbol)
|
|
|
|
|
// 切换品种后,如果原日期落在新合约的可选范围之外,清空它
|
|
|
|
|
if (form.trade_date && !isDateAllowed(toDate(form.trade_date))) {
|
|
|
|
|
form.trade_date = ''
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
active.value = null
|
|
|
|
|
ElMessage.error(err?.response?.data?.error || '加载主力合约失败')
|
|
|
|
|
} finally {
|
|
|
|
|
activeLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toDate(s: string) {
|
|
|
|
|
// s 形如 'YYYY-MM-DD'
|
|
|
|
|
const [y, m, d] = s.split('-').map(Number)
|
|
|
|
|
return new Date(y, m - 1, d)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isDateAllowed(d: Date): boolean {
|
|
|
|
|
if (!active.value) return true
|
|
|
|
|
const min = toDate(active.value.min_date).getTime()
|
|
|
|
|
const max = toDate(active.value.max_date).getTime()
|
|
|
|
|
const t = d.getTime()
|
|
|
|
|
return t >= min && t <= max
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function disabledDate(d: Date) {
|
|
|
|
|
return !isDateAllowed(d)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submit() {
|
|
|
|
|
if (!form.ts_code && !form.symbol) {
|
|
|
|
|
ElMessage.warning('请选择合约或填写品种代号')
|
|
|
|
|
if (!form.symbol) {
|
|
|
|
|
ElMessage.warning('请选择品种')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
result.value = null
|
|
|
|
|
try {
|
|
|
|
|
const req: { ts_code?: string; symbol?: string; trade_date?: string } = {}
|
|
|
|
|
if (form.ts_code) {
|
|
|
|
|
req.ts_code = form.ts_code
|
|
|
|
|
} else {
|
|
|
|
|
req.symbol = form.symbol
|
|
|
|
|
}
|
|
|
|
|
if (form.trade_date) {
|
|
|
|
|
req.trade_date = form.trade_date.replace(/-/g, '')
|
|
|
|
|
}
|
|
|
|
|
const req: { symbol: string; trade_date?: string } = { symbol: form.symbol }
|
|
|
|
|
if (form.trade_date) req.trade_date = form.trade_date.replace(/-/g, '')
|
|
|
|
|
const resp = await runPipeline(req)
|
|
|
|
|
result.value = resp
|
|
|
|
|
ElMessage.success('打分完成')
|
|
|
|
|
await nextTick()
|
|
|
|
|
resultRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
const msg = err?.response?.data?.error || err.message || '请求失败'
|
|
|
|
|
ElMessage.error(msg)
|
|
|
|
|
@@ -58,77 +90,73 @@ function signalTagType(s: string) {
|
|
|
|
|
return 'info'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
contracts.value = await listContracts().catch(() => [])
|
|
|
|
|
})
|
|
|
|
|
watch(() => form.symbol, loadActive)
|
|
|
|
|
onMounted(loadActive)
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="page">
|
|
|
|
|
<el-card shadow="never" title="手动打分">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>手动打分</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-form :model="form" label-width="100px" style="max-width: 480px">
|
|
|
|
|
<el-form-item label="合约">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="form.ts_code"
|
|
|
|
|
placeholder="选择已有合约(优先)"
|
|
|
|
|
clearable
|
|
|
|
|
filterable
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="c in contracts"
|
|
|
|
|
:key="c"
|
|
|
|
|
:label="c"
|
|
|
|
|
:value="c"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="品种">
|
|
|
|
|
<el-select v-model="form.symbol" style="width: 100%">
|
|
|
|
|
<el-select v-model="form.symbol" :loading="activeLoading" style="width: 100%">
|
|
|
|
|
<el-option v-for="s in SYMBOLS" :key="s" :label="s" :value="s" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="主力合约">
|
|
|
|
|
<span v-if="active">
|
|
|
|
|
{{ parseTsCode(active.ts_code).contract }}
|
|
|
|
|
<el-text type="info" size="small" style="margin-left: 8px">
|
|
|
|
|
({{ active.ts_code }})
|
|
|
|
|
</el-text>
|
|
|
|
|
</span>
|
|
|
|
|
<el-text v-else type="info">加载中…</el-text>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="打分日期">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="form.trade_date"
|
|
|
|
|
type="date"
|
|
|
|
|
placeholder="留空则对最新日期打分"
|
|
|
|
|
:placeholder="active ? `${active.min_date} ~ ${active.max_date},留空则取最新` : '加载中…'"
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
:disabled-date="disabledDate"
|
|
|
|
|
:disabled="!active"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" :loading="loading" @click="submit">
|
|
|
|
|
<el-button type="primary" :loading="loading" :disabled="!active" @click="submit">
|
|
|
|
|
执行打分
|
|
|
|
|
</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card v-if="result" shadow="never" class="result-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>打分结果</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-descriptions :column="2" border>
|
|
|
|
|
<el-descriptions-item label="品种">{{ parseTsCode(result.ts_code).symbol }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="合约">{{ parseTsCode(result.ts_code).contract }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="日期">{{ result.trade_date }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="收盘">{{ result.close }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="持仓">{{ result.oi }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="短期(7d)">{{ result.short_term }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="中期(15d)">{{ result.medium_term }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="长期(30d)">{{ result.long_term }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="综合">
|
|
|
|
|
<strong>{{ result.composite }}</strong>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="信号" :span="2">
|
|
|
|
|
<el-tag :type="signalTagType(result.signal)">{{ result.signal }}</el-tag>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</el-card>
|
|
|
|
|
<div v-if="result" ref="resultRef">
|
|
|
|
|
<el-card shadow="never" class="result-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>打分结果</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-descriptions :column="2" border>
|
|
|
|
|
<el-descriptions-item label="品种">{{ parseTsCode(result.ts_code).symbol }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="合约">{{ parseTsCode(result.ts_code).contract }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="日期">{{ result.trade_date }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="收盘">{{ result.close }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="持仓">{{ result.oi }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="短期(7d)">{{ result.short_term }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="中期(15d)">{{ result.medium_term }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="长期(30d)">{{ result.long_term }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="综合">
|
|
|
|
|
<strong>{{ result.composite }}</strong>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="信号" :span="2">
|
|
|
|
|
<el-tag :type="signalTagType(result.signal)">{{ result.signal }}</el-tag>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|