""" 价格行为学(PA)引擎 — 动K/静K、供需区Q评分、连续K加速、起爆点判定 核心概念: - 动K(Dynamic Candle):实体占比>70% + 振幅>1.5×ATR → 强方向性力量 - 静K(Static Candle):实体占比<40% + 振幅<0.8×ATR → 犹豫/平衡/蓄力 - 供需区:由动K+静K群组成,Q评分≥7才是高质量区 - 起爆点:静K群后出现动K = 从蓄力到爆发的转折 - 连续K:高低点不断抬高/降低 = 趋势加速段 """ import pandas as pd import numpy as np from typing import List, Dict, Optional, Tuple from config_loader import ( dynamic_k_thresholds, static_k_thresholds, zone_params, ignition_params, continuous_k_params, exhaustion_params, entry_point_params, ) # ==================== 动K/静K识别 ==================== def classify_candles(df: pd.DataFrame, atr: float) -> List[Dict]: """ 对每根K线做动K/静K分类 返回: [{"index": i, "type": "dynamic"/"static"/"neutral", "direction": 1/-1/0, ...}] """ dynamic_body_ratio_min, dynamic_atr_ratio_min = dynamic_k_thresholds() static_body_ratio_max, static_atr_ratio_max = static_k_thresholds() results = [] for i in range(len(df)): o = float(df["open"].iloc[i]) c = float(df["close"].iloc[i]) h = float(df["high"].iloc[i]) l = float(df["low"].iloc[i]) v = float(df["volume"].iloc[i]) body = abs(c - o) total_range = h - l if total_range <= 0 or l <= 0 or atr <= 0: results.append({"index": i, "type": "neutral", "direction": 0, "body_ratio": 0, "swing_ratio": 0, "volume": v}) continue body_ratio = body / total_range # 实体占比 swing_ratio = total_range / l # 振幅/低价 ≈ 涨跌幅 atr_ratio = total_range / atr # 振幅/ATR # 动K:实体占比>70% + 振幅>1.5×ATR if body_ratio > dynamic_body_ratio_min and atr_ratio > dynamic_atr_ratio_min: direction = 1 if c > o else -1 results.append({"index": i, "type": "dynamic", "direction": direction, "body_ratio": body_ratio, "swing_ratio": swing_ratio, "atr_ratio": atr_ratio, "volume": v}) # 静K:实体占比<40% + 振幅<0.8×ATR elif body_ratio < static_body_ratio_max and atr_ratio < static_atr_ratio_max: results.append({"index": i, "type": "static", "direction": 0, "body_ratio": body_ratio, "swing_ratio": swing_ratio, "atr_ratio": atr_ratio, "volume": v}) else: direction = 1 if c > o else -1 if c < o else 0 results.append({"index": i, "type": "neutral", "direction": direction, "body_ratio": body_ratio, "swing_ratio": swing_ratio, "atr_ratio": atr_ratio, "volume": v}) return results def calc_atr(df: pd.DataFrame, period: int = 14) -> float: """计算ATR""" if df is None or len(df) < period + 1: return 0.0 tr = pd.concat([ df["high"] - df["low"], abs(df["high"] - df["close"].shift(1)), abs(df["low"] - df["close"].shift(1)), ], axis=1).max(axis=1) atr = tr.rolling(period).mean().iloc[-1] return float(atr) if not pd.isna(atr) else 0.0 # ==================== 供需区 + Q评分 ==================== def find_supply_demand_zones(df: pd.DataFrame, candles_class: List[Dict], atr: float, lookback: int = 50) -> List[Dict]: """ 识别结构化供需区(动K+静K群+动K确认)并计算Q评分 供给区模式:阴动K(方向-1) → 静K群(2-5根) → 阳动K确认(方向+1)离开 需求区模式:阳动K(方向+1) → 静K群(2-5根) → 阴动K确认(方向-1)离开 返回: [{"type": "supply"/"demand", "top": float, "btm": float, "q_score": float, "freshness": int, ...}] """ cfg_lookback, min_static_count, _ = zone_params() lookback = cfg_lookback if cfg_lookback else lookback zones = [] if not candles_class or atr <= 0: return zones # 只看最近lookback根K线 start_idx = max(0, len(candles_class) - lookback) for i in range(start_idx, len(candles_class)): c = candles_class[i] if c["type"] != "dynamic": continue # 检查后面是否有静K群+确认动K base_dir = c["direction"] # 起始动K方向 # 寻找静K群(紧随起始动K之后的2-5根静K) static_start = i + 1 static_count = 0 static_end = static_start for j in range(static_start, min(static_start + 5, len(candles_class))): if candles_class[j]["type"] == "static": static_count += 1 static_end = j + 1 else: break if static_count < min_static_count: # 需要至少N根静K continue # 寻找确认动K(静K群之后的动K) confirm_idx = None for j in range(static_end, min(static_end + 3, len(candles_class))): if candles_class[j]["type"] == "dynamic": confirm_idx = j break if confirm_idx is None: continue confirm_dir = candles_class[confirm_idx]["direction"] # 判断区类型 # 供给区:起始是阴动K,确认是阳动K离开向上 # 需求区:起始是阳动K,确认是阴动K离开向下 if base_dir == -1 and confirm_dir == 1: zone_type = "supply" elif base_dir == 1 and confirm_dir == -1: zone_type = "demand" else: continue # 方向不匹配 # 计算区的范围 # 供给区:从静K群最低点到起始动K高点 # 需求区:从起始动K低点到静K群最高点 region_indices = [i] + list(range(static_start, static_end)) region_highs = [float(df["high"].iloc[ri]) for ri in region_indices] region_lows = [float(df["low"].iloc[ri]) for ri in region_indices] if zone_type == "supply": zone_top = max(region_highs) zone_btm = min(region_lows) else: zone_top = max(region_highs) # 需求区top = 阳动K高点+静K高点 zone_btm = min(region_lows) # 需求区btm = 最低点 zone_height = zone_top - zone_btm if zone_height <= 0: continue # 离去强度:确认动K的离开幅度 / 区高度 confirm_price = float(df["close"].iloc[confirm_idx]) if zone_type == "supply": leave_dist = confirm_price - zone_top # 离开供给区向上 else: leave_dist = zone_btm - confirm_price # 离开需求区向下 leave_ratio = abs(leave_dist) / zone_height if zone_height > 0 else 0 # Q评分(0-10分) q_score = _calc_q_score( zone_type=zone_type, base_speed=static_count, # 静K数量(基底速度) freshness=_calc_freshness(df, zone_top, zone_btm, i, zone_type), age=len(candles_class) - i, # K线年龄 leave_ratio=leave_ratio, reaction_strength=_calc_reaction_strength(df, zone_type, zone_top, zone_btm, i), ) zones.append({ "type": zone_type, "top": round(zone_top, 6), "btm": round(zone_btm, 6), "q_score": q_score, "leave_ratio": round(leave_ratio, 2), "base_index": i, "confirm_index": confirm_idx, "static_count": static_count, "age": len(candles_class) - i, }) # 去重:同价位区域保留Q评分最高的 deduped = {} for z in zones: key = (z["type"], round(z["btm"], 4), round(z["top"], 4)) if key not in deduped or z["q_score"] > deduped[key]["q_score"]: deduped[key] = z return sorted(deduped.values(), key=lambda z: z["q_score"], reverse=True) def _calc_q_score(zone_type, base_speed, freshness, age, leave_ratio, reaction_strength): """ Q质量评分(0-10分) 维度:基底速度(0-2) + 新鲜度(0-2) + 年龄(0-1) + 离去强度(0-3) + 回踩反应(0-2) """ _, _, q_score_breakpoints = zone_params() base_speed_bp = q_score_breakpoints.get("base_speed", [2, 3, 5]) freshness_bp = q_score_breakpoints.get("freshness", [0, 2]) age_bp = q_score_breakpoints.get("age", [20, 40]) leave_ratio_bp = q_score_breakpoints.get("leave_ratio", [1.0, 1.5, 2.5, 3.0]) score = 0.0 # 1. 基底速度(静K越少越好,说明蓄力时间短、爆发快) if base_speed <= base_speed_bp[0]: score += 2 elif base_speed <= base_speed_bp[1]: score += 1.5 elif base_speed <= base_speed_bp[2]: score += 1 else: score += 0 # 2. 新鲜度(未被触碰的次数越少越好) if freshness <= freshness_bp[0]: score += 2 elif freshness <= freshness_bp[1]: score += 1 else: score += 0 # 3. 年龄(越近越好) if age <= age_bp[0]: score += 1 elif age <= age_bp[1]: score += 0.5 else: score += 0 # 4. 离去强度(离开幅度/区域高度 ≥2.5倍=最强) if leave_ratio >= leave_ratio_bp[3]: score += 3 elif leave_ratio >= leave_ratio_bp[2]: score += 2.5 elif leave_ratio >= leave_ratio_bp[1]: score += 2 elif leave_ratio >= leave_ratio_bp[0]: score += 1 else: score += 0 # 5. 回踩反应(回踩后快速离开=强反应) score += min(reaction_strength, 2) return round(score, 1) def _calc_freshness(df, zone_top, zone_btm, base_index, zone_type): """计算区域被触碰次数(新鲜度)""" touches = 0 price = float(df["close"].iloc[-1]) for j in range(base_index + 1, len(df)): h = float(df["high"].iloc[j]) l = float(df["low"].iloc[j]) if zone_type == "supply" and l <= zone_top: touches += 1 elif zone_type == "demand" and h >= zone_btm: touches += 1 return touches def _calc_reaction_strength(df, zone_type, zone_top, zone_btm, base_index): """计算回踩后反应强度""" if base_index + 6 >= len(df): return 0 # 看回踩后2根K线是否快速离开区域 reaction = 0 for j in range(base_index + 2, min(base_index + 8, len(df))): c = float(df["close"].iloc[j]) if zone_type == "supply": # 回踩供给区后是否快速远离(向下远离=强反应) dist = (zone_btm - c) / (zone_top - zone_btm) if (zone_top - zone_btm) > 0 else 0 reaction = max(reaction, max(0, dist)) else: # 回踩需求区后是否快速远离(向上远离=强反应) dist = (c - zone_top) / (zone_top - zone_btm) if (zone_top - zone_btm) > 0 else 0 reaction = max(reaction, max(0, dist)) # 映射到0-2分 if reaction >= 2.0: return 2 elif reaction >= 1.0: return 1 elif reaction >= 0.5: return 0.5 return 0 # ==================== 连续K识别(趋势加速) ==================== def find_continuous_k(df: pd.DataFrame) -> List[Dict]: """ 识别连续K线组合(高低点不断抬高的阳线群/不断降低的阴线群) 对应TV的is_continue_hl逻辑 返回: [{"type": "bullish_continue"/"bearish_continue", "length": int, "start_index": int, "strength": float}] """ min_count = continuous_k_params().get("min_count", 3) results = [] if len(df) < min_count: return results closes = df["close"].values opens = df["open"].values highs = df["high"].values lows = df["low"].values # 从最新K线往回找连续模式 i = len(df) - 1 # 多头连续(阳线+高点抬高+低点抬高) bull_count = 0 bull_start = i for j in range(i, -1, -1): if closes[j] > opens[j]: # 阳线 if j < i: if lows[j] < lows[j+1] and highs[j] < highs[j+1]: # 低点抬高 + 高点抬高 = 多头连续 bull_count += 1 bull_start = j else: break else: bull_count = 1 bull_start = j else: break if bull_count >= min_count: strength = sum(highs[k] - lows[k] for k in range(bull_start, i+1)) / df["close"].iloc[-1] results.append({ "type": "bullish_continue", "length": bull_count, "start_index": bull_start, "strength": round(float(strength), 4), }) # 空头连续(阴线+高点降低+低点降低) bear_count = 0 bear_start = i for j in range(i, -1, -1): if closes[j] < opens[j]: # 阴线 if j < i: if lows[j] > lows[j+1] and highs[j] > highs[j+1]: bear_count += 1 bear_start = j else: break else: bear_count = 1 bear_start = j else: break if bear_count >= min_count: strength = sum(highs[k] - lows[k] for k in range(bear_start, i+1)) / float(df["close"].iloc[-1]) results.append({ "type": "bearish_continue", "length": bear_count, "start_index": bear_start, "strength": round(float(strength), 4), }) return results # ==================== 起爆点判定 ==================== def detect_ignition_point(candles_class: List[Dict], df: pd.DataFrame, atr: float, min_static_count: int = 2) -> List[Dict]: """ 检测起爆点:静K群后出现动K = 从蓄力到爆发的转折 条件: 1. 连续min_static_count根静K(蓄力段) 2. 紧随其后出现动K(方向性爆发) 3. 动K实体 > 静K群平均实体 × 3(爆发力量显著) 返回: [{"index": int, "direction": 1/-1, "strength": float, "static_before": int, "signal_type": str}] """ lookback, cfg_min_static_count, static_search_range, _ = ignition_params() min_static_count = cfg_min_static_count if cfg_min_static_count else min_static_count ignitions = [] if not candles_class or atr <= 0: return ignitions start_idx = max(0, len(candles_class) - lookback) # 只看最近N根 for i in range(start_idx, len(candles_class)): c = candles_class[i] if c["type"] != "dynamic": continue # 检查前面是否有静K群 static_count = 0 for j in range(i - 1, max(i - (static_search_range + 1), start_idx - 1), -1): if candles_class[j]["type"] == "static": static_count += 1 else: break if static_count < min_static_count: continue # 检查爆发强度:动K实体 vs 静K群平均实体 dynamic_body = abs(float(df["close"].iloc[i]) - float(df["open"].iloc[i])) # 静K群平均实体 static_bodies = [] for j in range(i - static_count, i): if candles_class[j]["type"] == "static": sb = abs(float(df["close"].iloc[j]) - float(df["open"].iloc[j])) static_bodies.append(sb) avg_static_body = sum(static_bodies) / len(static_bodies) if static_bodies else 0 # 爆发强度 = 动K实体 / 静K平均实体 if avg_static_body <= 0: strength_ratio = dynamic_body / atr if atr > 0 else 0 else: strength_ratio = dynamic_body / avg_static_body # 动K方向 direction = c["direction"] # 信号类型命名 if direction == 1: signal_type = "起爆点↑(静K→阳动K)" elif direction == -1: signal_type = "起爆点↓(静K→阴动K)" else: continue age_bars = len(candles_class) - 1 - i ignitions.append({ "index": i, "direction": direction, "strength_ratio": round(strength_ratio, 2), "static_before": static_count, "dynamic_atr_ratio": round(c["atr_ratio"], 2), "signal_type": signal_type, "age_bars": age_bars, }) return ignitions # ==================== 15min入场点分析 ==================== def analyze_entry_point(h1_df: pd.DataFrame, m15_df: pd.DataFrame, atr_1h: float, zones_4h: List[Dict], direction: int = 1) -> Dict: """ 15min级别入场点分析 方向=1(做多):寻找回踩到需求区+出现静K确认 = 最佳买点 方向=-1(做空):寻找反弹到供给区+出现静K确认 = 最佳卖点 返回: { "action": "即刻买入"/"等回踩"/"放弃", "entry_type": str, "wait_price": float, # 等回踩的目标价位 "confidence": float, # 0-1 "reason": str, "breakout_k_info": dict, # 突破K线信息 "pullback_info": dict, # 回踩信息 "false_breakout": bool, # 是否假突破 } """ if m15_df is None or len(m15_df) < 20 or atr_1h <= 0: return {"action": "放弃", "entry_type": "数据不足", "wait_price": 0, "confidence": 0, "reason": "15min数据不足", "false_breakout": False} entry_cfg = entry_point_params() recent_15min = entry_cfg.get("recent_15min", 6) dy_bear_max = entry_cfg.get("dy_bear_max", 3) price_1h = float(h1_df["close"].iloc[-1]) if h1_df is not None else 0 atr_15 = calc_atr(m15_df, 14) m15_class = classify_candles(m15_df, atr_15) # 找最近2-3根15min的突破K线 recent_15 = m15_class[-recent_15min:] # 最近N根15min breakout_k = None pullback_k = None false_breakout = False # 1. 找突破K线:必须是最近/上一根15min内发生,避免把1小时前突破当作“正在突破” max_breakout_age = entry_cfg.get("max_breakout_age_bars", 1) for c in reversed(recent_15): age_bars = len(m15_class) - 1 - c.get("index", -1) if age_bars > max_breakout_age: continue if c["type"] == "dynamic" and c["direction"] == direction: breakout_k = c breakout_k["age_bars"] = age_bars break # 2. 突破后是否有回踩(静K或neutral+小实体) if breakout_k: bk_idx = breakout_k["index"] # 看突破K线之后的K线 for c in m15_class[bk_idx+1:]: if c["type"] == "static" or (c["type"] == "neutral" and c["body_ratio"] < 0.35): pullback_k = c break # 3. 假突破检测:突破后立刻回落跌破突破K线低点(做多)/突破K线高点(做空) if breakout_k: bk_low = float(m15_df["low"].iloc[breakout_k["index"]]) bk_high = float(m15_df["high"].iloc[breakout_k["index"]]) current_price = float(m15_df["close"].iloc[-1]) if direction == 1 and current_price < bk_low: false_breakout = True elif direction == -1 and current_price > bk_high: false_breakout = True # 4. 判断入场类型 if false_breakout: return { "action": "放弃", "entry_type": "假突破", "wait_price": 0, "confidence": 0, "reason": "突破后回落,假突破信号", "breakout_k_info": {"index": breakout_k["index"] if breakout_k else -1, "atr_ratio": breakout_k["atr_ratio"] if breakout_k else 0}, "pullback_info": {}, "false_breakout": True, } # 5. 找目标回踩价位(4H需求区或1H均线支撑) wait_price = 0 if direction == 1: # 做多:找最近的4H需求区top作为回踩目标 demand_zones = [z for z in zones_4h if z["type"] == "demand" and z["q_score"] >= 7] if demand_zones: # 价格还在需求区上方 → 回踩到需求区top是最佳买点 nearest_demand = min(demand_zones, key=lambda z: abs(z["top"] - price_1h)) if nearest_demand["top"] < price_1h: wait_price = round(nearest_demand["top"], 6) else: # 无高质量需求区,用1H MA20作为回踩目标 if h1_df is not None and len(h1_df) >= 20: ma20_1h = float(h1_df["close"].rolling(20).mean().iloc[-1]) if ma20_1h < price_1h: wait_price = round(ma20_1h, 6) elif direction == -1: # 做空:找最近的4H供给区btm作为反弹目标 supply_zones = [z for z in zones_4h if z["type"] == "supply" and z["q_score"] >= 7] if supply_zones: nearest_supply = min(supply_zones, key=lambda z: abs(z["btm"] - price_1h)) if nearest_supply["btm"] > price_1h: wait_price = round(nearest_supply["btm"], 6) # 6. 综合判断(v11纯前瞻版 — 用PA行为替代RSI/布林) current_price_15 = float(m15_df["close"].iloc[-1]) # 即刻买入条件:15min动K连续 + 无连续阴动K超买反转 if breakout_k and not pullback_k: # 突破正在发生,没有回踩 — PA判断:15min近6根中阴动K<3即非超买反转 candles_15 = classify_candles(m15_df, calc_atr(m15_df, 14)) recent_15 = candles_15[-recent_15min:] if len(candles_15) >= recent_15min else candles_15 dy_bear_15 = sum(1 for c in recent_15 if c["type"] == "dynamic" and c["direction"] == -1) no_overbought_reversal = dy_bear_15 < dy_bear_max if no_overbought_reversal: return { "action": "即刻买入", "entry_type": "突破进行中(15min动K连续)", "wait_price": round(current_price_15, 6), "confidence": 0.8, "reason": f"15min突破K线正在发生,近{recent_15min}根阴动K={dy_bear_15}<{dy_bear_max}(无超买反转)", "breakout_k_info": {"index": breakout_k["index"], "atr_ratio": round(breakout_k["atr_ratio"], 2), "direction": breakout_k["direction"], "age_bars": breakout_k.get("age_bars", 0)}, "pullback_info": {}, "false_breakout": False, } # 等回踩条件:突破后出现静K回踩 if breakout_k and pullback_k: pb_idx = pullback_k["index"] pb_low = float(m15_df["low"].iloc[pb_idx]) pb_high = float(m15_df["high"].iloc[pb_idx]) return { "action": "等回踩", "entry_type": "突破后回踩确认", "wait_price": wait_price if wait_price > 0 else round(pb_low, 6), "confidence": 0.6, "reason": f"突破后15min出现静K回踩,等价格回到${wait_price or pb_low:.4f}附近入场", "breakout_k_info": {"index": breakout_k["index"], "atr_ratio": round(breakout_k["atr_ratio"], 2)}, "pullback_info": {"index": pb_idx, "low": round(pb_low, 6), "high": round(pb_high, 6)}, "false_breakout": False, } # 放弃条件:15min出现连续阴动K(PA超买反转信号) candles_15_all = classify_candles(m15_df, calc_atr(m15_df, 14)) recent_15_all = candles_15_all[-recent_15min:] if len(candles_15_all) >= recent_15min else candles_15_all dy_bear_15_all = sum(1 for c in recent_15_all if c["type"] == "dynamic" and c["direction"] == -1) if dy_bear_15_all >= dy_bear_max: return { "action": "放弃", "entry_type": "超买反转(PA)", "wait_price": 0, "confidence": 0, "reason": f"15min近{recent_15min}根阴动K={dy_bear_15_all}≥{dy_bear_max}(超买反转信号)", "false_breakout": False, } # 默认:HTF已确认多头,15min无明显突破K但也不存在反转信号 # → 不再强制等回踩(37%失败率已被数据证实),改为参考当前价即刻入场 # 由上层质量闸门对极端追高做最后拦截 return { "action": "即刻买入", "entry_type": "HTF趋势延续(15min无反转)", "wait_price": round(current_price_15, 6), "confidence": 0.5, "reason": f"1H/4H已确认多头,15min无动K突破但阴动K={dy_bear_15_all}<{dy_bear_max}(无反转),即刻入场", "breakout_k_info": {}, "pullback_info": {}, "false_breakout": False, } # ==================== 趋势衰减检测 ==================== def detect_trend_exhaustion(h1_df: pd.DataFrame, atr_1h: float) -> Dict: """ 检测1H级别趋势衰减信号(v11纯前瞻版 — 已删除MACD/RSI) 衰减信号(纯PA行为): - 连续2+根静K(方向性力量减弱) - 1H放量阴线×2(多头出货) - 1H连续K空头加速(趋势加速下行) - 1H连续≥3根阴动K(趋势反转) - 放量滞涨(量放大但价格不涨) 返回: {"exhausted": bool, "signals": [...], "severity": "low"/"medium"/"high"} """ if h1_df is None or len(h1_df) < 30 or atr_1h <= 0: return {"exhausted": False, "signals": [], "severity": "low"} ex_cfg = exhaustion_params() recent_static_count = ex_cfg.get("recent_static_count", 3) recent_static_threshold = ex_cfg.get("recent_static_threshold", 2) high_vol_bear_threshold = ex_cfg.get("high_vol_bear_threshold", 2) dy_bear_threshold = ex_cfg.get("dy_bear_threshold", 3) vol_ratio_stagnation = ex_cfg.get("vol_ratio_stagnation", 2.0) price_change_stagnation_pct = ex_cfg.get("price_change_stagnation", 0.005) * 100 min_count = continuous_k_params().get("min_count", 3) signals = [] severity = "low" candles = classify_candles(h1_df, atr_1h) # 1. 最近2根是否是静K(方向性力量减弱) recent_types = [c["type"] for c in candles[-recent_static_count:]] static_count_recent = sum(1 for t in recent_types if t == "static") if static_count_recent >= recent_static_threshold: signals.append(f"1H连续{static_count_recent}静K(方向性减弱)") severity = "medium" # 2. 1H放量阴线×2(多头出货) — 替代MACD柱收缩 vol_avg = float(h1_df["volume"].rolling(20).mean().iloc[-1]) recent_3 = h1_df.tail(3) high_vol_bear = 0 for _, row in recent_3.iterrows(): vol_r = row["volume"] / vol_avg if vol_avg > 0 else 0 if vol_r >= vol_ratio_stagnation and row["close"] < row["open"]: high_vol_bear += 1 if high_vol_bear >= high_vol_bear_threshold: signals.append(f"1H放量阴线×{high_vol_bear}(多头出货)") severity = "medium" if severity == "low" else severity # 3. 1H连续K空头加速 — 替代RSI回落 cont_k = find_continuous_k(h1_df) for ck in cont_k: if ck["type"] == "bearish_continue" and ck["length"] >= min_count: signals.append(f"1H连续{ck['length']}K空头加速") severity = "medium" if severity == "low" else severity # 4. 1H连续≥3根阴动K(趋势反转) — 替代MACD死叉 recent_candles = candles[-6:] if len(candles) >= 6 else candles dy_bear_count = sum(1 for c in recent_candles if c["type"] == "dynamic" and c["direction"] == -1) if dy_bear_count >= dy_bear_threshold: signals.append(f"1H连续{dy_bear_count}根阴动K(趋势反转)") severity = "high" # 5. 放量滞涨(PA行为) vol_latest = float(h1_df["volume"].iloc[-1]) vol_ratio = vol_latest / vol_avg if vol_avg > 0 else 1 price_change_latest = (float(h1_df["close"].iloc[-1]) / float(h1_df["close"].iloc[-2]) - 1) * 100 if vol_ratio >= vol_ratio_stagnation and abs(price_change_latest) < price_change_stagnation_pct: signals.append(f"1H放量滞涨(量{vol_ratio:.1f}倍但涨{price_change_latest:.2f}%)") severity = "high" exhausted = severity != "low" return { "exhausted": exhausted, "signals": signals, "severity": severity, } # ==================== 综合PA分析 ==================== def full_pa_analysis(df: pd.DataFrame, timeframe: str = "4h") -> Dict: """ 对单币单时间框架做完整PA分析 返回: { "candles_class": [...], "zones": [...], "continuous_k": [...], "ignition_points": [...], "atr": float, "trend_exhaustion": {...}, } """ atr = calc_atr(df, 14) if atr <= 0 or len(df) < 30: return {"candles_class": [], "zones": [], "continuous_k": [], "ignition_points": [], "atr": 0, "trend_exhaustion": {"exhausted": False, "signals": [], "severity": "low"}} candles_class = classify_candles(df, atr) zones = find_supply_demand_zones(df, candles_class, atr) continuous_k = find_continuous_k(df) ignition_points = detect_ignition_point(candles_class, df, atr) # 只在1H级别检测趋势衰减 exhaustion = {} if timeframe == "1h": exhaustion = detect_trend_exhaustion(df, atr) else: exhaustion = {"exhausted": False, "signals": [], "severity": "low"} return { "candles_class": candles_class, "zones": zones, "continuous_k": continuous_k, "ignition_points": ignition_points, "atr": round(atr, 6), "trend_exhaustion": exhaustion, }