# -*- 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 ], }