79 lines
2.5 KiB
Python
79 lines
2.5 KiB
Python
"""基于中台日线的简易回测引擎(均线交叉策略)。
|
||
|
||
读取 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),
|
||
},
|
||
}
|