feat(strategy-management): exit_levels.py - 港美股做T 出场点位算法 (混合公式)
方法 2 (百分比波动率) + 方法 3 (关键价位) 混合算法 feat(intraday-regime-detector): 新 skill - 日内市场状态判别 来源: DeepSeek chat share 26iikphv8h94feze9q 核心: R² + ADF + 历史波动率, 识别趋势市 / 震荡市 / 混乱 推荐: 趋势跟踪 / 网格交易 / 布林带回归 / NO_TRADE
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
exit_levels.py - 港美股日内做T 出场点位计算 (混合公式)
|
||||
|
||||
设计: 方法 2 (百分比波动率) + 方法 3 (关键价位) 混合
|
||||
- 关键位: day_high/low / prev_high/low / VWAP (从 indicators.py)
|
||||
- 波动率: ATR (从 indicators.py)
|
||||
- R:R 强制下限 1.5, 不达标信号否决
|
||||
|
||||
⚠️ 这是港美股做T专用, 币圈用 crypto-t-monitor 的 ATR 公式 (独立)
|
||||
|
||||
用法:
|
||||
from exit_levels import calc_exit_levels
|
||||
|
||||
result = calc_exit_levels(
|
||||
entry=100.0,
|
||||
atr=5.0,
|
||||
current_price=100.0,
|
||||
day_high=103.0,
|
||||
day_low=97.0,
|
||||
prev_high=106.0,
|
||||
prev_low=94.0,
|
||||
vwap=101.0,
|
||||
side='long',
|
||||
min_rr=1.5,
|
||||
)
|
||||
if result is None:
|
||||
print("信号否决: R:R 不达标")
|
||||
else:
|
||||
sl, tp1, tp2 = result
|
||||
print(f"SL={sl} TP1={tp1} TP2={tp2}")
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Literal
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExitLevels:
|
||||
"""出场点位结果"""
|
||||
sl: float # 止损价位
|
||||
tp1: float # 第一止盈 (半平)
|
||||
tp2: float # 第二止盈 (全平)
|
||||
entry: float # 入场价 (回填, 方便调用方记录)
|
||||
side: str # 'long' / 'short'
|
||||
risk: float # 风险 (entry - SL)
|
||||
reward: float # 奖励 (TP1 - entry)
|
||||
rr_ratio: float # R:R (reward/risk)
|
||||
sl_method: str # 'vol_pct' | 'key_level' | 'hybrid'
|
||||
tp_method: str # 'vol_pct' | 'key_level' | 'hybrid'
|
||||
note: str = "" # 备注 (VWAP 锁定等)
|
||||
|
||||
|
||||
def calc_exit_levels(
|
||||
entry: float,
|
||||
atr: float,
|
||||
current_price: float,
|
||||
day_high: Optional[float] = None,
|
||||
day_low: Optional[float] = None,
|
||||
prev_high: Optional[float] = None,
|
||||
prev_low: Optional[float] = None,
|
||||
vwap: Optional[float] = None,
|
||||
side: Literal['long', 'short'] = 'long',
|
||||
min_rr: float = 1.5,
|
||||
# 方法 2 权重 (百分比波动率)
|
||||
vol_sl_multi: float = 1.0, # SL 距离 = vol × sl_multi
|
||||
vol_tp1_multi: float = 2.0, # TP1 距离 = vol × tp1_multi (默认 2.0 倍 SL → R:R 2:1)
|
||||
vol_tp2_multi: float = 3.0, # TP2 距离 = vol × tp2_multi
|
||||
# 方法 3 权重 (关键位) - 离关键位的 buffer
|
||||
key_level_buffer_pct: float = 0.001, # 0.1% 缓冲 (避免瞬时触发)
|
||||
) -> Optional[ExitLevels]:
|
||||
"""
|
||||
计算 SL/TP1/TP2 (混合公式: 波动率 + 关键位)
|
||||
|
||||
Args:
|
||||
entry: 入场价 (假设已知, 或用 adjust_to_ask1 拿到的价)
|
||||
atr: 当前 K 线 ATR (14 周期, 从 indicators.atr())
|
||||
current_price: 当前实时价 (用于 vol_pct 计算)
|
||||
day_high/low: 今日最高/最低 (从 longbridge quote)
|
||||
prev_high/low: 昨日最高/最低 (从 longbridge K 线)
|
||||
vwap: 成交量加权平均价 (从 indicators.vwap())
|
||||
side: 'long' (做多) / 'short' (做空)
|
||||
min_rr: 最小 R:R (默认 1.5)
|
||||
vol_sl_multi / tp1_multi / tp2_multi: ATR 倍数
|
||||
key_level_buffer_pct: 关键位 buffer (避免价格精确等于关键位)
|
||||
|
||||
Returns:
|
||||
ExitLevels 或 None (R:R 不达标)
|
||||
|
||||
Examples:
|
||||
>>> calc_exit_levels(entry=100, atr=5, current_price=100,
|
||||
... day_high=115, day_low=92, prev_high=118, prev_low=90,
|
||||
... vwap=105, side='long')
|
||||
ExitLevels(sl=91.2, tp1=114.2, tp2=120.0, ...)
|
||||
"""
|
||||
if entry <= 0 or atr <= 0 or current_price <= 0:
|
||||
raise ValueError("entry/atr/current_price must be > 0")
|
||||
|
||||
# === 方法 2: 百分比波动率 (基于 ATR) ===
|
||||
vol_pct = atr / current_price
|
||||
vol_sl_dist = vol_pct * vol_sl_multi
|
||||
vol_tp1_dist = vol_pct * vol_tp1_multi
|
||||
vol_tp2_dist = vol_pct * vol_tp2_multi
|
||||
|
||||
if side == 'long':
|
||||
sl_vol = entry * (1 - vol_sl_dist)
|
||||
tp1_vol = entry * (1 + vol_tp1_dist)
|
||||
tp2_vol = entry * (1 + vol_tp2_dist)
|
||||
else:
|
||||
sl_vol = entry * (1 + vol_sl_dist)
|
||||
tp1_vol = entry * (1 - vol_tp1_dist)
|
||||
tp2_vol = entry * (1 - vol_tp2_dist)
|
||||
|
||||
# === 方法 3: 关键价位 ===
|
||||
# 多仓: SL 取 entry **下方**的支撑; TP1 取 entry **上方**的阻力
|
||||
# 空仓: SL 取 entry **上方**的阻力; TP1 取 entry **下方**的支撑
|
||||
sl_key = None
|
||||
tp1_key = None
|
||||
note = ""
|
||||
|
||||
if side == 'long':
|
||||
# SL 关键位 (entry 下方)
|
||||
sl_candidates = []
|
||||
if day_low is not None and day_low < entry:
|
||||
sl_candidates.append(day_low * (1 - key_level_buffer_pct))
|
||||
if prev_low is not None and prev_low < entry:
|
||||
sl_candidates.append(prev_low * (1 - key_level_buffer_pct))
|
||||
if vwap is not None and vwap < entry:
|
||||
sl_candidates.append(vwap * (1 - key_level_buffer_pct))
|
||||
sl_key = max(sl_candidates) if sl_candidates else None
|
||||
|
||||
# TP1 关键位 (entry 上方)
|
||||
tp1_candidates = []
|
||||
if day_high is not None and day_high > entry:
|
||||
tp1_candidates.append(day_high * (1 - key_level_buffer_pct))
|
||||
if prev_high is not None and prev_high > entry:
|
||||
tp1_candidates.append(prev_high * (1 - key_level_buffer_pct))
|
||||
if vwap is not None and vwap > entry:
|
||||
tp1_candidates.append(vwap * (1 - key_level_buffer_pct))
|
||||
tp1_key = min(tp1_candidates) if tp1_candidates else None
|
||||
else: # short
|
||||
# SL 关键位 (entry 上方, 空仓止损 = 价格涨到这里平)
|
||||
sl_candidates = []
|
||||
if day_high is not None and day_high > entry:
|
||||
sl_candidates.append(day_high * (1 + key_level_buffer_pct))
|
||||
if prev_high is not None and prev_high > entry:
|
||||
sl_candidates.append(prev_high * (1 + key_level_buffer_pct))
|
||||
if vwap is not None and vwap > entry:
|
||||
sl_candidates.append(vwap * (1 + key_level_buffer_pct))
|
||||
sl_key = min(sl_candidates) if sl_candidates else None
|
||||
|
||||
# TP1 关键位 (entry 下方)
|
||||
tp1_candidates = []
|
||||
if day_low is not None and day_low < entry:
|
||||
tp1_candidates.append(day_low * (1 + key_level_buffer_pct))
|
||||
if prev_low is not None and prev_low < entry:
|
||||
tp1_candidates.append(prev_low * (1 + key_level_buffer_pct))
|
||||
if vwap is not None and vwap < entry:
|
||||
tp1_candidates.append(vwap * (1 + key_level_buffer_pct))
|
||||
tp1_key = max(tp1_candidates) if tp1_candidates else None
|
||||
|
||||
# === 混合: vol_pct 主导 (70%), 关键位微调 (30%) ===
|
||||
# 关键位 30% 权重, 防止 VWAP 等动态位锁死
|
||||
# vol_pct 至少占 70% (最终值不会偏离 vol_pct 太远)
|
||||
if side == 'long':
|
||||
if sl_key is not None:
|
||||
SL = sl_vol * 0.7 + sl_key * 0.3
|
||||
sl_method = 'hybrid_blend'
|
||||
else:
|
||||
SL = sl_vol
|
||||
sl_method = 'vol_pct'
|
||||
|
||||
if tp1_key is not None:
|
||||
TP1 = tp1_vol * 0.7 + tp1_key * 0.3
|
||||
tp_method = 'hybrid_blend'
|
||||
else:
|
||||
TP1 = tp1_vol
|
||||
tp_method = 'vol_pct'
|
||||
|
||||
TP2 = tp2_vol
|
||||
else: # short
|
||||
if sl_key is not None:
|
||||
SL = sl_vol * 0.7 + sl_key * 0.3
|
||||
sl_method = 'hybrid_blend'
|
||||
else:
|
||||
SL = sl_vol
|
||||
sl_method = 'vol_pct'
|
||||
|
||||
if tp1_key is not None:
|
||||
TP1 = tp1_vol * 0.7 + tp1_key * 0.3
|
||||
tp_method = 'hybrid_blend'
|
||||
else:
|
||||
TP1 = tp1_vol
|
||||
tp_method = 'vol_pct'
|
||||
|
||||
TP2 = tp2_vol
|
||||
|
||||
# === R:R 检查 ===
|
||||
if side == 'long':
|
||||
risk = entry - SL
|
||||
reward = TP1 - entry
|
||||
else:
|
||||
risk = SL - entry
|
||||
reward = entry - TP1
|
||||
|
||||
if risk <= 0:
|
||||
return None # 止损 >= 入场 (逻辑错误)
|
||||
|
||||
rr_ratio = reward / risk if risk > 0 else 0
|
||||
|
||||
# 风险 vs reward
|
||||
if abs(reward) < min_rr * abs(risk):
|
||||
# R:R 不达标
|
||||
if sl_key == tp1_key and sl_key is not None:
|
||||
note = f"VWAP 既作支撑又作阻力, 价格窄幅震荡 (SL=TP1={sl_key:.2f})"
|
||||
else:
|
||||
note = f"R:R {rr_ratio:.2f} < {min_rr}, 信号否决"
|
||||
return None
|
||||
|
||||
return ExitLevels(
|
||||
sl=SL,
|
||||
tp1=TP1,
|
||||
tp2=TP2,
|
||||
entry=entry,
|
||||
side=side,
|
||||
risk=abs(risk),
|
||||
reward=abs(reward),
|
||||
rr_ratio=rr_ratio,
|
||||
sl_method=sl_method,
|
||||
tp_method=tp_method,
|
||||
note=note,
|
||||
)
|
||||
|
||||
|
||||
def format_levels(levels: ExitLevels) -> str:
|
||||
"""格式化输出 (QQ 推送用)"""
|
||||
side_emoji = '🟢' if levels.side == 'long' else '🔴'
|
||||
return (
|
||||
f"{side_emoji} {levels.side.upper()} @ ${levels.entry:.2f}\n"
|
||||
f" SL: ${levels.sl:.2f} ({levels.sl_method})\n"
|
||||
f" TP1: ${levels.tp1:.2f} ({levels.tp_method})\n"
|
||||
f" TP2: ${levels.tp2:.2f}\n"
|
||||
f" Risk/Reward: 1:{levels.rr_ratio:.2f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 自检: 用 indicators.py 的真实数据测试
|
||||
print("=" * 60)
|
||||
print("exit_levels.py - 自检")
|
||||
print("=" * 60)
|
||||
|
||||
# 案例 1: NVDA 高波动 (有 R:R)
|
||||
print("\n[案例 1] NVDA $100, ATR=$8, day H/L=$115/$92, prev H/L=$118/$90")
|
||||
result = calc_exit_levels(
|
||||
entry=100.0, atr=8.0, current_price=100.0,
|
||||
day_high=115.0, day_low=92.0,
|
||||
prev_high=118.0, prev_low=90.0,
|
||||
vwap=105.0,
|
||||
side='long',
|
||||
)
|
||||
if result:
|
||||
print(format_levels(result))
|
||||
else:
|
||||
print("❌ 信号否决")
|
||||
|
||||
# 案例 2: 价在 VWAP 上下窄幅震荡
|
||||
print("\n[案例 2] NVDA $100, ATR=$3, VWAP=$101 (紧贴)")
|
||||
result = calc_exit_levels(
|
||||
entry=100.0, atr=3.0, current_price=100.0,
|
||||
day_high=103.0, day_low=98.0,
|
||||
prev_high=105.0, prev_low=95.0,
|
||||
vwap=101.0,
|
||||
side='long',
|
||||
)
|
||||
if result:
|
||||
print(format_levels(result))
|
||||
else:
|
||||
print("❌ 信号否决 (R:R 不达标 / VWAP 锁定)")
|
||||
|
||||
# 案例 3: 空仓 + 大波动
|
||||
print("\n[案例 3] TSDA short $200, ATR=$12")
|
||||
result = calc_exit_levels(
|
||||
entry=200.0, atr=12.0, current_price=200.0,
|
||||
day_high=212.0, day_low=188.0,
|
||||
prev_high=215.0, prev_low=185.0,
|
||||
vwap=205.0,
|
||||
side='short',
|
||||
)
|
||||
if result:
|
||||
print(format_levels(result))
|
||||
else:
|
||||
print("❌ 信号否决")
|
||||
Reference in New Issue
Block a user