Initial commit

This commit is contained in:
freedakgmail
2026-07-17 18:49:07 +08:00
commit 9c7d7abdd4
100 changed files with 41337 additions and 0 deletions
+663
View File
@@ -0,0 +1,663 @@
"""
交易信号检测模块(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] 访问 ~50nspandas 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上穿DK-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})<MA20({ma20:.2f})✗")
if cond_macd:
parts.append("MACD翻红✓")
else:
parts.append(f"MACD={macd_val:.3f}")
if cond_vol:
parts.append("放量✓")
else:
parts.append("未放量✗")
if triggered:
desc = f"✅ 已触发!{met}/4条件满足: {', '.join(parts)}"
else:
desc = f"{met}/4条件(需≥3: {', '.join(parts)}"
status.append({
'type': 'true_dragon', 'name': '真龙', 'strength': 70,
'triggered': triggered, 'description': desc,
'readiness': min(100, met * 25) if cond_price else min(50, met * 15)
})
# 5. 短底背离
w10_start = max(0, n - 11)
window_10 = close_arr[w10_start:]
price_min_10 = float(window_10.min())
dif_w10 = dif_arr[w10_start:n - 1] if n > 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}),差{abs(gap_pct):.2f}%,等待上穿"
else:
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