"""基于中台日线的简易回测引擎(均线交叉策略)。 读取 quotes_daily,金叉满仓 / 死叉空仓,输出资金曲线与核心指标。 """ from __future__ import annotations from sqlalchemy import select from db import get_session from models import DailyQuote def _ma(arr, n): out = [None] * len(arr) s = 0.0 for i, v in enumerate(arr): s += v if i >= n: s -= arr[i - n] if i >= n - 1: out[i] = s / n return out def run_backtest(symbol: str, fast: int = 5, slow: int = 20, fee: float = 0.0005): with get_session() as s: rows = s.execute( select(DailyQuote.date, DailyQuote.close) .where(DailyQuote.code == symbol) .order_by(DailyQuote.date) ).all() if len(rows) < slow + 5: return {"ok": False, "msg": "该股票库内日线不足,请先在数据中台入库", "have": len(rows)} dates = [r[0].isoformat() for r in rows] close = [float(r[1]) for r in rows] maf, mas = _ma(close, fast), _ma(close, slow) equity, bench = [], [] cash, pos, shares = 1.0, 0, 0.0 trades, wins, entry = 0, 0, 0.0 peak, max_dd = 1.0, 0.0 base = close[0] for i in range(len(close)): if maf[i] is not None and mas[i] is not None: if pos == 0 and maf[i] > mas[i]: shares = cash * (1 - fee) / close[i] cash, pos, entry = 0.0, 1, close[i] trades += 1 elif pos == 1 and maf[i] < mas[i]: cash = shares * close[i] * (1 - fee) shares, pos = 0.0, 0 if close[i] > entry: wins += 1 nav = cash + shares * close[i] equity.append(round(nav, 4)) bench.append(round(close[i] / base, 4)) peak = max(peak, nav) max_dd = max(max_dd, (peak - nav) / peak) total_ret = equity[-1] - 1 bench_ret = bench[-1] - 1 closed = trades - pos win_rate = (wins / closed) if closed > 0 else 0.0 return { "ok": True, "symbol": symbol, "fast": fast, "slow": slow, "dates": dates, "equity": equity, "bench": bench, "metrics": { "total_return": round(total_ret * 100, 2), "bench_return": round(bench_ret * 100, 2), "excess": round((total_ret - bench_ret) * 100, 2), "max_drawdown": round(max_dd * 100, 2), "trades": trades, "win_rate": round(win_rate * 100, 1), }, }