165 lines
4.8 KiB
Python
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
|
|
],
|
|
}
|