784 lines
29 KiB
Python
784 lines
29 KiB
Python
"""
|
||
价格行为学(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,
|
||
} |