Files
stock_cursor_v0/backend/backtest.py

79 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""基于中台日线的简易回测引擎(均线交叉策略)。
读取 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),
},
}