Files
stock-advisor/engine/indicators.py
T

165 lines
4.8 KiB
Python

# -*- coding: utf-8 -*-
"""
技术指标计算:MA / RSI / MACD / KDJ / 量比 / 动量 / 波动率
输入 bars:按日期升序的 [{date, open, high, low, close, volume}, ...]
"""
import math
def _sma(vals, n):
if len(vals) < n:
return None
return sum(vals[-n:]) / n
def _ema(vals, n):
if not vals:
return None
k = 2 / (n + 1)
e = vals[0]
for v in vals[1:]:
e = v * k + e * (1 - k)
return e
def _ema_series(vals, n):
out = []
if not vals:
return out
k = 2 / (n + 1)
e = vals[0]
out.append(e)
for v in vals[1:]:
e = v * k + e * (1 - k)
out.append(e)
return out
def rsi14(closes):
"""Wilder RSI(14)"""
if len(closes) < 15:
return 50.0
gains, losses = [], []
for i in range(1, len(closes)):
chg = closes[i] - closes[i - 1]
gains.append(max(chg, 0))
losses.append(max(-chg, 0))
avg_g = sum(gains[:14]) / 14
avg_l = sum(losses[:14]) / 14
for i in range(14, len(gains)):
avg_g = (avg_g * 13 + gains[i]) / 14
avg_l = (avg_l * 13 + losses[i]) / 14
if avg_l == 0:
return 100.0
rs = avg_g / avg_l
return 100 - 100 / (1 + rs)
def kdj(bars, n=9, k_smooth=3, d_smooth=3):
"""返回 (K, D, J)"""
if len(bars) < n:
return 50.0, 50.0, 50.0
k, d = 50.0, 50.0
for i in range(n - 1, len(bars)):
window = bars[i - n + 1:i + 1]
low_n = min(b["low"] for b in window)
high_n = max(b["high"] for b in window)
rsv = 0 if high_n == low_n else (bars[i]["close"] - low_n) / (high_n - low_n) * 100
k = (k * (k_smooth - 1) + rsv) / k_smooth
d = (d * (d_smooth - 1) + k) / d_smooth
j = 3 * k - 2 * d
return k, d, j
def compute_indicators(bars):
"""计算全部技术指标,返回 dict(最新值 + 序列用于画图)"""
if not bars:
return {}
closes = [b["close"] for b in bars]
last = bars[-1]
prev = bars[-2] if len(bars) > 1 else last
ma5 = _sma(closes, 5)
ma10 = _sma(closes, 10)
ma20 = _sma(closes, 20)
ma60 = _sma(closes, 60)
# MACD
ema12 = _ema_series(closes, 12)
ema26 = _ema_series(closes, 26)
dif_series = [e12 - e26 for e12, e26 in zip(ema12, ema26)]
dea_series = _ema_series(dif_series, 9)
dif = dif_series[-1] if dif_series else 0
dea = dea_series[-1] if dea_series else 0
macd = (dif - dea) * 2
rsi = rsi14(closes)
k, d, j = kdj(bars)
# 涨跌幅
chg_1d = (last["close"] - prev["close"]) / prev["close"] * 100 if prev["close"] else 0
chg_5d = (last["close"] - closes[-6]) / closes[-6] * 100 if len(closes) > 6 else chg_1d
chg_10d = (last["close"] - closes[-11]) / closes[-11] * 100 if len(closes) > 11 else chg_1d
chg_20d = (last["close"] - closes[-21]) / closes[-21] * 100 if len(closes) > 21 else chg_1d
# 量比 = 今日量 / 前5日均量
vol_ratio = 1.0
if len(bars) > 6:
avg5 = sum(b["volume"] for b in bars[-6:-1]) / 5
if avg5 > 0:
vol_ratio = last["volume"] / avg5
# 20日波动率(年化近似省略,日波动)
returns = []
for i in range(1, len(closes)):
if closes[i - 1]:
returns.append((closes[i] - closes[i - 1]) / closes[i - 1])
vol20 = (sum(r * r for r in returns[-20:]) / max(len(returns[-20:]), 1)) ** 0.5 * 100 if returns else 0
# 区间高低(近120日)
window = bars[-120:] if len(bars) > 120 else bars
high52 = max(b["high"] for b in window)
low52 = min(b["low"] for b in window)
# 均线多头排列
if ma5 and ma10 and ma20:
bull = ma5 > ma10 > ma20
partial = ma5 > ma10 or ma10 > ma20
else:
bull, partial = False, False
return {
"date": last["date"],
"close": last["close"],
"open": last["open"],
"high": last["high"],
"low": last["low"],
"volume": last["volume"],
"change_pct": round(chg_1d, 2),
"chg_5d": round(chg_5d, 2),
"chg_10d": round(chg_10d, 2),
"chg_20d": round(chg_20d, 2),
"ma5": round(ma5, 2) if ma5 else None,
"ma10": round(ma10, 2) if ma10 else None,
"ma20": round(ma20, 2) if ma20 else None,
"ma60": round(ma60, 2) if ma60 else None,
"rsi": round(rsi, 2),
"kdj_k": round(k, 2),
"kdj_d": round(d, 2),
"kdj_j": round(j, 2),
"dif": round(dif, 3),
"dea": round(dea, 3),
"macd": round(macd, 3),
"vol_ratio": round(vol_ratio, 2),
"volatility": round(vol20, 2),
"high_52w": round(high52, 2),
"low_52w": round(low52, 2),
"trend_bull": bull,
"trend_partial": partial,
"bars": [
{"date": b["date"], "open": b["open"], "high": b["high"],
"low": b["low"], "close": b["close"], "volume": b["volume"]}
for b in bars
],
}