@@ -219,8 +242,12 @@ onMounted(async () => {
每日收益
-
-
+
+ {{ fmtDate(row.date) }}
+
+
+ {{ fmtPnl(row.pnl) }}
+
@@ -230,7 +257,7 @@ onMounted(async () => {
系统日志
- 暂无日志数据
+ {{ logText || '暂无日志数据' }}
diff --git a/sanguo_backtest/cta_engine.py b/sanguo_backtest/cta_engine.py
index ff315c4..471760d 100644
--- a/sanguo_backtest/cta_engine.py
+++ b/sanguo_backtest/cta_engine.py
@@ -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))