""" 交易信号检测模块(numpy向量化优化版) 实现7个交易信号:主升浪、日线底背离、龙抬头、真龙、短底背离、老鼠仓、反弹 信号按胜率排名: 1. 主升浪 85% - MACD零上金叉 2. 日线底背离 80% - 价格新低但MACD不新低(20日版) 3. 龙抬头 75% - SKDJ超跌金叉 4. 真龙 70% - 趋势启动确认 5. 短底背离 65% - 短周期底背离(10日版) 6. 老鼠仓 60% - 盘中急跌后快速回收 7. 反弹 55% - EMA3上穿EMA21 优化要点: - 所有信号检测函数使用 .values numpy原生数组替代 pandas .iloc - numpy arr[i] 访问 ~50ns,pandas iloc[i] 访问 ~5μs,提升 ~100x - 底背离函数使用 np.argmin 替代 pandas idxmin - detect_all_signals 智能跳过已是 float 的类型转换 - _check_all_signal_status 使用 numpy 数组切片替代 pandas 切片 """ import pandas as pd import numpy as np from services.technical_indicators import calc_all_indicators def detect_main_rising_wave(df, lookback=5): """ 主升浪信号(胜率85%)— numpy优化版 条件:MACD零上金叉 —— DIF和DEA都在零轴上方,DIF从下往上穿越DEA 含义:趋势走好,进入加速拉升阶段 """ signals = [] if len(df) < 30: return signals dif = df['dif'].values dea = df['dea'].values dates = df['date'].values closes = df['close'].values n = len(dif) start = max(1, n - lookback) for i in range(start, n): if dif[i] > 0 and dea[i] > 0 and dif[i - 1] <= dea[i - 1] and dif[i] > dea[i]: signals.append({ 'date': str(dates[i]), 'type': 'main_rising_wave', 'name': '主升浪', 'direction': 'buy', 'strength': 85, 'price': float(closes[i]), 'description': f"MACD零上金叉: DIF={dif[i]:.3f}, DEA={dea[i]:.3f},进入加速拉升阶段", }) return signals def detect_daily_bottom_divergence(df, lookback=5, window=20): """ 日线底背离信号(胜率80%)— numpy优化版 条件:价格创20日新低,但MACD的DIF未创对应新低 含义:真正跌透,迎来大级别反转 """ signals = [] if len(df) < window + 10: return signals close = df['close'].values.astype(np.float64) dif = df['dif'].values.astype(np.float64) dates = df['date'].values n = len(close) start = max(window, n - lookback) for i in range(start, n): window_slice = close[i - window:i + 1] curr_price = close[i] price_min = window_slice.min() if curr_price > price_min * 1.01: continue # 当前日必须是窗口内的实际最低点(等价于 idxmin() == index[-1]) if np.argmin(window_slice) == len(window_slice) - 1: dif_window = dif[i - window:i] if len(dif_window) == 0: continue dif_at_prev_lows = dif_window.min() curr_dif = dif[i] if curr_dif > dif_at_prev_lows and curr_dif < 0: signals.append({ 'date': str(dates[i]), 'type': 'daily_bottom_divergence', 'name': '日线底背离', 'direction': 'buy', 'strength': 80, 'price': float(curr_price), 'description': f"价格创{window}日新低,但MACD的DIF未创新低(DIF={curr_dif:.3f}),大级别反转信号", }) return signals def detect_dragon_head(df, lookback=5): """ 龙抬头信号(胜率75%)— numpy优化版 条件:SKDJ的K值从超跌区域(<20)发生金叉(K上穿D),且信号稳定 含义:短线起爆点,反弹稳定性强 """ signals = [] if len(df) < 20: return signals sk = df['skdj_k'].values.astype(np.float64) sd = df['skdj_d'].values.astype(np.float64) dates = df['date'].values closes = df['close'].values n = len(sk) start = max(2, n - lookback) for i in range(start, n): oversold = sk[i - 1] < 20 or sk[i] < 30 golden_cross = sk[i - 1] <= sd[i - 1] and sk[i] > sd[i] stable = True if i >= 3: recent_k = sk[i - 2:i + 1] # ddof=1 与 pandas Series.std() 保持一致 stable = np.std(recent_k, ddof=1) < 15 if oversold and golden_cross and stable: signals.append({ 'date': str(dates[i]), 'type': 'dragon_head', 'name': '龙抬头', 'direction': 'buy', 'strength': 75, 'price': float(closes[i]), 'description': f"SKDJ超跌金叉: K={sk[i]:.1f}, D={sd[i]:.1f},短线起爆点", }) return signals def detect_true_dragon(df, lookback=5): """ 真龙信号(胜率70%)— numpy优化版 条件:价格突破MA20,MA5上穿MA20(金叉),MACD柱由负转正,成交量放大 含义:中期趋势刚刚启动 """ signals = [] if len(df) < 25: return signals close = df['close'].values.astype(np.float64) ma5 = df['ma5'].values.astype(np.float64) ma20 = df['ma20'].values.astype(np.float64) macd = df['macd'].values.astype(np.float64) volume = df['volume'].values.astype(np.float64) dates = df['date'].values n = len(close) start = max(2, n - lookback) for i in range(start, n): price_above_ma20 = close[i] > ma20[i] ma5_cross_ma20 = (ma5[i - 1] <= ma20[i - 1]) and (ma5[i] > ma20[i]) ma5_above_ma20 = ma5[i] > ma20[i] macd_turn_positive = macd[i] > 0 and macd[i - 1] <= 0 vol_start = max(0, i - 10) vol_avg = volume[vol_start:i].mean() if i > vol_start else 0.0 volume_up = volume[i] > vol_avg * 1.2 if vol_avg > 0 else False conditions_met = sum([price_above_ma20, ma5_cross_ma20 or ma5_above_ma20, macd_turn_positive, volume_up]) if conditions_met >= 3 and price_above_ma20: desc_parts = [] if ma5_cross_ma20: desc_parts.append("MA5金叉MA20") if macd_turn_positive: desc_parts.append("MACD翻红") if volume_up: desc_parts.append("放量") signals.append({ 'date': str(dates[i]), 'type': 'true_dragon', 'name': '真龙', 'direction': 'buy', 'strength': 70, 'price': float(close[i]), 'description': f"趋势启动: {'+'.join(desc_parts)},中期趋势确立", }) return signals def detect_short_bottom_divergence(df, lookback=5, window=10): """ 短底背离信号(胜率65%)— numpy优化版 条件:价格创10日新低,但MACD的DIF未创对应新低 含义:小级别反弹,灵敏度高但力度偏弱 """ signals = [] if len(df) < window + 5: return signals close = df['close'].values.astype(np.float64) dif = df['dif'].values.astype(np.float64) dates = df['date'].values n = len(close) start = max(window, n - lookback) for i in range(start, n): window_slice = close[i - window:i + 1] curr_price = close[i] price_min = window_slice.min() if curr_price > price_min * 1.01: continue # 当前日必须是窗口内的实际最低点 if np.argmin(window_slice) == len(window_slice) - 1: dif_window = dif[i - window:i] if len(dif_window) == 0: continue dif_at_prev_lows = dif_window.min() curr_dif = dif[i] if curr_dif > dif_at_prev_lows: signals.append({ 'date': str(dates[i]), 'type': 'short_bottom_divergence', 'name': '短底背离', 'direction': 'buy', 'strength': 65, 'price': float(curr_price), 'description': f"价格创{window}日新低,但DIF未新低(DIF={curr_dif:.3f}),小级别反弹信号", }) return signals def detect_rat_trading(df, lookback=5): """ 老鼠仓信号(胜率60%)— numpy优化版 条件:盘中急跌(最低价大幅低于开盘价),但收盘收回(收盘价接近或高于开盘价),且成交量放大 含义:主力偷偷吸筹,上涨不具备即时性 """ signals = [] if len(df) < 10: return signals close = df['close'].values.astype(np.float64) open_p = df['open'].values.astype(np.float64) low = df['low'].values.astype(np.float64) high = df['high'].values.astype(np.float64) volume = df['volume'].values.astype(np.float64) dates = df['date'].values n = len(close) start = max(1, n - lookback) for i in range(start, n): if open_p[i] <= 0: continue drop_from_open = (low[i] - open_p[i]) / open_p[i] * 100 hl_diff = high[i] - low[i] recovery = (close[i] - low[i]) / hl_diff * 100 if hl_diff != 0 else 50.0 close_vs_open = (close[i] - open_p[i]) / open_p[i] * 100 vol_start = max(0, i - 10) vol_avg = volume[vol_start:i].mean() if i > vol_start else 0.0 volume_up = volume[i] > vol_avg * 1.3 if vol_avg > 0 else False if drop_from_open < -3 and recovery > 60 and close_vs_open > -1 and volume_up: signals.append({ 'date': str(dates[i]), 'type': 'rat_trading', 'name': '老鼠仓', 'direction': 'buy', 'strength': 60, 'price': float(close[i]), 'description': f"盘中急跌{drop_from_open:.1f}%后回收{recovery:.0f}%,放量吸筹信号", }) return signals def detect_rebound(df, lookback=5): """ 反弹信号(胜率55%)— numpy优化版 条件:EMA3从下向上穿越EMA21 含义:普通均线金叉,震荡市适用、熊市易现假反弹 """ signals = [] if len(df) < 25: return signals ema3 = df['ema3'].values.astype(np.float64) ema21 = df['ema21'].values.astype(np.float64) dates = df['date'].values closes = df['close'].values n = len(ema3) start = max(1, n - lookback) for i in range(start, n): if ema3[i - 1] <= ema21[i - 1] and ema3[i] > ema21[i]: signals.append({ 'date': str(dates[i]), 'type': 'rebound', 'name': '反弹', 'direction': 'buy', 'strength': 55, 'price': float(closes[i]), 'description': f"EMA3上穿EMA21: EMA3={ema3[i]:.2f}, EMA21={ema21[i]:.2f},均线金叉反弹", }) return signals def detect_all_signals(df, lookback=5): """ 检测所有7个交易信号(优化版) 参数: df: 包含 date, open, high, low, close, volume 列的DataFrame lookback: 向后检测的天数(默认检测最近5天) 返回: dict: { 'signals': [...], # 检测到的所有信号列表 'latest_signals': [...], # 最新一天的信号 'signal_summary': {...}, # 信号统计摘要 'indicators': {...} # 最新技术指标值 } 优化:智能类型转换,已是 float64 的列直接跳过 """ required_cols = {'date', 'open', 'high', 'low', 'close', 'volume'} if not required_cols.issubset(set(df.columns)): missing = required_cols - set(df.columns) return {'error': f'缺少必要列: {missing}', 'signals': [], 'latest_signals': []} df = df.copy() # 智能类型转换:仅在列不是 float 时才做转换(本地DB数据已是 float64,跳过) for col in ['open', 'high', 'low', 'close', 'volume']: if not np.issubdtype(df[col].dtype, np.floating): df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0).astype(np.float64) df = calc_all_indicators(df) all_signals = [] all_signals.extend(detect_main_rising_wave(df, lookback)) all_signals.extend(detect_daily_bottom_divergence(df, lookback)) all_signals.extend(detect_dragon_head(df, lookback)) all_signals.extend(detect_true_dragon(df, lookback)) all_signals.extend(detect_short_bottom_divergence(df, lookback)) all_signals.extend(detect_rat_trading(df, lookback)) all_signals.extend(detect_rebound(df, lookback)) all_signals.sort(key=lambda x: (-x['strength'], x['date']), reverse=False) latest_date = str(df['date'].values[-1]) if len(df) > 0 else '' latest_signals = [s for s in all_signals if s['date'] == latest_date] signal_summary = { 'total_signals': len(all_signals), 'latest_date': latest_date, 'latest_count': len(latest_signals), 'signal_types': {}, } for s in all_signals: t = s['type'] if t not in signal_summary['signal_types']: signal_summary['signal_types'][t] = 0 signal_summary['signal_types'][t] += 1 indicators = {} if len(df) > 0: last = df.iloc[-1] indicators = { 'macd': {'dif': round(float(last.get('dif', 0)), 4), 'dea': round(float(last.get('dea', 0)), 4), 'macd': round(float(last.get('macd', 0)), 4)}, 'skdj': {'k': round(float(last.get('skdj_k', 0)), 2), 'd': round(float(last.get('skdj_d', 0)), 2)}, 'kdj': {'k': round(float(last.get('kdj_k', 0)), 2), 'd': round(float(last.get('kdj_d', 0)), 2), 'j': round(float(last.get('kdj_j', 0)), 2)}, 'ema': {'ema3': round(float(last.get('ema3', 0)), 2), 'ema21': round(float(last.get('ema21', 0)), 2)}, 'ma': {'ma5': round(float(last.get('ma5', 0)), 2), 'ma10': round(float(last.get('ma10', 0)), 2), 'ma20': round(float(last.get('ma20', 0)), 2)}, } signal_status = _check_all_signal_status(df) return { 'signals': all_signals, 'latest_signals': latest_signals, 'signal_summary': signal_summary, 'indicators': indicators, 'signal_status': signal_status, } def _check_all_signal_status(df): """ 检查7个信号的当前状态,返回每个信号的就绪程度和说明(numpy优化版) """ if len(df) < 30: return [] n = len(df) last = df.iloc[-1] prev = df.iloc[-2] if n > 1 else last dif = float(last.get('dif', 0)) dea = float(last.get('dea', 0)) macd_val = float(last.get('macd', 0)) prev_dif = float(prev.get('dif', 0)) prev_dea = float(prev.get('dea', 0)) sk = float(last.get('skdj_k', 50)) sd = float(last.get('skdj_d', 50)) prev_sk = float(prev.get('skdj_k', 50)) prev_sd = float(prev.get('skdj_d', 50)) ema3 = float(last.get('ema3', 0)) ema21 = float(last.get('ema21', 0)) prev_ema3 = float(prev.get('ema3', 0)) prev_ema21 = float(prev.get('ema21', 0)) close = float(last.get('close', 0)) open_p = float(last.get('open', 0)) low = float(last.get('low', 0)) high = float(last.get('high', 0)) ma5 = float(last.get('ma5', 0)) ma20 = float(last.get('ma20', 0)) status = [] # 1. 主升浪 above_zero = dif > 0 and dea > 0 golden = prev_dif <= prev_dea and dif > dea triggered = bool(above_zero and golden) if triggered: desc = f"✅ 已触发!DIF={dif:.3f}>0, DEA={dea:.3f}>0, DIF上穿DEA" elif dif > 0 and dea > 0: desc = f"DIF和DEA均在零上,等待DIF上穿DEA(差值{dif-dea:.3f})" elif dif > dea: desc = f"DIF已在DEA上方,但需等待两者都转正(DIF={dif:.3f})" else: desc = f"DIF={dif:.3f}, DEA={dea:.3f},均在零下,距离触发较远" status.append({ 'type': 'main_rising_wave', 'name': '主升浪', 'strength': 85, 'triggered': triggered, 'description': desc, 'readiness': _calc_readiness(dif, dea, 'main_rising_wave') }) # 2. 日线底背离 — 使用numpy数组切片替代pandas切片 close_arr = df['close'].values.astype(np.float64) dif_arr = df['dif'].values.astype(np.float64) w20_start = max(0, n - 21) window_20 = close_arr[w20_start:] price_min_20 = float(window_20.min()) dif_w20 = dif_arr[w20_start:n - 1] if n > w20_start + 1 else dif_arr[:max(0, n - 1)] dif_min_20 = float(dif_w20.min()) if len(dif_w20) > 0 else 0.0 at_low = close <= price_min_20 * 1.01 is_actual_min = bool(np.argmin(window_20) == len(window_20) - 1) dif_diverge = dif > dif_min_20 and dif < 0 triggered = bool(at_low and is_actual_min and dif_diverge) if triggered: desc = f"✅ 已触发!价格接近20日新低,DIF({dif:.3f})高于前低({dif_min_20:.3f})" elif at_low and not is_actual_min: desc = f"价格接近20日低位({price_min_20:.2f}),但非当前最低点" elif at_low: desc = f"价格在20日低位,但DIF也在低位(DIF={dif:.3f}),暂无背离" elif dif < 0: desc = f"DIF在零下({dif:.3f}),需等待价格下探至20日新低({price_min_20:.2f})附近" else: desc = f"DIF={dif:.3f}在零上,价格距20日低点{price_min_20:.2f}较远" status.append({ 'type': 'daily_bottom_divergence', 'name': '日线底背离', 'strength': 80, 'triggered': triggered, 'description': desc, 'readiness': _calc_readiness_divergence(close, price_min_20, dif, dif_min_20) }) # 3. 龙抬头 oversold = prev_sk < 20 or sk < 30 sk_cross = prev_sk <= prev_sd and sk > sd # 稳定性检查:与检测函数一致,最近3根K值标准差 < 15 sk_arr = df['skdj_k'].values.astype(np.float64) stable = True if len(sk_arr) >= 3: # ddof=1 与 pandas Series.std() 保持一致 stable = float(np.std(sk_arr[-3:], ddof=1)) < 15 triggered = bool(oversold and sk_cross and stable) if triggered: desc = f"✅ 已触发!SKDJ超跌金叉 K={sk:.1f}, D={sd:.1f}" elif sk < 20: desc = f"K={sk:.1f}在超卖区(<20),等待K上穿D(K-D={sk-sd:.1f})" elif sk < 30: desc = f"K={sk:.1f}接近超卖区(<20),继续下探可能触发" elif sk < 50: desc = f"K={sk:.1f}在中位,距超卖区(K<20)还有较大距离" else: desc = f"K={sk:.1f}偏高,远离超卖区,不满足条件" status.append({ 'type': 'dragon_head', 'name': '龙抬头', 'strength': 75, 'triggered': triggered, 'description': desc, 'readiness': _calc_readiness_dragon(sk, sd, prev_sk, prev_sd) }) # 4. 真龙 cond_price = close > ma20 cond_ma = ma5 > ma20 cond_macd = macd_val > 0 and float(prev.get('macd', 0)) <= 0 vol_arr = df['volume'].values.astype(np.float64) vol_avg = float(vol_arr[max(0, n - 11):n - 1].mean()) if n > 10 else float(vol_arr.mean()) cond_vol = float(vol_arr[-1]) > vol_avg * 1.2 if vol_avg > 0 else False met = sum([cond_price, cond_ma, cond_macd, cond_vol]) triggered = bool(met >= 3 and cond_price) parts = [] if cond_price: parts.append(f"价格>{ma20:.2f}(MA20)✓") else: parts.append(f"价格{close:.2f}<{ma20:.2f}(MA20)✗") if cond_ma: parts.append("MA5>MA20✓") else: parts.append(f"MA5({ma5:.2f}) w10_start + 1 else dif_arr[:max(0, n - 1)] dif_min_10 = float(dif_w10.min()) if len(dif_w10) > 0 else 0.0 at_low_10 = close <= price_min_10 * 1.01 is_actual_min_10 = bool(np.argmin(window_10) == len(window_10) - 1) dif_div_10 = dif > dif_min_10 triggered = bool(at_low_10 and is_actual_min_10 and dif_div_10) if triggered: desc = f"✅ 已触发!价格接近10日新低,DIF({dif:.3f})高于前低({dif_min_10:.3f})" elif at_low_10 and not is_actual_min_10: desc = f"价格接近10日低位({price_min_10:.2f}),但非当前最低点" elif at_low_10: desc = f"价格在10日低位,但DIF也在低位,暂无背离" else: desc = f"价格距10日低点{price_min_10:.2f}尚远,等待回调" status.append({ 'type': 'short_bottom_divergence', 'name': '短底背离', 'strength': 65, 'triggered': triggered, 'description': desc, 'readiness': _calc_readiness_divergence(close, price_min_10, dif, dif_min_10) }) # 6. 老鼠仓 if open_p > 0: drop = (low - open_p) / open_p * 100 recovery = (close - low) / (high - low) * 100 if high != low else 50 close_vs_open = (close - open_p) / open_p * 100 vol_avg_10 = float(vol_arr[max(0, n - 11):n - 1].mean()) if n > 10 else float(vol_arr.mean()) vol_up = float(vol_arr[-1]) > vol_avg_10 * 1.3 if vol_avg_10 > 0 else False triggered = bool(drop < -3 and recovery > 60 and close_vs_open > -1 and vol_up) if triggered: desc = f"✅ 已触发!盘中跌{drop:.1f}%后回收{recovery:.0f}%,放量吸筹" else: parts = [] if drop >= -3: parts.append(f"盘中最大跌幅{drop:.1f}%(需<-3%)") else: parts.append(f"盘中跌{drop:.1f}%✓") if recovery <= 60: parts.append(f"回收{recovery:.0f}%(需>60%)") else: parts.append(f"回收{recovery:.0f}%✓") if not vol_up: parts.append("未放量") desc = f"{', '.join(parts)}" else: triggered = False desc = "数据异常" status.append({ 'type': 'rat_trading', 'name': '老鼠仓', 'strength': 60, 'triggered': triggered, 'description': desc, 'readiness': 0 }) # 7. 反弹 cross = prev_ema3 <= prev_ema21 and ema3 > ema21 triggered = bool(cross) gap = ema3 - ema21 gap_pct = gap / ema21 * 100 if ema21 > 0 else 0 if triggered: desc = f"✅ 已触发!EMA3({ema3:.2f})上穿EMA21({ema21:.2f})" elif ema3 < ema21: desc = f"EMA3({ema3:.2f})EMA21({ema21:.2f}),已在上方但非刚穿越" status.append({ 'type': 'rebound', 'name': '反弹', 'strength': 55, 'triggered': triggered, 'description': desc, 'readiness': _calc_readiness_rebound(ema3, ema21, prev_ema3, prev_ema21) }) return status def _calc_readiness(dif, dea, signal_type): if dif > 0 and dea > 0 and dif > dea: return 100 elif dif > 0 and dea > 0: return 70 elif dif > dea: return 40 else: return max(0, int(20 + dif * 100)) def _calc_readiness_divergence(close, price_min, dif, dif_min): price_near = close <= price_min * 1.03 dif_higher = dif > dif_min if price_near and dif_higher: return 90 elif price_near: return 50 elif dif_higher and dif < 0: return 30 return 10 def _calc_readiness_dragon(sk, sd, prev_sk, prev_sd): if sk < 20 and sk > sd and prev_sk <= prev_sd: return 100 elif sk < 20: return 70 elif sk < 30: return 40 elif sk < 50: return 20 return 5 def _calc_readiness_rebound(ema3, ema21, prev_ema3, prev_ema21): if prev_ema3 <= prev_ema21 and ema3 > ema21: return 100 gap_pct = (ema3 - ema21) / ema21 * 100 if ema21 > 0 else 0 if gap_pct > 0: return 60 elif gap_pct > -1: return 40 elif gap_pct > -3: return 20 return 5