#!/usr/bin/env python3 """indicators.py — 通用技术指标库(2026-08-11 从 mr_scanner 抽取) 背景:calc_ma/calc_rsi/calc_atr/calc_obv 原定义在 mr_scanner.py, 被 s2_scanner.py import 复用 —— 策略扫描器互相 import 工具函数是坏味道。 抽取到公共模块,供所有策略扫描器(mr/s2/accumulation/p_oversold)共用。 算法与 backtest_framework.py 完全一致(零偏差)。 """ def calc_ma(series, n): """简单移动平均。前 n-1 位返回 None。""" result = [] for i in range(len(series)): if i < n - 1: result.append(None) else: result.append(sum(series[i - n + 1:i + 1]) / n) return result def calc_rsi(series, n=14): """RSI(相对强弱指数),Wilder 平滑。""" deltas = [series[i] - series[i - 1] for i in range(1, len(series))] gains = [d if d > 0 else 0 for d in deltas] losses = [-d if d < 0 else 0 for d in deltas] result = [None] * (n + 1) avg_gain = sum(gains[:n]) / n avg_loss = sum(losses[:n]) / n if avg_loss == 0: result.append(100) else: rs = avg_gain / avg_loss result.append(100 - 100 / (1 + rs)) for i in range(n, len(gains)): avg_gain = (avg_gain * (n - 1) + gains[i]) / n avg_loss = (avg_loss * (n - 1) + losses[i]) / n if avg_loss == 0: result.append(100) else: rs = avg_gain / avg_loss result.append(100 - 100 / (1 + rs)) while len(result) < len(series): result.insert(0, None) return result[:len(series)] def calc_atr(klines, n=14): """ATR(Average True Range),与回测 calc_factors 的 atr_pct 同口径。 klines: [{high, low, close, ...}]""" if len(klines) < n + 1: return None trs = [] for i in range(1, len(klines)): h, l, pc = klines[i]["high"], klines[i]["low"], klines[i - 1]["close"] tr = max(h - l, abs(h - pc), abs(l - pc)) trs.append(tr) atr = sum(trs[-n:]) / n return atr def calc_obv(klines): """OBV 能量潮(近20日变化量),资金流向指标。 klines: [{close, volume, ...}]""" if len(klines) < 21: return 0 obv = 0 for i in range(1, len(klines)): if klines[i]["close"] > klines[i - 1]["close"]: obv += klines[i]["volume"] * 100 # 手→股 elif klines[i]["close"] < klines[i - 1]["close"]: obv -= klines[i]["volume"] * 100 # 近20日 OBV 变化 obv_now = 0 for i in range(max(1, len(klines) - 20), len(klines)): if klines[i]["close"] > klines[i - 1]["close"]: obv_now += klines[i]["volume"] * 100 elif klines[i]["close"] < klines[i - 1]["close"]: obv_now -= klines[i]["volume"] * 100 return obv_now