fix(backtest): 结果页4个显示bug—成交枚举/时间/浮点精度/日志空
1. 交易详情方向/开平显原始枚举: Direction.LONG/Offset.OPEN → 买/卖、开仓/平仓/平今/平昨
(TradesTable 加 fmtDir/fmtOff, 方向带涨跌色)
2. 交易详情时间 2024-06-25T00:00:00+08:00 → 2024-06-25 (日内带时分则保留)
3. 每日收益浮点精度 0.6533000000054017 → 0.65 (toFixed2, 涨跌色), 日期去 00:00:00
4. 日志tab硬编码"暂无日志数据": 前端接 getLog(/task/:id/log) + 后端 cta_engine 加
stdout tee 把引擎 load/run/stats 输出落盘到 {task_id}.log (worker 进程内隔离)
→ 新回测日志 tab 显示真实引擎输出(2117字)
验证: 交易 买/开仓/2024-06-25, 收益 0.15, 日志 2117字真实内容
This commit is contained in:
@@ -2,15 +2,62 @@
|
||||
import type { Trade } from '@/api/backtest'
|
||||
|
||||
defineProps<{ trades: Trade[] }>()
|
||||
|
||||
// vnpy 枚举 repr → 中文(兼容已是中文的情况)
|
||||
function fmtDir(d: string): string {
|
||||
if (!d) return ''
|
||||
const s = String(d)
|
||||
if (s.includes('LONG') || s === '多') return '买'
|
||||
if (s.includes('SHORT') || s === '空') return '卖'
|
||||
if (s.includes('NET')) return '净'
|
||||
return s
|
||||
}
|
||||
function fmtOff(o: string): string {
|
||||
if (!o) return ''
|
||||
const s = String(o)
|
||||
if (s.includes('CLOSETODAY')) return '平今'
|
||||
if (s.includes('CLOSEFIRST')) return '平昨'
|
||||
if (s.includes('CLOSE')) return '平仓'
|
||||
if (s.includes('OPEN')) return '开仓'
|
||||
return s
|
||||
}
|
||||
// 2024-06-25T00:00:00+08:00 → 2024-06-25(日内带时分则保留)
|
||||
function fmtDate(dt: string): string {
|
||||
if (!dt) return ''
|
||||
const s = String(dt).slice(0, 19).replace('T', ' ')
|
||||
return s.endsWith(' 00:00:00') ? s.slice(0, 10) : s
|
||||
}
|
||||
function dirClass(d: string): string {
|
||||
const v = fmtDir(d)
|
||||
return v === '买' ? 'up' : v === '卖' ? 'down' : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-table :data="trades" stripe size="small" empty-text="无成交">
|
||||
<el-table-column prop="datetime" label="时间" width="220" />
|
||||
<el-table-column prop="direction" label="方向" width="80" />
|
||||
<el-table-column prop="offset" label="开平" width="80" />
|
||||
<el-table-column prop="price" label="价格" width="100" />
|
||||
<el-table-column prop="volume" label="数量" width="100" />
|
||||
<el-table-column prop="vt_symbol" label="标的" />
|
||||
<el-table-column label="时间" width="150">
|
||||
<template #default="{ row }"><span class="mono">{{ fmtDate(row.datetime) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方向" width="70">
|
||||
<template #default="{ row }"><span :class="dirClass(row.direction)">{{ fmtDir(row.direction) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开平" width="70">
|
||||
<template #default="{ row }">{{ fmtOff(row.offset) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="100" align="right">
|
||||
<template #default="{ row }"><span class="mono">{{ row.price }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="90" align="right">
|
||||
<template #default="{ row }"><span class="mono">{{ row.volume }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标的" min-width="100">
|
||||
<template #default="{ row }"><span class="mono">{{ row.vt_symbol }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.mono) { font-family: var(--mono); font-size: 12px; color: var(--text); }
|
||||
.up { color: var(--up); }
|
||||
.down { color: var(--down); }
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
getResult, getEquityCurve, getDailyPnl, getTrades, getKline,
|
||||
getRelativeMetrics, getBenchmarkCurve, getRiskSeries,
|
||||
getRelativeMetrics, getBenchmarkCurve, getRiskSeries, getLog,
|
||||
type EquityPoint, type PnlPoint, type Trade, type KlineBar,
|
||||
type RelativeMetrics, type BenchmarkCurveData, type RiskSeriesData,
|
||||
} from '@/api/backtest'
|
||||
@@ -42,6 +42,7 @@ const equity = ref<EquityPoint[]>([])
|
||||
const pnl = ref<PnlPoint[]>([])
|
||||
const trades = ref<Trade[]>([])
|
||||
const kline = ref<KlineBar[]>([])
|
||||
const logText = ref('')
|
||||
|
||||
// statEntries is no longer used in the new layout but kept for potential future use
|
||||
// const statEntries = computed(() =>
|
||||
@@ -135,10 +136,32 @@ onMounted(async () => {
|
||||
kline.value = []
|
||||
}
|
||||
}
|
||||
try {
|
||||
logText.value = await getLog(taskId)
|
||||
} catch {
|
||||
logText.value = ''
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 每日收益格式化:浮点精度 → 2 位;日期去 00:00:00
|
||||
function fmtPnl(v: number | string): string {
|
||||
const n = typeof v === 'number' ? v : parseFloat(v)
|
||||
if (!Number.isFinite(n)) return String(v)
|
||||
return n.toFixed(2)
|
||||
}
|
||||
function fmtPnlClass(v: number | string): string {
|
||||
const n = typeof v === 'number' ? v : parseFloat(v)
|
||||
if (!Number.isFinite(n) || n === 0) return ''
|
||||
return n > 0 ? 'up' : 'down'
|
||||
}
|
||||
function fmtDate(d: string): string {
|
||||
if (!d) return ''
|
||||
const s = String(d).slice(0, 19).replace('T', ' ')
|
||||
return s.endsWith(' 00:00:00') ? s.slice(0, 10) : s
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -219,8 +242,12 @@ onMounted(async () => {
|
||||
<template #header><span class="section-title">每日收益</span></template>
|
||||
<div class="daily-pnl-section">
|
||||
<el-table :data="pnl" stripe size="small" empty-text="无数据">
|
||||
<el-table-column prop="date" label="日期" width="120" />
|
||||
<el-table-column prop="pnl" label="收益" width="120" />
|
||||
<el-table-column label="日期" width="140">
|
||||
<template #default="{ row }"><span class="mono">{{ fmtDate(row.date) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收益" width="120" align="right">
|
||||
<template #default="{ row }"><span class="mono" :class="fmtPnlClass(row.pnl)">{{ fmtPnl(row.pnl) }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -230,7 +257,7 @@ onMounted(async () => {
|
||||
<el-tab-pane label="日志输出">
|
||||
<el-card>
|
||||
<template #header><span class="section-title">系统日志</span></template>
|
||||
<pre class="log-output">暂无日志数据</pre>
|
||||
<pre class="log-output">{{ logText || '暂无日志数据' }}</pre>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
@@ -20,6 +20,24 @@ from sanguo_data.datareader import read_index_daily
|
||||
from sanguo_backtest.metrics import compute_metrics, BENCHMARK_SYMBOL
|
||||
|
||||
|
||||
class _Tee:
|
||||
"""同时写多个流(用于把引擎 stdout 落盘到 per-task 日志)。"""
|
||||
|
||||
def __init__(self, *streams):
|
||||
self.streams = streams
|
||||
|
||||
def write(self, data):
|
||||
for s in self.streams:
|
||||
s.write(data)
|
||||
|
||||
def flush(self):
|
||||
for s in self.streams:
|
||||
try:
|
||||
s.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Mock Exchange enum for local use (replaces vnpy.trader.constant.Exchange)
|
||||
class MockExchange:
|
||||
SSE = "SSE" # Shanghai Stock Exchange
|
||||
@@ -115,16 +133,34 @@ def run_cta_backtest(strategy_class, symbol: str, params: dict, start: str, end:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Load historical data
|
||||
engine.load_data()
|
||||
# Capture engine run output (load/run/stats) to per-task log file, so the
|
||||
# result page 日志 tab has real content. Tee stdout inside the worker process
|
||||
# (contained — doesn't affect the main API process).
|
||||
file_dir = os.path.dirname(os.path.abspath(db_path))
|
||||
log_path = os.path.join(file_dir, f"{task_id}.log")
|
||||
_log_f = open(log_path, "w", encoding="utf-8")
|
||||
_log_f.write(
|
||||
f"==== 回测日志 ====\n任务: {task_id}\n策略: {getattr(strategy_class, '__name__', strategy_class)}\n"
|
||||
f"标的: {vt_symbol}\n区间: {start} ~ {end}\n参数: {params}\n基准: {benchmark}\n==================\n"
|
||||
)
|
||||
_log_f.flush()
|
||||
_orig_stdout = sys.stdout
|
||||
sys.stdout = _Tee(_orig_stdout, _log_f)
|
||||
try:
|
||||
# Load historical data
|
||||
engine.load_data()
|
||||
|
||||
# Run backtesting
|
||||
engine.run_backtesting()
|
||||
# Run backtesting
|
||||
engine.run_backtesting()
|
||||
|
||||
# Calculate statistics — calculate_result() returns a daily DataFrame,
|
||||
# calculate_statistics(df) returns the stats dict (sharpe/drawdown/etc.)
|
||||
daily_df = engine.calculate_result()
|
||||
raw_stats = engine.calculate_statistics(daily_df, output=False) or {}
|
||||
# Calculate statistics — calculate_result() returns a daily DataFrame,
|
||||
# calculate_statistics(df) returns the stats dict (sharpe/drawdown/etc.)
|
||||
daily_df = engine.calculate_result()
|
||||
raw_stats = engine.calculate_statistics(daily_df, output=False) or {}
|
||||
finally:
|
||||
sys.stdout = _orig_stdout
|
||||
_log_f.flush()
|
||||
_log_f.close()
|
||||
# Ensure JSON-serializable (vnpy may include Timestamp / non-numeric / NaN values)
|
||||
statistics = {
|
||||
k: (None if (isinstance(v, float) and not math.isfinite(v))
|
||||
|
||||
Reference in New Issue
Block a user