2026-04-21 16:58:38 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Adaptive Exit Engine — parallel shadow mode for BLUE.
|
|
|
|
|
|
|
|
|
|
|
|
Runs alongside V7 per active trade. Does NOT interfere with real exits.
|
|
|
|
|
|
Logs shadow decisions to ClickHouse table `adaptive_exit_shadow` and
|
|
|
|
|
|
accumulates outcomes for online model updates.
|
|
|
|
|
|
|
|
|
|
|
|
Integration pattern (dolphin_actor.py):
|
|
|
|
|
|
from adaptive_exit.adaptive_exit_engine import AdaptiveExitEngine
|
|
|
|
|
|
_adaptive_exit = AdaptiveExitEngine.load()
|
|
|
|
|
|
|
|
|
|
|
|
# In _on_rt_exit_timer or _on_scan_timer, per active trade:
|
|
|
|
|
|
shadow = _adaptive_exit.evaluate(
|
|
|
|
|
|
trade_id=_tid,
|
|
|
|
|
|
asset=_asset,
|
|
|
|
|
|
direction=_dir, # -1 = SHORT
|
|
|
|
|
|
entry_price=_entry,
|
|
|
|
|
|
current_price=_cur_px,
|
|
|
|
|
|
bars_held=_bars,
|
|
|
|
|
|
max_hold=120,
|
|
|
|
|
|
recent_prices=_price_buf, # list[float], last 20+ bars
|
|
|
|
|
|
exf=self._last_exf,
|
|
|
|
|
|
)
|
|
|
|
|
|
# shadow is dict with: action, p_continuation, exit_reason_shadow, bucket_id
|
|
|
|
|
|
# Log it; never use it to exit.
|
|
|
|
|
|
|
|
|
|
|
|
Decision logic mirrors the spec:
|
|
|
|
|
|
EXIT if:
|
|
|
|
|
|
- mae > mae_threshold(vol) [hard stop]
|
|
|
|
|
|
- giveback: mfe < k * peak_mfe AND p_continuation < p_threshold
|
|
|
|
|
|
- tau > 1.0 [time cap]
|
|
|
|
|
|
"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import urllib.request
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
from typing import Dict, Optional
|
2026-04-21 16:58:38 +02:00
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
from adaptive_exit.bucket_engine import build_buckets, get_bucket
|
|
|
|
|
|
from adaptive_exit.continuation_model import ContinuationModelBank, FEATURE_COLS
|
|
|
|
|
|
|
|
|
|
|
|
# ── Config ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
P_THRESHOLD = 0.40 # P(continuation) below this → consider exit
|
|
|
|
|
|
GIVEBACK_K = 0.50 # MFE giveback fraction
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
MAE_MULT_TIER1 = 3.5 # fallback multiplier when bucket-specific entry missing
|
2026-04-21 16:58:38 +02:00
|
|
|
|
MAE_MULT_TIER2 = 7.0
|
|
|
|
|
|
ATR_WINDOW = 20
|
|
|
|
|
|
MIN_ATR = 1e-6
|
|
|
|
|
|
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
# Per-bucket MAE multipliers — replaces single MAE_MULT_TIER1 in the stop check.
|
|
|
|
|
|
# Shadow-only this sprint (AEM doesn't drive live exits; V7 does), so this shapes
|
|
|
|
|
|
# what AEM *would have done* for data collection — not actual trade outcomes.
|
|
|
|
|
|
# `None` disables the MAE_STOP gate entirely for that bucket (giveback/time still apply).
|
|
|
|
|
|
# B3 — natural winners; shadow shows 5.0–5.1 MAE peaks before FIXED_TP succeeds
|
|
|
|
|
|
# B4 — gross-negative alpha; cut fast before drawdown compounds
|
|
|
|
|
|
# B6 — extreme-vol assets; wide band or we trip on noise
|
|
|
|
|
|
MAE_MULT_BY_BUCKET: Dict[int, Optional[float]] = {
|
|
|
|
|
|
0: 3.5,
|
|
|
|
|
|
1: 3.0,
|
|
|
|
|
|
2: 3.5,
|
|
|
|
|
|
3: None,
|
|
|
|
|
|
4: 2.0,
|
|
|
|
|
|
5: 4.0,
|
|
|
|
|
|
6: 6.0,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 16:58:38 +02:00
|
|
|
|
_CH_URL = "http://localhost:8123/"
|
|
|
|
|
|
_CH_HEADERS = {"X-ClickHouse-User": "dolphin", "X-ClickHouse-Key": "dolphin_ch_2026"}
|
|
|
|
|
|
|
|
|
|
|
|
# Shadow outcome logging
|
|
|
|
|
|
_SHADOW_TABLE = "adaptive_exit_shadow"
|
|
|
|
|
|
_SHADOW_DB = "dolphin"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ch_insert(row: dict, db: str = _SHADOW_DB) -> None:
|
|
|
|
|
|
"""Non-blocking fire-and-forget insert."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
body = (json.dumps(row) + "\n").encode()
|
|
|
|
|
|
url = f"{_CH_URL}?database={db}&query=INSERT+INTO+{_SHADOW_TABLE}+FORMAT+JSONEachRow"
|
|
|
|
|
|
req = urllib.request.Request(url, data=body, method="POST")
|
|
|
|
|
|
for k, v in _CH_HEADERS.items():
|
|
|
|
|
|
req.add_header(k, v)
|
|
|
|
|
|
urllib.request.urlopen(req, timeout=3)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass # shadow logging is best-effort
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_shadow_table() -> None:
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
"""Create shadow table if it doesn't exist.
|
|
|
|
|
|
|
|
|
|
|
|
Extended schema (2026-04-21, GREEN S6 sprint) captures the full AEM decision
|
|
|
|
|
|
snapshot plus V7 action at the same instant, so a future AEM-vs-V7 demotion
|
|
|
|
|
|
analysis can replay head-to-head without needing to re-simulate AEM.
|
|
|
|
|
|
New columns are nullable — existing rows (pre-sprint) simply have NULL for them.
|
|
|
|
|
|
"""
|
2026-04-21 16:58:38 +02:00
|
|
|
|
ddl = (
|
|
|
|
|
|
f"CREATE TABLE IF NOT EXISTS {_SHADOW_DB}.{_SHADOW_TABLE} ("
|
|
|
|
|
|
"ts DateTime64(6, 'UTC'),"
|
|
|
|
|
|
"ts_day Date MATERIALIZED toDate(ts),"
|
|
|
|
|
|
"trade_id String,"
|
|
|
|
|
|
"asset LowCardinality(String),"
|
|
|
|
|
|
"bucket_id UInt8,"
|
|
|
|
|
|
"bars_held UInt16,"
|
|
|
|
|
|
"mae_norm Float32,"
|
|
|
|
|
|
"mfe_norm Float32,"
|
|
|
|
|
|
"tau_norm Float32,"
|
|
|
|
|
|
"p_cont Float32,"
|
|
|
|
|
|
"vel_div_entry Float32,"
|
|
|
|
|
|
"vel_div_now Float32,"
|
|
|
|
|
|
"action LowCardinality(String),"
|
|
|
|
|
|
"exit_reason LowCardinality(String),"
|
|
|
|
|
|
"actual_exit LowCardinality(String),"
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
"pnl_pct Float32,"
|
|
|
|
|
|
# ── AEM decision params (Nullable to stay backward-compat) ──
|
|
|
|
|
|
"mae_mult_applied Nullable(Float32),"
|
|
|
|
|
|
"mae_threshold Nullable(Float32),"
|
|
|
|
|
|
"atr Nullable(Float32),"
|
|
|
|
|
|
"p_threshold Nullable(Float32),"
|
|
|
|
|
|
"giveback_k Nullable(Float32),"
|
|
|
|
|
|
# ── V7 head-to-head (authoritative path) ──
|
|
|
|
|
|
"v7_action LowCardinality(Nullable(String)),"
|
|
|
|
|
|
"v7_exit_reason LowCardinality(Nullable(String)),"
|
|
|
|
|
|
# ── Naive counterfactual (what the dumb TP/STOP/MAX_HOLD would have done) ──
|
|
|
|
|
|
"naive_would_have LowCardinality(Nullable(String))"
|
2026-04-21 16:58:38 +02:00
|
|
|
|
") ENGINE = MergeTree()"
|
|
|
|
|
|
" ORDER BY (ts_day, asset, ts)"
|
|
|
|
|
|
" TTL ts_day + INTERVAL 90 DAY"
|
|
|
|
|
|
)
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
# For pre-existing tables, add the new columns idempotently. CH treats
|
|
|
|
|
|
# ADD COLUMN IF NOT EXISTS as a no-op when the column is already present.
|
|
|
|
|
|
alters = [
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS mae_mult_applied Nullable(Float32)",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS mae_threshold Nullable(Float32)",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS atr Nullable(Float32)",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS p_threshold Nullable(Float32)",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS giveback_k Nullable(Float32)",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS v7_action LowCardinality(Nullable(String))",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS v7_exit_reason LowCardinality(Nullable(String))",
|
|
|
|
|
|
f"ALTER TABLE {_SHADOW_DB}.{_SHADOW_TABLE} ADD COLUMN IF NOT EXISTS naive_would_have LowCardinality(Nullable(String))",
|
|
|
|
|
|
]
|
2026-04-21 16:58:38 +02:00
|
|
|
|
try:
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
for stmt in (ddl, *alters):
|
|
|
|
|
|
body = stmt.encode()
|
|
|
|
|
|
req = urllib.request.Request(_CH_URL, data=body, method="POST")
|
|
|
|
|
|
for k, v in _CH_HEADERS.items():
|
|
|
|
|
|
req.add_header(k, v)
|
|
|
|
|
|
urllib.request.urlopen(req, timeout=10)
|
2026-04-21 16:58:38 +02:00
|
|
|
|
except Exception as e:
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
print(f"[AdaptiveExitEngine] Warning: could not create/alter shadow table: {e}")
|
2026-04-21 16:58:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Per-trade state ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class _TradeState:
|
|
|
|
|
|
def __init__(self, trade_id: str, asset: str, direction: int,
|
|
|
|
|
|
entry_price: float, bucket_id: int, vel_div_entry: float = 0.0):
|
|
|
|
|
|
self.trade_id = trade_id
|
|
|
|
|
|
self.asset = asset
|
|
|
|
|
|
self.direction = direction # -1 = SHORT, 1 = LONG
|
|
|
|
|
|
self.entry_price = entry_price
|
|
|
|
|
|
self.bucket_id = bucket_id
|
|
|
|
|
|
self.vel_div_entry = vel_div_entry
|
|
|
|
|
|
self.mae = 0.0
|
|
|
|
|
|
self.mfe = 0.0
|
|
|
|
|
|
self.peak_mfe = 0.0
|
|
|
|
|
|
self.price_buf: list[float] = [] # rolling price history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Engine ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class AdaptiveExitEngine:
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, model_bank: ContinuationModelBank, bucket_data: dict):
|
|
|
|
|
|
self._model = model_bank
|
|
|
|
|
|
self._bucket_data = bucket_data
|
|
|
|
|
|
self._states: dict[str, _TradeState] = {}
|
|
|
|
|
|
self._lock = threading.Lock()
|
|
|
|
|
|
self._pending_outcomes: list[dict] = []
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def load(cls) -> "AdaptiveExitEngine":
|
|
|
|
|
|
"""Load pre-trained models. Falls back gracefully if not trained yet."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
bank = ContinuationModelBank.load()
|
|
|
|
|
|
print("[AdaptiveExitEngine] Continuation models loaded")
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
print("[AdaptiveExitEngine] WARNING: no trained model found — using untrained fallback")
|
|
|
|
|
|
bank = ContinuationModelBank()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
bucket_data = build_buckets(force_rebuild=False)
|
|
|
|
|
|
print(f"[AdaptiveExitEngine] Bucket assignments loaded: "
|
|
|
|
|
|
f"{bucket_data['n_buckets']} buckets")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[AdaptiveExitEngine] WARNING: bucket data unavailable ({e})")
|
|
|
|
|
|
bucket_data = {"assignments": {}, "n_buckets": 0}
|
|
|
|
|
|
|
|
|
|
|
|
_ensure_shadow_table()
|
|
|
|
|
|
return cls(bank, bucket_data)
|
|
|
|
|
|
|
|
|
|
|
|
# ── Trade lifecycle ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def on_entry(self, trade_id: str, asset: str, direction: int,
|
|
|
|
|
|
entry_price: float, vel_div_entry: float = 0.0) -> None:
|
|
|
|
|
|
bid = get_bucket(asset, self._bucket_data, fallback=0)
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
self._states[trade_id] = _TradeState(trade_id, asset, direction,
|
|
|
|
|
|
entry_price, bid, vel_div_entry)
|
|
|
|
|
|
|
|
|
|
|
|
def on_exit(self, trade_id: str, actual_exit_reason: str,
|
|
|
|
|
|
pnl_pct: float) -> None:
|
|
|
|
|
|
"""Called when the real system closes a trade — records outcome for online update.
|
|
|
|
|
|
|
|
|
|
|
|
Only natural exits feed the model (FIXED_TP, MAX_HOLD, V7/AE stops).
|
|
|
|
|
|
Forced exits (HIBERNATE_HALT, SUBDAY_ACB_NORMALIZATION) are filtered by
|
|
|
|
|
|
the model bank's natural-exits-only guard, preventing regime artifacts
|
|
|
|
|
|
from biasing the continuation distribution.
|
|
|
|
|
|
"""
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
st = self._states.pop(trade_id, None)
|
|
|
|
|
|
if st is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
cont = 1 if pnl_pct > 0 else 0
|
|
|
|
|
|
if st.price_buf:
|
|
|
|
|
|
prices = np.array(st.price_buf[-ATR_WINDOW:])
|
|
|
|
|
|
atr = max(np.std(np.diff(np.log(np.maximum(prices, 1e-12)))), MIN_ATR)
|
|
|
|
|
|
mae_norm = st.mae / atr
|
|
|
|
|
|
mfe_norm = st.mfe / atr
|
|
|
|
|
|
tau_norm = min(len(st.price_buf) / 120.0, 1.0)
|
|
|
|
|
|
ret_1 = float(np.log(prices[-1] / prices[-2])) if len(prices) >= 2 else 0.0
|
|
|
|
|
|
ret_3 = float(np.log(prices[-1] / prices[-4])) if len(prices) >= 4 else ret_1
|
|
|
|
|
|
|
|
|
|
|
|
obf = self._bucket_data.get("features", {})
|
|
|
|
|
|
obf_row = {}
|
|
|
|
|
|
if hasattr(obf, "loc") and st.asset in obf.index:
|
|
|
|
|
|
obf_row = obf.loc[st.asset].to_dict()
|
|
|
|
|
|
|
|
|
|
|
|
vel_div_now = float(prices[-1]) if len(prices) >= 1 else st.vel_div_entry # placeholder; overridden if caller passes it
|
|
|
|
|
|
p_pred = self._model.predict(
|
|
|
|
|
|
mae_norm=mae_norm, mfe_norm=mfe_norm, tau_norm=tau_norm,
|
|
|
|
|
|
ret_1=ret_1, ret_3=ret_3,
|
|
|
|
|
|
vel_div_entry=st.vel_div_entry, vel_div_now=st.vel_div_entry,
|
|
|
|
|
|
spread_bps=float(obf_row.get("spread_bps", 0.0)),
|
|
|
|
|
|
depth_usd=float(obf_row.get("depth_usd", 0.0)),
|
|
|
|
|
|
fill_prob=float(obf_row.get("fill_prob", 0.9)),
|
|
|
|
|
|
bucket_id=st.bucket_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._model.online_update(
|
|
|
|
|
|
bucket_id=st.bucket_id,
|
|
|
|
|
|
mae_norm=mae_norm,
|
|
|
|
|
|
mfe_norm=mfe_norm,
|
|
|
|
|
|
tau_norm=tau_norm,
|
|
|
|
|
|
ret_1=ret_1,
|
|
|
|
|
|
ret_3=ret_3,
|
|
|
|
|
|
vel_div_entry=st.vel_div_entry,
|
|
|
|
|
|
vel_div_now=st.vel_div_entry,
|
|
|
|
|
|
spread_bps=float(obf_row.get("spread_bps", 0.0)),
|
|
|
|
|
|
depth_usd=float(obf_row.get("depth_usd", 0.0)),
|
|
|
|
|
|
fill_prob=float(obf_row.get("fill_prob", 0.9)),
|
|
|
|
|
|
continuation=cont,
|
|
|
|
|
|
exit_reason=actual_exit_reason,
|
|
|
|
|
|
p_pred=p_pred,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Log one final shadow row at close so actual_exit is queryable for comparison
|
|
|
|
|
|
threading.Thread(target=_ch_insert, args=({
|
|
|
|
|
|
"ts": int(time.time() * 1e6),
|
|
|
|
|
|
"trade_id": trade_id,
|
|
|
|
|
|
"asset": st.asset,
|
|
|
|
|
|
"bucket_id": int(st.bucket_id),
|
|
|
|
|
|
"bars_held": int(tau_norm * 120),
|
|
|
|
|
|
"mae_norm": float(mae_norm),
|
|
|
|
|
|
"mfe_norm": float(mfe_norm),
|
|
|
|
|
|
"tau_norm": float(tau_norm),
|
|
|
|
|
|
"p_cont": float(p_pred),
|
|
|
|
|
|
"vel_div_entry": float(st.vel_div_entry),
|
|
|
|
|
|
"vel_div_now": float(st.vel_div_entry),
|
|
|
|
|
|
"action": "CLOSED",
|
|
|
|
|
|
"exit_reason": "",
|
|
|
|
|
|
"actual_exit": actual_exit_reason,
|
|
|
|
|
|
"pnl_pct": float(pnl_pct),
|
|
|
|
|
|
},), daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
# ── Per-bar evaluation ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def evaluate(
|
|
|
|
|
|
self,
|
|
|
|
|
|
trade_id: str,
|
|
|
|
|
|
asset: str,
|
|
|
|
|
|
direction: int,
|
|
|
|
|
|
entry_price: float,
|
|
|
|
|
|
current_price: float,
|
|
|
|
|
|
bars_held: int,
|
|
|
|
|
|
max_hold: int = 120,
|
|
|
|
|
|
recent_prices: Optional[list] = None,
|
|
|
|
|
|
exf: Optional[dict] = None,
|
|
|
|
|
|
vel_div_now: float = 0.0,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Evaluate whether the adaptive engine would exit this trade.
|
|
|
|
|
|
|
|
|
|
|
|
Returns shadow decision dict (never executed — caller logs only).
|
|
|
|
|
|
"""
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
if trade_id not in self._states:
|
|
|
|
|
|
bid = get_bucket(asset, self._bucket_data, fallback=0)
|
|
|
|
|
|
self._states[trade_id] = _TradeState(trade_id, asset, direction,
|
|
|
|
|
|
entry_price, bid, vel_div_now)
|
|
|
|
|
|
st = self._states[trade_id]
|
|
|
|
|
|
|
|
|
|
|
|
# Update price buffer
|
|
|
|
|
|
if recent_prices:
|
|
|
|
|
|
st.price_buf = list(recent_prices[-ATR_WINDOW - 5:])
|
|
|
|
|
|
elif current_price:
|
|
|
|
|
|
st.price_buf.append(current_price)
|
|
|
|
|
|
|
|
|
|
|
|
# Compute delta (positive = favorable for direction)
|
|
|
|
|
|
delta = direction * (entry_price - current_price) / entry_price
|
|
|
|
|
|
# For SHORT (dir=-1): delta = -(entry - cur)/entry = (cur - entry)/entry
|
|
|
|
|
|
# Wait — direction=-1 means SHORT, favorable = price drops = cur < entry
|
|
|
|
|
|
# delta = (entry - cur)/entry * abs(direction) ... let's be explicit:
|
|
|
|
|
|
if direction == -1: # SHORT
|
|
|
|
|
|
delta = (entry_price - current_price) / entry_price # +ve if price dropped
|
|
|
|
|
|
else: # LONG
|
|
|
|
|
|
delta = (current_price - entry_price) / entry_price # +ve if price rose
|
|
|
|
|
|
|
|
|
|
|
|
adverse = max(0.0, -delta)
|
|
|
|
|
|
favorable = max(0.0, delta)
|
|
|
|
|
|
st.mae = max(st.mae, adverse)
|
|
|
|
|
|
st.mfe = max(st.mfe, favorable)
|
|
|
|
|
|
st.peak_mfe = max(st.peak_mfe, st.mfe)
|
|
|
|
|
|
|
|
|
|
|
|
# ATR from price buffer
|
|
|
|
|
|
prices_arr = np.array(st.price_buf, dtype=float) if st.price_buf else np.array([current_price])
|
|
|
|
|
|
if len(prices_arr) >= 2:
|
|
|
|
|
|
log_rets = np.diff(np.log(np.maximum(prices_arr, 1e-12)))
|
|
|
|
|
|
atr = max(float(np.std(log_rets[-ATR_WINDOW:])), MIN_ATR)
|
|
|
|
|
|
else:
|
|
|
|
|
|
atr = MIN_ATR
|
|
|
|
|
|
|
|
|
|
|
|
mae_norm = st.mae / atr
|
|
|
|
|
|
mfe_norm = st.mfe / atr
|
|
|
|
|
|
tau_norm = bars_held / max_hold
|
|
|
|
|
|
|
|
|
|
|
|
prices_f = prices_arr[-ATR_WINDOW:]
|
|
|
|
|
|
ret_1 = float(np.log(prices_f[-1] / prices_f[-2])) if len(prices_f) >= 2 else 0.0
|
|
|
|
|
|
ret_3 = float(np.log(prices_f[-1] / prices_f[-4])) if len(prices_f) >= 4 else ret_1
|
|
|
|
|
|
|
|
|
|
|
|
# OBF static features for this asset
|
|
|
|
|
|
obf_feats = self._bucket_data.get("features", {})
|
|
|
|
|
|
obf_row = {}
|
|
|
|
|
|
if hasattr(obf_feats, "loc") and asset in obf_feats.index:
|
|
|
|
|
|
obf_row = obf_feats.loc[asset].to_dict()
|
|
|
|
|
|
|
|
|
|
|
|
# P(continuation)
|
|
|
|
|
|
p_cont = self._model.predict(
|
|
|
|
|
|
mae_norm=mae_norm,
|
|
|
|
|
|
mfe_norm=mfe_norm,
|
|
|
|
|
|
tau_norm=tau_norm,
|
|
|
|
|
|
ret_1=ret_1,
|
|
|
|
|
|
ret_3=ret_3,
|
|
|
|
|
|
vel_div_entry=st.vel_div_entry,
|
|
|
|
|
|
vel_div_now=vel_div_now,
|
|
|
|
|
|
spread_bps=float(obf_row.get("spread_bps", 0.0)),
|
|
|
|
|
|
depth_usd=float(obf_row.get("depth_usd", 0.0)),
|
|
|
|
|
|
fill_prob=float(obf_row.get("fill_prob", 0.9)),
|
|
|
|
|
|
bucket_id=st.bucket_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
# Decision logic — per-bucket MAE multiplier. `None` entry disables the
|
|
|
|
|
|
# MAE_STOP gate for that bucket (giveback + time checks still apply).
|
|
|
|
|
|
mae_mult = MAE_MULT_BY_BUCKET.get(st.bucket_id, MAE_MULT_TIER1)
|
|
|
|
|
|
mae_threshold = max(0.005, mae_mult * atr) if mae_mult is not None else None
|
2026-04-21 16:58:38 +02:00
|
|
|
|
action = "HOLD"
|
|
|
|
|
|
exit_reason = ""
|
|
|
|
|
|
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
if mae_threshold is not None and st.mae > mae_threshold:
|
2026-04-21 16:58:38 +02:00
|
|
|
|
action = "EXIT"
|
|
|
|
|
|
exit_reason = "AE_MAE_STOP"
|
|
|
|
|
|
elif (st.peak_mfe > 0 and st.mfe < GIVEBACK_K * st.peak_mfe
|
|
|
|
|
|
and p_cont < P_THRESHOLD):
|
|
|
|
|
|
action = "EXIT"
|
|
|
|
|
|
exit_reason = "AE_GIVEBACK_LOW_CONT"
|
|
|
|
|
|
elif tau_norm > 1.0:
|
|
|
|
|
|
action = "EXIT"
|
|
|
|
|
|
exit_reason = "AE_TIME"
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"trade_id": trade_id,
|
|
|
|
|
|
"asset": st.asset,
|
|
|
|
|
|
"action": action,
|
|
|
|
|
|
"exit_reason_shadow": exit_reason,
|
|
|
|
|
|
"p_continuation": p_cont,
|
|
|
|
|
|
"mae_norm": mae_norm,
|
|
|
|
|
|
"mfe_norm": mfe_norm,
|
|
|
|
|
|
"tau_norm": tau_norm,
|
|
|
|
|
|
"bucket_id": st.bucket_id,
|
|
|
|
|
|
"vel_div_entry": st.vel_div_entry,
|
|
|
|
|
|
"vel_div_now": vel_div_now,
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
"mae_mult_applied": mae_mult,
|
|
|
|
|
|
"mae_threshold": mae_threshold,
|
|
|
|
|
|
"atr": atr,
|
|
|
|
|
|
"p_threshold": P_THRESHOLD,
|
|
|
|
|
|
"giveback_k": GIVEBACK_K,
|
2026-04-21 16:58:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
def log_shadow(
|
|
|
|
|
|
self,
|
|
|
|
|
|
shadow: dict,
|
|
|
|
|
|
actual_exit: str = "",
|
|
|
|
|
|
pnl_pct: float = 0.0,
|
|
|
|
|
|
v7_action: Optional[str] = None,
|
|
|
|
|
|
v7_exit_reason: Optional[str] = None,
|
|
|
|
|
|
naive_would_have: Optional[str] = None,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Async log a shadow decision to ClickHouse.
|
|
|
|
|
|
|
|
|
|
|
|
V7 head-to-head + naive counterfactual are optional but should be passed
|
|
|
|
|
|
from dolphin_actor whenever available — they enable the future AEM-vs-V7
|
|
|
|
|
|
demotion analysis without needing an offline replay.
|
|
|
|
|
|
"""
|
|
|
|
|
|
def _opt(v):
|
|
|
|
|
|
return None if v is None else float(v)
|
|
|
|
|
|
|
2026-04-21 16:58:38 +02:00
|
|
|
|
row = {
|
|
|
|
|
|
"ts": int(time.time() * 1e6),
|
|
|
|
|
|
"trade_id": shadow.get("trade_id", ""),
|
|
|
|
|
|
"asset": shadow.get("asset", ""),
|
|
|
|
|
|
"bucket_id": int(shadow.get("bucket_id", 0)),
|
|
|
|
|
|
"bars_held": int(shadow.get("tau_norm", 0) * 120),
|
|
|
|
|
|
"mae_norm": float(shadow.get("mae_norm", 0)),
|
|
|
|
|
|
"mfe_norm": float(shadow.get("mfe_norm", 0)),
|
|
|
|
|
|
"tau_norm": float(shadow.get("tau_norm", 0)),
|
|
|
|
|
|
"p_cont": float(shadow.get("p_continuation", 0.5)),
|
|
|
|
|
|
"vel_div_entry": float(shadow.get("vel_div_entry", 0.0)),
|
|
|
|
|
|
"vel_div_now": float(shadow.get("vel_div_now", 0.0)),
|
|
|
|
|
|
"action": shadow.get("action", "HOLD"),
|
|
|
|
|
|
"exit_reason": shadow.get("exit_reason_shadow", ""),
|
|
|
|
|
|
"actual_exit": actual_exit,
|
|
|
|
|
|
"pnl_pct": float(pnl_pct),
|
feat(aem): per-bucket MAE_MULT table + shadow logging completeness
AEM stays shadow-only this sprint (V7 drives live exits); changes affect
what AEM *would have done*, logged to CH for future V7→AEM demotion analysis.
adaptive_exit_engine.py:
- Replace single MAE_MULT_TIER1=3.5 with MAE_MULT_BY_BUCKET dict
(B3→None disables MAE stop, B4→2.0 strict, B6→6.0 wide band)
- evaluate() return dict extended: mae_mult_applied, mae_threshold,
atr, p_threshold, giveback_k (all params that drove the decision)
- adaptive_exit_shadow schema extended (new Nullable columns added via
ALTER TABLE IF NOT EXISTS — backward-compat with pre-sprint rows)
- log_shadow() signature extended: v7_action, v7_exit_reason,
naive_would_have (TP/STOP/MAX_HOLD counterfactual at same instant)
dolphin_actor.py:
- AEM shadow call now passes V7 head-to-head decision and naive
counterfactual so future retrospective requires no offline replay
- EsoF listener registered on DOLPHIN_FEATURES map (esof_advisor_latest
key); label fed into engine._current_esof_label before each step_bar
- S6/bucket loaders (_load_s6_size_table, _load_asset_bucket_data) and
constructor wiring for the new GREEN engine kwargs
Plan refs: Tasks 5, 7, 10 — V7 path untouched, AEM return value is
never gated, CH shadow is best-effort (daemon thread).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:07:46 +02:00
|
|
|
|
# New AEM-decision params (Nullable-capable)
|
|
|
|
|
|
"mae_mult_applied": _opt(shadow.get("mae_mult_applied")),
|
|
|
|
|
|
"mae_threshold": _opt(shadow.get("mae_threshold")),
|
|
|
|
|
|
"atr": _opt(shadow.get("atr")),
|
|
|
|
|
|
"p_threshold": _opt(shadow.get("p_threshold")),
|
|
|
|
|
|
"giveback_k": _opt(shadow.get("giveback_k")),
|
|
|
|
|
|
# Head-to-head
|
|
|
|
|
|
"v7_action": v7_action,
|
|
|
|
|
|
"v7_exit_reason": v7_exit_reason,
|
|
|
|
|
|
"naive_would_have": naive_would_have,
|
2026-04-21 16:58:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
threading.Thread(target=_ch_insert, args=(row,), daemon=True).start()
|