first commit
This commit is contained in:
90
frontend/src/components/BacktestForm.vue
Normal file
90
frontend/src/components/BacktestForm.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Select from 'primevue/select';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import Button from 'primevue/button';
|
||||
import type { BacktestRequest } from '@/api/types';
|
||||
|
||||
defineProps<{ loading: boolean }>();
|
||||
const emit = defineEmits<{ run: [req: BacktestRequest] }>();
|
||||
|
||||
interface ParamDef { k: string; label: string; def: number; }
|
||||
const STRATS: { id: string; label: string; params: ParamDef[] }[] = [
|
||||
{ id: 'ma_cross', label: '双均线交叉', params: [{ k: 'fast', label: '快均线', def: 5 }, { k: 'slow', label: '慢均线', def: 20 }] },
|
||||
{ id: 'single_ma', label: '单均线(价格穿越)', params: [{ k: 'period', label: '均线周期', def: 20 }] },
|
||||
{ id: 'macd_cross', label: 'MACD 金叉死叉', params: [{ k: 'fast', label: '快线', def: 12 }, { k: 'slow', label: '慢线', def: 26 }, { k: 'signal', label: '信号线', def: 9 }] },
|
||||
];
|
||||
const stratOptions = STRATS.map((s) => ({ label: s.label, value: s.id }));
|
||||
|
||||
const form = reactive({
|
||||
symbol: '000001',
|
||||
timeframe: '1d',
|
||||
strategy: 'ma_cross',
|
||||
params: {} as Record<string, number>,
|
||||
initial_cash: 1000000,
|
||||
fast_mode: false,
|
||||
});
|
||||
|
||||
function applyDefaults(stratId: string) {
|
||||
const s = STRATS.find((x) => x.id === stratId)!;
|
||||
form.params = Object.fromEntries(s.params.map((p) => [p.k, p.def]));
|
||||
}
|
||||
watch(() => form.strategy, (id) => applyDefaults(id));
|
||||
applyDefaults(form.strategy);
|
||||
|
||||
const currentParams = computed(() => STRATS.find((x) => x.id === form.strategy)!.params);
|
||||
|
||||
function onRun() {
|
||||
emit('run', {
|
||||
symbol: form.symbol,
|
||||
timeframe: form.timeframe,
|
||||
strategy: form.strategy,
|
||||
params: { ...form.params },
|
||||
initial_cash: form.initial_cash,
|
||||
fast_mode: form.fast_mode,
|
||||
} satisfies BacktestRequest);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<div class="field">
|
||||
<label>策略</label>
|
||||
<Select v-model="form.strategy" :options="stratOptions" optionLabel="label" optionValue="value" size="small" style="width: 170px" />
|
||||
</div>
|
||||
<div class="field" v-for="p in currentParams" :key="p.k">
|
||||
<label>{{ p.label }}</label>
|
||||
<InputNumber v-model="form.params[p.k]" :min="1" :max="250" size="small" inputStyle="width:64px" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>周期</label>
|
||||
<Select v-model="form.timeframe" :options="[{label:'日线',value:'1d'},{label:'周线',value:'1w'},{label:'月线',value:'1M'},{label:'年线',value:'1y'}]" optionLabel="label" optionValue="value" size="small" style="width: 100px" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>标的</label>
|
||||
<InputText v-model="form.symbol" size="small" style="width: 110px" placeholder="如 000001" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>初始资金</label>
|
||||
<InputNumber v-model="form.initial_cash" :min="1000" :step="100000" size="small" mode="currency" currency="CNY" inputStyle="width:130px" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>fast 模式</label>
|
||||
<ToggleSwitch v-model="form.fast_mode" />
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<Button label="开始回测" icon="pi pi-play" :loading="loading" size="small" @click="onRun" />
|
||||
</div>
|
||||
|
||||
<div class="quick">
|
||||
<span class="qlabel">快捷:</span>
|
||||
<button v-for="q in [{code:'000001',name:'平安银行'},{code:'600519',name:'贵州茅台'},{code:'000858',name:'五粮液'},{code:'601318',name:'中国平安'},{code:'DEMO',name:'合成数据'}]" :key="q.code" class="qchip" :class="{ active: form.symbol === q.code }" type="button" @click="form.symbol = q.code">
|
||||
{{ q.code }} <span class="qname">{{ q.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hint">
|
||||
策略可选 双均线 / 单均线 / MACD;参数随策略自适应。<code>DEMO</code> 为合成数据,其余为真实 A 股(首次自动经 Tushare 拉取并缓存)。
|
||||
</div>
|
||||
</template>
|
||||
60
frontend/src/components/EquityChart.vue
Normal file
60
frontend/src/components/EquityChart.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { EquityPoint } from '@/api/types';
|
||||
|
||||
const props = defineProps<{ equity: EquityPoint[] }>();
|
||||
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
function buildOption() {
|
||||
const dates = props.equity.map(p => p.ts.slice(0, 10));
|
||||
const vals = props.equity.map(p => Number(p.value.toFixed(2)));
|
||||
const first = vals.length ? vals[0] : 0;
|
||||
const last = vals.length ? vals[vals.length - 1] : 0;
|
||||
const lineColor = last >= first ? '#f6465d' : '#0ecb81'; // A股:盈利红、亏损绿
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
grid: { left: 64, right: 18, top: 14, bottom: 26 },
|
||||
tooltip: {
|
||||
trigger: 'axis' as const,
|
||||
backgroundColor: '#1b2230', borderColor: 'rgba(255,255,255,0.1)', borderWidth: 1,
|
||||
textStyle: { color: '#e6edf3' },
|
||||
valueFormatter: (v: number) => (v ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 }),
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category', data: dates, boundaryGap: false,
|
||||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#5c6675' }, axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', scale: true,
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)' } },
|
||||
axisLabel: { color: '#5c6675' },
|
||||
},
|
||||
series: [{
|
||||
type: 'line', data: vals, symbol: 'none', smooth: false,
|
||||
lineStyle: { color: lineColor, width: 2 },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: lineColor + '40' },
|
||||
{ offset: 1, color: lineColor + '00' },
|
||||
]),
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (container.value) chart = echarts.init(container.value, undefined, { renderer: 'canvas' });
|
||||
chart?.setOption(buildOption());
|
||||
});
|
||||
onBeforeUnmount(() => { chart?.dispose(); chart = null; });
|
||||
watch(() => props.equity, () => chart?.setOption(buildOption(), true), { deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" class="chart-equity"></div>
|
||||
</template>
|
||||
237
frontend/src/components/KLineChart.vue
Normal file
237
frontend/src/components/KLineChart.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import {
|
||||
createChart, CandlestickSeries, HistogramSeries, LineSeries,
|
||||
createSeriesMarkers, CrosshairMode, LineStyle,
|
||||
type IChartApi, type ISeriesApi, type ISeriesMarkersPluginApi, type SeriesMarker, type Time,
|
||||
} from 'lightweight-charts';
|
||||
import type { Candle, IndicatorOut, SignalOut } from '@/api/types';
|
||||
|
||||
const props = defineProps<{
|
||||
candles: Candle[];
|
||||
indicators: IndicatorOut;
|
||||
signals: SignalOut[];
|
||||
symbol?: string;
|
||||
timeframe?: string;
|
||||
strategy?: string;
|
||||
}>();
|
||||
|
||||
const TF_LABEL: Record<string, string> = { '1d': '日线', '1w': '周线', '1M': '月线', '1y': '年线' };
|
||||
const STRAT_LABEL: Record<string, string> = { macd_cross: 'MACD', ma_cross: '双均线', single_ma: '单均线' };
|
||||
const WD = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
|
||||
const UP = '#f6465d';
|
||||
const DOWN = '#0ecb81';
|
||||
const DIF = '#5b8ff9';
|
||||
const DEA = '#f6bd16';
|
||||
const MA_COLORS = ['#5b8ff9', '#f6bd16', '#c084fc', '#34d399'];
|
||||
|
||||
const IND_LABEL: Record<string, string> = { macd: 'DIF', signal: 'DEA', hist: 'MACD', fast: '快线', slow: '慢线', ma: '均线' };
|
||||
const IND_COLOR: Record<string, string> = {
|
||||
macd: DIF, signal: DEA, hist: '#9aa4b2', fast: DIF, slow: DEA, ma: '#c084fc',
|
||||
};
|
||||
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
||||
let chart: IChartApi | null = null;
|
||||
let candleSeries: ISeriesApi<'Candlestick'> | null = null;
|
||||
let volumeSeries: ISeriesApi<'Histogram'> | null = null;
|
||||
let difSeries: ISeriesApi<'Line'> | null = null;
|
||||
let deaSeries: ISeriesApi<'Line'> | null = null;
|
||||
let histSeries: ISeriesApi<'Histogram'> | null = null;
|
||||
let maSeriesArr: { key: string; series: ISeriesApi<'Line'> }[] = [];
|
||||
let markersApi: ISeriesMarkersPluginApi<Time> | null = null;
|
||||
|
||||
interface DayRec {
|
||||
open: number; high: number; low: number; close: number; volume: number;
|
||||
prevClose: number | null; ind: Record<string, number | null>;
|
||||
}
|
||||
let byTime: Record<string, DayRec> = {};
|
||||
|
||||
const tip = ref<{ visible: boolean; x: number; y: number }>({ visible: false, x: 0, y: 0 });
|
||||
const tipData = ref<ReturnType<typeof buildTip> | null>(null);
|
||||
|
||||
const isMACD = computed(() => props.strategy === 'macd_cross' || 'hist' in (props.indicators.data ?? {}));
|
||||
|
||||
const legendChips = computed(() => {
|
||||
const keys = Object.keys(props.indicators.data ?? {});
|
||||
if (isMACD.value) return [{ label: 'DIF', color: DIF }, { label: 'DEA', color: DEA }];
|
||||
return keys.map((k, i) => ({ label: IND_LABEL[k] ?? k, color: MA_COLORS[i % MA_COLORS.length] }));
|
||||
});
|
||||
|
||||
const t = (ts: string): Time => ts.slice(0, 10) as Time;
|
||||
function timeKey(time: Time): string {
|
||||
if (typeof time === 'string') return time.slice(0, 10);
|
||||
const bd = time as { year: number; month: number; day: number };
|
||||
if (bd && typeof bd === 'object' && 'year' in bd) {
|
||||
return `${bd.year}-${String(bd.month).padStart(2, '0')}-${String(bd.day).padStart(2, '0')}`;
|
||||
}
|
||||
return String(time);
|
||||
}
|
||||
const fmt2 = (v: number | null) => (v == null ? '—' : v.toFixed(2));
|
||||
const fmt3 = (v: number | null) => (v == null ? '—' : v.toFixed(3));
|
||||
function weekdayOf(s: string) { return WD[new Date(s + 'T00:00:00').getDay()] ?? ''; }
|
||||
function hexA(hex: string, a: number) {
|
||||
const n = hex.replace('#', '');
|
||||
return `rgba(${parseInt(n.slice(0, 2), 16)},${parseInt(n.slice(2, 4), 16)},${parseInt(n.slice(4, 6), 16)},${a})`;
|
||||
}
|
||||
function toLine(arr: (number | null)[], c: Candle[]) {
|
||||
return arr.map((v, i) => (v == null ? { time: t(c[i].ts) } : { time: t(c[i].ts), value: v }));
|
||||
}
|
||||
|
||||
function buildTip(rec: DayRec, key: string) {
|
||||
const prev = rec.prevClose ?? rec.open;
|
||||
const change = rec.close - prev;
|
||||
return {
|
||||
date: key, weekday: weekdayOf(key),
|
||||
open: rec.open, high: rec.high, low: rec.low, close: rec.close,
|
||||
change, chgPct: prev ? (change / prev) * 100 : 0,
|
||||
amplitude: prev ? ((rec.high - rec.low) / prev) * 100 : 0,
|
||||
volLots: Math.round(rec.volume / 100),
|
||||
ind: rec.ind, up: change >= 0,
|
||||
};
|
||||
}
|
||||
|
||||
function build() {
|
||||
if (!container.value) return;
|
||||
const ch = createChart(container.value, {
|
||||
autoSize: true,
|
||||
layout: { background: { color: 'transparent' }, textColor: '#9aa4b2', fontSize: 11, attributionLogo: false },
|
||||
grid: { vertLines: { color: 'rgba(255,255,255,0.04)' }, horzLines: { color: 'rgba(255,255,255,0.04)' } },
|
||||
crosshair: {
|
||||
mode: CrosshairMode.Normal,
|
||||
vertLine: { color: 'rgba(255,255,255,0.25)', width: 1, style: LineStyle.Dashed, labelBackgroundColor: '#2a2e39' },
|
||||
horzLine: { color: 'rgba(255,255,255,0.25)', width: 1, style: LineStyle.Dashed, labelBackgroundColor: '#2a2e39' },
|
||||
},
|
||||
rightPriceScale: { borderColor: 'rgba(255,255,255,0.08)', scaleMargins: { top: 0.08, bottom: 0.28 } },
|
||||
timeScale: { borderColor: 'rgba(255,255,255,0.08)', rightOffset: 6, barSpacing: 8 },
|
||||
});
|
||||
chart = ch;
|
||||
|
||||
candleSeries = ch.addSeries(CandlestickSeries, {
|
||||
upColor: UP, downColor: DOWN, borderUpColor: UP, borderDownColor: DOWN, wickUpColor: UP, wickDownColor: DOWN,
|
||||
priceFormat: { type: 'price', precision: 2, minMove: 0.01 },
|
||||
}, 0);
|
||||
volumeSeries = ch.addSeries(HistogramSeries, { priceFormat: { type: 'volume' }, priceScaleId: 'vol' }, 0);
|
||||
volumeSeries.priceScale().applyOptions({ scaleMargins: { top: 0.82, bottom: 0 } });
|
||||
|
||||
const keys = Object.keys(props.indicators.data ?? {});
|
||||
if (isMACD.value && props.indicators.data?.hist) {
|
||||
// MACD 进副图(震荡指标,独立刻度)
|
||||
difSeries = ch.addSeries(LineSeries, { color: DIF, lineWidth: 2, priceScaleId: 'macd', priceLineVisible: false, lastValueVisible: true }, 1);
|
||||
deaSeries = ch.addSeries(LineSeries, { color: DEA, lineWidth: 2, priceScaleId: 'macd', priceLineVisible: false, lastValueVisible: true }, 1);
|
||||
histSeries = ch.addSeries(HistogramSeries, { priceScaleId: 'macd', priceLineVisible: false, lastValueVisible: false }, 1);
|
||||
try { ch.panes()[1]?.setHeight(140); } catch { /* pane 未就绪 */ }
|
||||
} else {
|
||||
// 均线叠加在主图(价格刻度,与 K 线同坐标系)
|
||||
maSeriesArr = keys.map((k, i) => ({
|
||||
key: k,
|
||||
series: ch.addSeries(LineSeries, {
|
||||
color: MA_COLORS[i % MA_COLORS.length], lineWidth: 1, priceLineVisible: false,
|
||||
lastValueVisible: false, crosshairMarkerVisible: true,
|
||||
}, 0),
|
||||
}));
|
||||
}
|
||||
|
||||
markersApi = createSeriesMarkers(candleSeries, []);
|
||||
|
||||
ch.subscribeCrosshairMove((param) => {
|
||||
const pt = param.point;
|
||||
if (!param.time || !pt || !container.value) { tip.value.visible = false; return; }
|
||||
const key = timeKey(param.time);
|
||||
const rec = byTime[key];
|
||||
if (!rec) { tip.value.visible = false; return; }
|
||||
tipData.value = buildTip(rec, key);
|
||||
const W = container.value.clientWidth, H = container.value.clientHeight;
|
||||
const TW = 220, TH = 188;
|
||||
let x = pt.x + 16; if (x + TW > W) x = pt.x - TW - 16; if (x < 4) x = 4;
|
||||
let y = pt.y + 16; if (y + TH > H) y = H - TH - 6; if (y < 4) y = 4;
|
||||
tip.value = { visible: true, x, y };
|
||||
});
|
||||
|
||||
fillData();
|
||||
ch.timeScale().fitContent();
|
||||
}
|
||||
|
||||
function fillData() {
|
||||
if (!chart || !candleSeries) return;
|
||||
const c = props.candles;
|
||||
const keys = Object.keys(props.indicators.data ?? {});
|
||||
|
||||
byTime = {};
|
||||
c.forEach((k, i) => {
|
||||
const ind: Record<string, number | null> = {};
|
||||
keys.forEach((key) => { ind[key] = props.indicators.data[key]?.[i] ?? null; });
|
||||
byTime[t(k.ts) as unknown as string] = {
|
||||
open: k.open, high: k.high, low: k.low, close: k.close, volume: k.volume,
|
||||
prevClose: i > 0 ? c[i - 1].close : null, ind,
|
||||
};
|
||||
});
|
||||
|
||||
candleSeries.setData(c.map(k => ({ time: t(k.ts), open: k.open, high: k.high, low: k.low, close: k.close })));
|
||||
volumeSeries?.setData(c.map(k => ({
|
||||
time: t(k.ts), value: k.volume, color: k.close >= k.open ? hexA(UP, 0.5) : hexA(DOWN, 0.5),
|
||||
})));
|
||||
|
||||
if (difSeries && deaSeries && histSeries && props.indicators.data?.hist) {
|
||||
difSeries.setData(toLine(props.indicators.data.macd ?? [], c));
|
||||
deaSeries.setData(toLine(props.indicators.data.signal ?? [], c));
|
||||
histSeries.setData((props.indicators.data.hist ?? []).map((v, i) => ({
|
||||
time: t(c[i].ts), value: v ?? 0, color: (v ?? 0) >= 0 ? hexA(UP, 0.6) : hexA(DOWN, 0.6),
|
||||
})));
|
||||
} else {
|
||||
maSeriesArr.forEach((m) => m.series.setData(toLine(props.indicators.data[m.key] ?? [], c)));
|
||||
}
|
||||
|
||||
const markers: SeriesMarker<Time>[] = props.signals.map(s => ({
|
||||
time: t(s.ts),
|
||||
position: s.side === 'buy' ? 'belowBar' : 'aboveBar',
|
||||
color: s.side === 'buy' ? UP : DOWN,
|
||||
shape: s.side === 'buy' ? 'arrowUp' : 'arrowDown',
|
||||
text: s.side === 'buy' ? 'B' : 'S',
|
||||
}));
|
||||
markersApi?.setMarkers(markers);
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
chart?.remove();
|
||||
chart = null; candleSeries = null; volumeSeries = null;
|
||||
difSeries = deaSeries = histSeries = null; maSeriesArr = []; markersApi = null;
|
||||
}
|
||||
|
||||
onMounted(build);
|
||||
onBeforeUnmount(teardown);
|
||||
watch(() => [props.candles, props.indicators, props.signals, props.strategy], () => { teardown(); build(); }, { deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kline-wrap">
|
||||
<div class="lc-legend">
|
||||
<span class="sym">{{ symbol ?? '—' }}<small>{{ TF_LABEL[timeframe ?? '1d'] ?? timeframe }} · {{ STRAT_LABEL[strategy ?? 'macd_cross'] ?? strategy }}</small></span>
|
||||
<span v-for="(chip, i) in legendChips" :key="i" class="chip"><i :style="{ background: chip.color }"></i>{{ chip.label }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="tip.visible && tipData" class="lc-tooltip" :style="{ left: tip.x + 'px', top: tip.y + 'px' }">
|
||||
<div class="tt-date">{{ tipData.date }} <span class="tt-wd">{{ tipData.weekday }}</span></div>
|
||||
<div class="tt-grid">
|
||||
<div>开 <b :class="tipData.up ? 'pos' : 'neg'">{{ fmt2(tipData.open) }}</b></div>
|
||||
<div>高 <b class="pos">{{ fmt2(tipData.high) }}</b></div>
|
||||
<div>低 <b class="neg">{{ fmt2(tipData.low) }}</b></div>
|
||||
<div>收 <b :class="tipData.up ? 'pos' : 'neg'">{{ fmt2(tipData.close) }}</b></div>
|
||||
</div>
|
||||
<div class="tt-row">
|
||||
涨跌 <b :class="tipData.up ? 'pos' : 'neg'">{{ tipData.change >= 0 ? '+' : '' }}{{ fmt2(tipData.change) }}</b>
|
||||
· 涨幅 <b :class="tipData.up ? 'pos' : 'neg'">{{ tipData.chgPct.toFixed(2) }}%</b>
|
||||
</div>
|
||||
<div class="tt-row">振幅 {{ tipData.amplitude.toFixed(2) }}% · 量 {{ tipData.volLots.toLocaleString() }} 手</div>
|
||||
<div class="tt-sep"></div>
|
||||
<div class="tt-ind">
|
||||
<span v-for="(v, k) in tipData.ind" :key="k" :style="{ color: IND_COLOR[k] ?? '#9aa4b2' }">
|
||||
{{ IND_LABEL[k] ?? k }} {{ fmt3(v) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="container" class="chart-kline"></div>
|
||||
</div>
|
||||
</template>
|
||||
20
frontend/src/components/MetricsPanel.vue
Normal file
20
frontend/src/components/MetricsPanel.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { MetricsOut } from '@/api/types';
|
||||
|
||||
defineProps<{ metrics: MetricsOut }>();
|
||||
|
||||
const pct = (x: number) => `${(x * 100).toFixed(2)}%`;
|
||||
const num = (x: number) => x.toFixed(2);
|
||||
const sign = (x: number) => (x >= 0 ? 'pos' : 'neg'); // A股:正=红、负=绿
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stats">
|
||||
<div class="stat"><span class="label">总收益</span><span class="value" :class="sign(metrics.total_return)">{{ pct(metrics.total_return) }}</span></div>
|
||||
<div class="stat"><span class="label">最大回撤</span><span class="value neg">{{ pct(metrics.max_drawdown) }}</span></div>
|
||||
<div class="stat"><span class="label">夏普</span><span class="value" :class="sign(metrics.sharpe)">{{ num(metrics.sharpe) }}</span></div>
|
||||
<div class="stat"><span class="label">年化波动</span><span class="value">{{ pct(metrics.volatility) }}</span></div>
|
||||
<div class="stat"><span class="label">胜率</span><span class="value">{{ pct(metrics.win_rate) }}</span></div>
|
||||
<div class="stat"><span class="label">交易数</span><span class="value">{{ metrics.num_trades }}</span></div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user