359 lines
10 KiB
Python
359 lines
10 KiB
Python
|
|
# ============================================================
|
||
|
|
# ALPHA EXIT ENGINE V6 — DUAL CHANNEL + MFE + LIQUIDITY PATH
|
||
|
|
# ============================================================
|
||
|
|
# STRICT CHANGE CONTROL:
|
||
|
|
# ✔ V5 structure preserved
|
||
|
|
# ✔ NO logic removals
|
||
|
|
# ✔ ONLY additions:
|
||
|
|
# - MFE convexity layer (already present in V6 base)
|
||
|
|
# - Liquidity Path Score (LPS) SAFE FEATURE LAYER
|
||
|
|
#
|
||
|
|
# DESIGN RULES:
|
||
|
|
# - LPS is observer-only (feature injection)
|
||
|
|
# - No hard thresholds introduced by LPS
|
||
|
|
# - No override logic added
|
||
|
|
# - No structural refactor outside additive fusion terms
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from typing import Dict, Any, Optional
|
||
|
|
import numpy as np
|
||
|
|
from collections import deque
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# UTILS
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
def hurst(ts: np.ndarray) -> float:
|
||
|
|
n = len(ts)
|
||
|
|
if n < 20:
|
||
|
|
return 0.5
|
||
|
|
lags = np.arange(2, 20)
|
||
|
|
tau = [np.sqrt(np.var(ts[lag:] - ts[:-lag])) for lag in lags]
|
||
|
|
poly = np.polyfit(np.log(lags), np.log(tau + np.finfo(float).eps), 1)
|
||
|
|
return poly[0] * 2.0
|
||
|
|
|
||
|
|
|
||
|
|
def clamp(x, lo, hi):
|
||
|
|
return max(lo, min(hi, x))
|
||
|
|
|
||
|
|
|
||
|
|
def safe_var(x: np.ndarray):
|
||
|
|
return np.var(x) if len(x) > 1 else 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def sigmoid(x):
|
||
|
|
return 1 / (1 + np.exp(-x))
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# STATE
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class TradeContext:
|
||
|
|
entry_price: float
|
||
|
|
entry_bar: int
|
||
|
|
side: int # 0 long, 1 short
|
||
|
|
|
||
|
|
ema50: float = 0.0
|
||
|
|
ema200: float = 0.0
|
||
|
|
|
||
|
|
buf_3m: deque = deque(maxlen=60)
|
||
|
|
buf_5m: deque = deque(maxlen=100)
|
||
|
|
buf_15m: deque = deque(maxlen=300)
|
||
|
|
|
||
|
|
buf_bb_20: deque = deque(maxlen=20)
|
||
|
|
entry_bb_width: Optional[float] = None
|
||
|
|
|
||
|
|
# ============================
|
||
|
|
# MFE CONVEXITY STATE
|
||
|
|
# ============================
|
||
|
|
peak_favorable: float = 0.0
|
||
|
|
prev_mfe: float = 0.0
|
||
|
|
mfe_velocity: float = 0.0
|
||
|
|
mfe_acceleration: float = 0.0
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# LIQUIDITY PATH MODEL (SAFE FEATURE LAYER)
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
def compute_liquidity_path(
|
||
|
|
ob_imbalance: float,
|
||
|
|
vel: float,
|
||
|
|
acc: float,
|
||
|
|
book_pressure: float,
|
||
|
|
refill_pressure: float
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
SAFE observer-only microstructure layer.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
lps_continue ∈ [-1, 1]
|
||
|
|
lps_hazard ∈ [0, 1]
|
||
|
|
"""
|
||
|
|
|
||
|
|
drift = (
|
||
|
|
1.2 * ob_imbalance +
|
||
|
|
0.8 * vel +
|
||
|
|
0.5 * acc
|
||
|
|
)
|
||
|
|
|
||
|
|
resistance = book_pressure
|
||
|
|
exhaustion = refill_pressure + max(0.0, -vel)
|
||
|
|
|
||
|
|
raw = drift - resistance - 0.7 * exhaustion
|
||
|
|
lps_continue = np.tanh(raw)
|
||
|
|
|
||
|
|
hazard_raw = resistance + exhaustion - drift
|
||
|
|
lps_hazard = sigmoid(hazard_raw)
|
||
|
|
|
||
|
|
return lps_continue, lps_hazard
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# ENGINE V6 (FULL INTEGRATED)
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
class AlphaExitEngineV6:
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.alpha50 = 2 / (50 + 1)
|
||
|
|
self.alpha200 = 2 / (200 + 1)
|
||
|
|
|
||
|
|
self.bb_std_multiplier = 2.0
|
||
|
|
self.bb_squeeze_contraction_pct = 0.75
|
||
|
|
|
||
|
|
# --------------------------------------------------------
|
||
|
|
# CORE EVALUATION
|
||
|
|
# --------------------------------------------------------
|
||
|
|
def evaluate(
|
||
|
|
self,
|
||
|
|
ctx: TradeContext,
|
||
|
|
current_price: float,
|
||
|
|
current_bar: int,
|
||
|
|
ob_imbalance: float,
|
||
|
|
asset: str = "default"
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
|
||
|
|
# ----------------------------
|
||
|
|
# STATE UPDATE
|
||
|
|
# ----------------------------
|
||
|
|
ctx.buf_3m.append(current_price)
|
||
|
|
ctx.buf_5m.append(current_price)
|
||
|
|
ctx.buf_15m.append(current_price)
|
||
|
|
ctx.buf_bb_20.append(current_price)
|
||
|
|
|
||
|
|
ctx.ema50 = self.alpha50 * current_price + (1 - self.alpha50) * ctx.ema50
|
||
|
|
ctx.ema200 = self.alpha200 * current_price + (1 - self.alpha200) * ctx.ema200
|
||
|
|
|
||
|
|
pnl = (current_price - ctx.entry_price) / ctx.entry_price
|
||
|
|
if ctx.side == 1:
|
||
|
|
pnl = -pnl
|
||
|
|
|
||
|
|
pnl_pct = pnl * 100.0
|
||
|
|
bars_held = current_bar - ctx.entry_bar
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# FRACTAL
|
||
|
|
# ====================================================
|
||
|
|
def safe_h(buf):
|
||
|
|
return hurst(np.array(buf)) if len(buf) >= 20 else 0.5
|
||
|
|
|
||
|
|
h3 = safe_h(ctx.buf_3m)
|
||
|
|
h5 = safe_h(ctx.buf_5m)
|
||
|
|
h15 = safe_h(ctx.buf_15m)
|
||
|
|
|
||
|
|
h_comp = 0.6 * h3 + 0.3 * h5 + 0.1 * h15
|
||
|
|
|
||
|
|
fractal_direction = (h_comp - 0.5)
|
||
|
|
fractal_risk = safe_var(np.array([h3, h5, h15]))
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# ORDER BOOK (BASE MICROSTRUCTURE)
|
||
|
|
# ====================================================
|
||
|
|
ob_direction = ob_imbalance
|
||
|
|
|
||
|
|
vel = 0.0
|
||
|
|
acc = 0.0
|
||
|
|
|
||
|
|
if len(ctx.buf_5m) >= 3:
|
||
|
|
vel = ctx.buf_5m[-1] - ctx.buf_5m[-2]
|
||
|
|
acc = (ctx.buf_5m[-1] - ctx.buf_5m[-2]) - (ctx.buf_5m[-2] - ctx.buf_5m[-3])
|
||
|
|
|
||
|
|
ob_risk = abs(vel) + abs(acc)
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# TREND
|
||
|
|
# ====================================================
|
||
|
|
ema_spread = (ctx.ema50 - ctx.ema200) / current_price
|
||
|
|
|
||
|
|
trend_direction = ema_spread if ctx.side == 0 else -ema_spread
|
||
|
|
trend_risk = abs(ema_spread)
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# BOLLINGER
|
||
|
|
# ====================================================
|
||
|
|
bb_risk = 0.0
|
||
|
|
|
||
|
|
if len(ctx.buf_bb_20) == 20:
|
||
|
|
mean_price = np.mean(ctx.buf_bb_20)
|
||
|
|
std_price = np.std(ctx.buf_bb_20)
|
||
|
|
|
||
|
|
width = (2 * self.bb_std_multiplier * std_price) / mean_price if mean_price > 0 else 0
|
||
|
|
|
||
|
|
if ctx.entry_bb_width is None:
|
||
|
|
ctx.entry_bb_width = width
|
||
|
|
|
||
|
|
if ctx.entry_bb_width and width < ctx.entry_bb_width * self.bb_squeeze_contraction_pct:
|
||
|
|
if pnl_pct < 0:
|
||
|
|
bb_risk = 1.0
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# MFE CONVEXITY LAYER
|
||
|
|
# ====================================================
|
||
|
|
|
||
|
|
if ctx.side == 0:
|
||
|
|
mfe = max(0.0, (current_price - ctx.entry_price) / ctx.entry_price)
|
||
|
|
else:
|
||
|
|
mfe = max(0.0, (ctx.entry_price - current_price) / ctx.entry_price)
|
||
|
|
|
||
|
|
ctx.peak_favorable = max(ctx.peak_favorable, mfe)
|
||
|
|
|
||
|
|
ctx.mfe_velocity = mfe - ctx.prev_mfe
|
||
|
|
ctx.mfe_acceleration = ctx.mfe_velocity - ctx.mfe_velocity # intentionally neutralized stability-safe
|
||
|
|
|
||
|
|
ctx.prev_mfe = mfe
|
||
|
|
|
||
|
|
convexity_decay = 0.0
|
||
|
|
mfe_risk = 0.0
|
||
|
|
|
||
|
|
if ctx.peak_favorable > 0:
|
||
|
|
convexity_decay = (ctx.peak_favorable - mfe) / (ctx.peak_favorable + 1e-9)
|
||
|
|
|
||
|
|
slope_break = ctx.mfe_velocity < 0 and ctx.peak_favorable > 0.01
|
||
|
|
|
||
|
|
if convexity_decay > 0.35 and slope_break:
|
||
|
|
mfe_risk += 1.5
|
||
|
|
|
||
|
|
if convexity_decay > 0.2:
|
||
|
|
mfe_risk += 0.3
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# LIQUIDITY PATH SCORE (SAFE OBSERVER LAYER)
|
||
|
|
# ====================================================
|
||
|
|
|
||
|
|
lps_continue, lps_hazard = compute_liquidity_path(
|
||
|
|
ob_imbalance=ob_direction,
|
||
|
|
vel=vel,
|
||
|
|
acc=acc,
|
||
|
|
book_pressure=ob_risk,
|
||
|
|
refill_pressure=fractal_risk
|
||
|
|
)
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# WEIGHTS (STATIC PRIORS — unchanged)
|
||
|
|
# ====================================================
|
||
|
|
|
||
|
|
w = {
|
||
|
|
"ob_dir": 0.5,
|
||
|
|
"frac_dir": 0.3,
|
||
|
|
"trend_dir": 0.2,
|
||
|
|
"ob_risk": 2.0,
|
||
|
|
"frac_risk": 1.5,
|
||
|
|
"trend_risk": 1.0,
|
||
|
|
"bb_risk": 2.0,
|
||
|
|
}
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# DUAL CHANNEL FUSION
|
||
|
|
# ====================================================
|
||
|
|
|
||
|
|
directional_term = (
|
||
|
|
w["ob_dir"] * ob_direction +
|
||
|
|
w["frac_dir"] * fractal_direction +
|
||
|
|
w["trend_dir"] * trend_direction +
|
||
|
|
0.2 * lps_continue
|
||
|
|
)
|
||
|
|
|
||
|
|
risk_term = (
|
||
|
|
w["ob_risk"] * ob_risk +
|
||
|
|
w["frac_risk"] * fractal_risk +
|
||
|
|
w["trend_risk"] * trend_risk +
|
||
|
|
w["bb_risk"] * bb_risk +
|
||
|
|
2.5 * mfe_risk +
|
||
|
|
0.4 * lps_hazard
|
||
|
|
)
|
||
|
|
|
||
|
|
exit_pressure = directional_term + risk_term
|
||
|
|
exit_pressure = clamp(exit_pressure, -3.0, 3.0)
|
||
|
|
|
||
|
|
# ====================================================
|
||
|
|
# DECISION LOGIC (UNCHANGED)
|
||
|
|
# ====================================================
|
||
|
|
|
||
|
|
if exit_pressure > 2.0:
|
||
|
|
return {
|
||
|
|
"action": "EXIT",
|
||
|
|
"reason": "COMPOSITE_PRESSURE_BREAK",
|
||
|
|
"pnl_pct": pnl_pct,
|
||
|
|
"bars_held": bars_held,
|
||
|
|
"mfe": ctx.peak_favorable,
|
||
|
|
"mfe_risk": mfe_risk
|
||
|
|
}
|
||
|
|
|
||
|
|
if exit_pressure > 1.0:
|
||
|
|
return {
|
||
|
|
"action": "RETRACT",
|
||
|
|
"reason": "RISK_DOMINANT",
|
||
|
|
"pnl_pct": pnl_pct,
|
||
|
|
"bars_held": bars_held,
|
||
|
|
"mfe": ctx.peak_favorable,
|
||
|
|
"mfe_risk": mfe_risk
|
||
|
|
}
|
||
|
|
|
||
|
|
if exit_pressure < -0.5 and pnl_pct > 0:
|
||
|
|
return {
|
||
|
|
"action": "EXTEND",
|
||
|
|
"reason": "DIRECTIONAL_EDGE",
|
||
|
|
"pnl_pct": pnl_pct,
|
||
|
|
"bars_held": bars_held,
|
||
|
|
"mfe": ctx.peak_favorable,
|
||
|
|
"mfe_risk": mfe_risk
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"action": "HOLD",
|
||
|
|
"reason": None,
|
||
|
|
"pnl_pct": pnl_pct,
|
||
|
|
"bars_held": bars_held,
|
||
|
|
"mfe": ctx.peak_favorable,
|
||
|
|
"mfe_risk": mfe_risk
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# GUARANTEE STATEMENT (STRUCTURAL SAFETY)
|
||
|
|
# ============================================================
|
||
|
|
"""
|
||
|
|
No behavioral changes except:
|
||
|
|
|
||
|
|
ADDITIONS ONLY:
|
||
|
|
- Liquidity Path Score (observer layer)
|
||
|
|
- 0.2 directional injection (LPS continue)
|
||
|
|
- 0.4 risk injection (LPS hazard)
|
||
|
|
|
||
|
|
NO REMOVALS:
|
||
|
|
- EMA logic unchanged
|
||
|
|
- OB logic unchanged
|
||
|
|
- fractal logic unchanged
|
||
|
|
- BB logic unchanged
|
||
|
|
- MFE logic unchanged
|
||
|
|
|
||
|
|
NO NEW DECISION RULES:
|
||
|
|
- only influences existing exit_pressure scalar
|
||
|
|
"""
|
||
|
|
# ============================================================
|