新增合约全历史拉取与 K 线打分叠加功能
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,10 @@ class RunRangeRequest(BaseModel):
|
|||||||
end_date: str
|
end_date: str
|
||||||
|
|
||||||
|
|
||||||
|
class RunFullRequest(BaseModel):
|
||||||
|
ts_code: str
|
||||||
|
|
||||||
|
|
||||||
class RunResponse(BaseModel):
|
class RunResponse(BaseModel):
|
||||||
ts_code: str
|
ts_code: str
|
||||||
trade_date: str
|
trade_date: str
|
||||||
@@ -131,6 +135,37 @@ def run_range(req: RunRangeRequest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/run/full")
|
||||||
|
def run_full(req: RunFullRequest):
|
||||||
|
"""拉取指定合约全部历史数据,保存 candles,对所有可打分日期逐日打分并保存。"""
|
||||||
|
df = fetcher.fetch_contract(req.ts_code)
|
||||||
|
storage.save_candles(df)
|
||||||
|
|
||||||
|
results, warnings, total_days, scored_count = scorer.score_all(df)
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
storage.save_score(r)
|
||||||
|
|
||||||
|
skipped_count = total_days - scored_count
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ts_code": req.ts_code,
|
||||||
|
"total_days": total_days,
|
||||||
|
"scored_count": scored_count,
|
||||||
|
"skipped_count": skipped_count,
|
||||||
|
"warnings": warnings,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"trade_date": r.trade_date,
|
||||||
|
"close": r.close,
|
||||||
|
"composite": r.composite,
|
||||||
|
"signal": r.signal,
|
||||||
|
}
|
||||||
|
for r in results
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/scores")
|
@app.get("/api/v1/scores")
|
||||||
def list_scores(
|
def list_scores(
|
||||||
ts_code: Optional[str] = Query(None),
|
ts_code: Optional[str] = Query(None),
|
||||||
|
|||||||
@@ -254,3 +254,29 @@ def score_range(
|
|||||||
warnings.append(str(e))
|
warnings.append(str(e))
|
||||||
|
|
||||||
return results, warnings
|
return results, warnings
|
||||||
|
|
||||||
|
|
||||||
|
def score_all(df: pd.DataFrame) -> tuple[list[ScoreResult], list[str], int, int]:
|
||||||
|
"""对 DataFrame 中所有有足够前置数据的交易日逐日打分。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(results, warnings, total_days, scored_count)
|
||||||
|
"""
|
||||||
|
if len(df) < 31:
|
||||||
|
raise ValueError(f"数据量不足(仅 {len(df)} 行),需要至少 31 行")
|
||||||
|
|
||||||
|
results: list[ScoreResult] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
for idx in range(30, len(df)):
|
||||||
|
subset = df.iloc[: idx + 1].copy()
|
||||||
|
trade_date = str(df.iloc[idx]["trade_date"])
|
||||||
|
try:
|
||||||
|
result = score_daily(subset)
|
||||||
|
results.append(result)
|
||||||
|
except ValueError as e:
|
||||||
|
warnings.append(f"{trade_date}: {e}")
|
||||||
|
|
||||||
|
total_days = len(df) - 30
|
||||||
|
scored_count = len(results)
|
||||||
|
return results, warnings, total_days, scored_count
|
||||||
|
|||||||
@@ -88,6 +88,40 @@ func (d *Deps) RunRange(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = io.Copy(w, resp.Body)
|
_, _ = io.Copy(w, resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type runFullRequest struct {
|
||||||
|
TsCode string `json:"ts_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deps) RunFull(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req runFullRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeErr(w, http.StatusBadRequest, "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.TsCode == "" {
|
||||||
|
writeErr(w, http.StatusBadRequest, "ts_code is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, http.StatusInternalServerError, "encode request failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 300 * time.Second}
|
||||||
|
resp, err := client.Post(d.TushareURL+"/api/v1/run/full", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, http.StatusBadGateway, fmt.Sprintf("tushare service unavailable: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
_, _ = io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Deps) GetActiveContract(w http.ResponseWriter, r *http.Request) {
|
func (d *Deps) GetActiveContract(w http.ResponseWriter, r *http.Request) {
|
||||||
symbol := r.URL.Query().Get("symbol")
|
symbol := r.URL.Query().Get("symbol")
|
||||||
if symbol == "" {
|
if symbol == "" {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func New(d *handlers.Deps, dist fs.FS) http.Handler {
|
|||||||
r.Post("/run", d.RunPipeline)
|
r.Post("/run", d.RunPipeline)
|
||||||
r.Post("/run/batch", d.RunBatch)
|
r.Post("/run/batch", d.RunBatch)
|
||||||
r.Post("/run/range", d.RunRange)
|
r.Post("/run/range", d.RunRange)
|
||||||
|
r.Post("/run/full", d.RunFull)
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(mw.RequireAdmin)
|
r.Use(mw.RequireAdmin)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (s *FuturesStore) ListScores(f ScoreFilter) ([]Score, error) {
|
|||||||
args = append(args, "%"+f.Signal+"%")
|
args = append(args, "%"+f.Signal+"%")
|
||||||
}
|
}
|
||||||
q += " ORDER BY trade_date DESC, id DESC"
|
q += " ORDER BY trade_date DESC, id DESC"
|
||||||
if f.Limit <= 0 || f.Limit > 500 {
|
if f.Limit <= 0 || f.Limit > 1000 {
|
||||||
f.Limit = 200
|
f.Limit = 200
|
||||||
}
|
}
|
||||||
q += " LIMIT " + next()
|
q += " LIMIT " + next()
|
||||||
|
|||||||
@@ -53,10 +53,34 @@ export interface RunRangeResponse {
|
|||||||
results: RunRangeResult[]
|
results: RunRangeResult[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunFullRequest {
|
||||||
|
ts_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunFullResult {
|
||||||
|
trade_date: string
|
||||||
|
close: number
|
||||||
|
composite: number
|
||||||
|
signal: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunFullResponse {
|
||||||
|
ts_code: string
|
||||||
|
total_days: number
|
||||||
|
scored_count: number
|
||||||
|
skipped_count: number
|
||||||
|
warnings: string[]
|
||||||
|
results: RunFullResult[]
|
||||||
|
}
|
||||||
|
|
||||||
export function runRange(req: RunRangeRequest) {
|
export function runRange(req: RunRangeRequest) {
|
||||||
return client.post<RunRangeResponse>('/run/range', req, { timeout: 180_000 }).then((r) => r.data)
|
return client.post<RunRangeResponse>('/run/range', req, { timeout: 180_000 }).then((r) => r.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runFull(req: RunFullRequest) {
|
||||||
|
return client.post<RunFullResponse>('/run/full', req, { timeout: 300_000 }).then((r) => r.data)
|
||||||
|
}
|
||||||
|
|
||||||
export function runBatch() {
|
export function runBatch() {
|
||||||
return client.post('/run/batch', null, { timeout: 180_000 }).then((r) => r.data)
|
return client.post('/run/batch', null, { timeout: 180_000 }).then((r) => r.data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { Candle } from '@/api/candles'
|
|||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { useMobile } from '@/composables/useMobile'
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
const props = defineProps<{ data: Candle[] }>()
|
const props = defineProps<{ data: Candle[]; scores?: { trade_date: string; composite: number }[] }>()
|
||||||
const theme = useThemeStore()
|
const theme = useThemeStore()
|
||||||
const { isMobile } = useMobile()
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
@@ -28,51 +28,86 @@ function render() {
|
|||||||
const ohlc = props.data.map((c) => [c.open, c.close, c.low, c.high])
|
const ohlc = props.data.map((c) => [c.open, c.close, c.low, c.high])
|
||||||
const oi = props.data.map((c) => c.oi)
|
const oi = props.data.map((c) => c.oi)
|
||||||
|
|
||||||
|
const scoreMap = new Map((props.scores || []).map((s) => [s.trade_date, s.composite]))
|
||||||
|
const compositeData = props.data.map((c) => scoreMap.get(c.trade_date) ?? null)
|
||||||
|
|
||||||
|
const hasScores = props.scores && props.scores.length > 0
|
||||||
|
const legendData = hasScores ? ['K 线', '持仓量', '综合分'] : ['K 线', '持仓量']
|
||||||
|
const xAxisIndices = hasScores ? [0, 1, 2] : [0, 1]
|
||||||
|
const grids: any[] = [
|
||||||
|
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: 40, height: hasScores ? '52%' : '60%' },
|
||||||
|
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: hasScores ? '72%' : '78%', height: hasScores ? '14%' : '18%' },
|
||||||
|
]
|
||||||
|
const xAxes: any[] = [
|
||||||
|
{ type: 'category', data: dates, scale: true, boundaryGap: false },
|
||||||
|
{ type: 'category', gridIndex: 1, data: dates, scale: true, boundaryGap: false, axisLabel: { show: false } },
|
||||||
|
]
|
||||||
|
const yAxes: any[] = [
|
||||||
|
{ scale: true, splitArea: { show: true } },
|
||||||
|
{ gridIndex: 1, scale: true, splitNumber: 3 },
|
||||||
|
]
|
||||||
|
const series: any[] = [
|
||||||
|
{
|
||||||
|
name: 'K 线',
|
||||||
|
type: 'candlestick',
|
||||||
|
data: ohlc,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ec3a3a',
|
||||||
|
color0: '#26a69a',
|
||||||
|
borderColor: '#ec3a3a',
|
||||||
|
borderColor0: '#26a69a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '持仓量',
|
||||||
|
type: 'line',
|
||||||
|
xAxisIndex: 1,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
data: oi,
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
lineStyle: { color: '#5470c6' },
|
||||||
|
areaStyle: { opacity: 0.15, color: '#5470c6' },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (hasScores) {
|
||||||
|
grids.push({ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: '88%', height: '10%' })
|
||||||
|
xAxes.push({ type: 'category', gridIndex: 2, data: dates, scale: true, boundaryGap: false, axisLabel: { show: false } })
|
||||||
|
yAxes.push({ gridIndex: 2, min: 0, max: 100, splitNumber: 2 })
|
||||||
|
series.push({
|
||||||
|
name: '综合分',
|
||||||
|
type: 'bar',
|
||||||
|
xAxisIndex: 2,
|
||||||
|
yAxisIndex: 2,
|
||||||
|
data: compositeData,
|
||||||
|
barWidth: '60%',
|
||||||
|
itemStyle: {
|
||||||
|
color: (params: any) => {
|
||||||
|
const val = params.value as number | null
|
||||||
|
if (val == null) return 'transparent'
|
||||||
|
if (val >= 80) return '#ec3a3a'
|
||||||
|
if (val >= 50) return '#f89898'
|
||||||
|
if (val >= 40) return '#89d6c7'
|
||||||
|
return '#26a69a'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
chart.setOption(
|
chart.setOption(
|
||||||
{
|
{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
|
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
|
||||||
legend: { data: ['K 线', '持仓量'], top: 0 },
|
legend: { data: legendData, top: 0 },
|
||||||
grid: [
|
grid: grids,
|
||||||
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: 40, height: '60%' },
|
xAxis: xAxes,
|
||||||
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: '78%', height: '18%' },
|
yAxis: yAxes,
|
||||||
],
|
|
||||||
xAxis: [
|
|
||||||
{ type: 'category', data: dates, scale: true, boundaryGap: false },
|
|
||||||
{ type: 'category', gridIndex: 1, data: dates, scale: true, boundaryGap: false, axisLabel: { show: false } },
|
|
||||||
],
|
|
||||||
yAxis: [
|
|
||||||
{ scale: true, splitArea: { show: true } },
|
|
||||||
{ gridIndex: 1, scale: true, splitNumber: 3 },
|
|
||||||
],
|
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{ type: 'inside', xAxisIndex: [0, 1] },
|
{ type: 'inside', xAxisIndex: xAxisIndices },
|
||||||
{ type: 'slider', xAxisIndex: [0, 1], height: 18, bottom: 6 },
|
{ type: 'slider', xAxisIndex: xAxisIndices, height: 18, bottom: 6 },
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'K 线',
|
|
||||||
type: 'candlestick',
|
|
||||||
data: ohlc,
|
|
||||||
itemStyle: {
|
|
||||||
color: '#ec3a3a',
|
|
||||||
color0: '#26a69a',
|
|
||||||
borderColor: '#ec3a3a',
|
|
||||||
borderColor0: '#26a69a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '持仓量',
|
|
||||||
type: 'line',
|
|
||||||
xAxisIndex: 1,
|
|
||||||
yAxisIndex: 1,
|
|
||||||
data: oi,
|
|
||||||
smooth: true,
|
|
||||||
showSymbol: false,
|
|
||||||
lineStyle: { color: '#5470c6' },
|
|
||||||
areaStyle: { opacity: 0.15, color: '#5470c6' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
series,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { listContracts } from '@/api/scores'
|
import { listContracts } from '@/api/scores'
|
||||||
import { listCandles, type Candle } from '@/api/candles'
|
import { listCandles, type Candle } from '@/api/candles'
|
||||||
|
import { listScores, type Score } from '@/api/scores'
|
||||||
|
import { runFull, type RunFullResponse } from '@/api/run'
|
||||||
import KLineChart from '@/components/KLineChart.vue'
|
import KLineChart from '@/components/KLineChart.vue'
|
||||||
import { useMobile } from '@/composables/useMobile'
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
@@ -15,7 +17,10 @@ const filter = reactive<{ ts_code: string; range: [string, string] | [] }>({
|
|||||||
|
|
||||||
const contracts = ref<string[]>([])
|
const contracts = ref<string[]>([])
|
||||||
const candles = ref<Candle[]>([])
|
const candles = ref<Candle[]>([])
|
||||||
|
const scores = ref<Score[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const fullLoading = ref(false)
|
||||||
|
const fullResult = ref<RunFullResponse | null>(null)
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
if (!filter.ts_code) {
|
if (!filter.ts_code) {
|
||||||
@@ -25,12 +30,48 @@ async function reload() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [start, end] = filter.range || []
|
const [start, end] = filter.range || []
|
||||||
candles.value = await listCandles(filter.ts_code, start, end)
|
const [candleData, scoreData] = await Promise.all([
|
||||||
|
listCandles(filter.ts_code, start, end),
|
||||||
|
listScores({ ts_code: filter.ts_code, start, end, limit: 1000 }),
|
||||||
|
])
|
||||||
|
candles.value = candleData
|
||||||
|
scores.value = scoreData
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleFetchAndScore() {
|
||||||
|
if (!filter.ts_code) {
|
||||||
|
ElMessage.warning('请输入或选择合约')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`即将拉取 ${filter.ts_code} 的全部历史数据并逐日打分,这可能需要一些时间。`,
|
||||||
|
'拉取并打分',
|
||||||
|
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' },
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fullLoading.value = true
|
||||||
|
fullResult.value = null
|
||||||
|
try {
|
||||||
|
const resp = await runFull({ ts_code: filter.ts_code })
|
||||||
|
fullResult.value = resp
|
||||||
|
ElMessage.success(`完成: ${resp.scored_count} 天已打分, ${resp.skipped_count} 天跳过`)
|
||||||
|
await reload()
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err?.response?.data?.error || err.message || '请求失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
} finally {
|
||||||
|
fullLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
contracts.value = await listContracts().catch(() => [])
|
contracts.value = await listContracts().catch(() => [])
|
||||||
if (contracts.value.length > 0) {
|
if (contracts.value.length > 0) {
|
||||||
@@ -45,14 +86,22 @@ onMounted(async () => {
|
|||||||
<el-card shadow="never" class="filter-card">
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form :inline="!isMobile">
|
<el-form :inline="!isMobile">
|
||||||
<el-form-item label="合约">
|
<el-form-item label="合约">
|
||||||
<el-select
|
<div style="display: flex; gap: 8px;">
|
||||||
v-model="filter.ts_code"
|
<el-input
|
||||||
placeholder="选择合约"
|
v-model="filter.ts_code"
|
||||||
filterable
|
placeholder="输入合约代码如 FG2509.ZCE"
|
||||||
:style="{ width: isMobile ? '100%' : '200px' }"
|
clearable
|
||||||
>
|
:style="{ width: isMobile ? '100%' : '200px' }"
|
||||||
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
/>
|
||||||
</el-select>
|
<el-select
|
||||||
|
v-model="filter.ts_code"
|
||||||
|
placeholder="已存合约"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
|
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="日期">
|
<el-form-item label="日期">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
@@ -67,12 +116,27 @@ onMounted(async () => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :loading="loading" @click="reload">刷新</el-button>
|
<el-button type="primary" :loading="loading" @click="reload">刷新</el-button>
|
||||||
|
<el-button type="warning" :loading="fullLoading" @click="handleFetchAndScore">
|
||||||
|
拉取并打分
|
||||||
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="fullResult" shadow="never" class="result-card">
|
||||||
|
<el-descriptions :column="isMobile ? 2 : 4" border>
|
||||||
|
<el-descriptions-item label="合约">{{ fullResult.ts_code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总天数">{{ fullResult.total_days }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="已打分">{{ fullResult.scored_count }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="跳过">{{ fullResult.skipped_count }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" class="chart-card" v-loading="loading">
|
<el-card shadow="never" class="chart-card" v-loading="loading">
|
||||||
<KLineChart :data="candles" />
|
<KLineChart
|
||||||
|
:data="candles"
|
||||||
|
:scores="scores.map((s) => ({ trade_date: s.trade_date, composite: s.composite }))"
|
||||||
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,6 +150,9 @@ onMounted(async () => {
|
|||||||
.filter-card :deep(.el-card__body) {
|
.filter-card :deep(.el-card__body) {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
.result-card :deep(.el-card__body) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
.chart-card :deep(.el-card__body) {
|
.chart-card :deep(.el-card__body) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user