# -*- coding: utf-8 -*- """ 推荐原因生成器:因子截面值 → 带具体数字和方向的中文推荐理由。 """ import numpy as np import pandas as pd def build_reason(code, kline_df, weights=None): """ 从单股日K线(时间升序,≥30行)生成多维度推荐原因。 返回 (reason_str, factor_detail_dict) """ if kline_df is None or len(kline_df) < 30: return '', {} close = kline_df['close'].reset_index(drop=True) vol = kline_df['volume'].reset_index(drop=True) high = kline_df['high'].reset_index(drop=True) low = kline_df['low'].reset_index(drop=True) n = len(close) c = close.iloc[-1] parts = [] # 动量 if n > 21: mom = (c / close.iloc[-21] - 1) * 100 tag = '强' if mom > 5 else ('偏强' if mom > 0 else '偏弱' if mom > -5 else '弱') parts.append(f"20日动量{mom:+.1f}%({tag})") # 趋势(MA20 偏离) ma20 = close.rolling(20).mean().iloc[-1] if ma20 and ma20 > 0: dev = (c - ma20) / ma20 * 100 tag = '强势区' if dev > 3 else ('偏高水平' if dev > 0 else '偏低水平' if dev > -3 else '弱势区') parts.append(f"距MA20 {dev:+.1f}%({tag})") # 量能 v20 = vol.rolling(20).mean().iloc[-1] if v20 and v20 > 0: vr = vol.iloc[-1] / v20 if vr > 1.5: parts.append(f"量比{vr:.1f}(放量)") elif vr < 0.5: parts.append(f"量比{vr:.1f}(极度缩量)") # RSI close_s = close delta = close_s.diff() gain = delta.clip(lower=0).rolling(14).mean() loss = (-delta.clip(upper=0)).rolling(14).mean() rs = gain / loss.replace(0, np.nan) rsi = (100 - 100 / (1 + rs)).iloc[-1] if rsi == rsi: if rsi > 70: parts.append(f"RSI={rsi:.0f}(超买)") elif rsi < 30: parts.append(f"RSI={rsi:.0f}(超卖)") # MACD ema12 = close.ewm(span=12, adjust=False).mean() ema26 = close.ewm(span=26, adjust=False).mean() dif = ema12 - ema26 dea = dif.ewm(span=9, adjust=False).mean() hist = (dif - dea).iloc[-1] if hist > 0: parts.append("MACD多头") else: parts.append("MACD空头") return ';'.join(parts), {}