Initial commit: stock analysis backend and prototype UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
78
backend/backtest.py
Normal file
78
backend/backtest.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""基于中台日线的简易回测引擎(均线交叉策略)。
|
||||
|
||||
读取 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),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user