664 lines
24 KiB
Python
664 lines
24 KiB
Python
"""
|
||
交易信号检测模块(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})<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
|