uv(parity): exhaustive exit-doctrine re-crawl x3 (BLUE TP, BLUE SL+ADVSL, UV) — supersedes phase-1 matrix
TP: cascade x1.40 SATURATED not dormant (313870/314158 decisions; effective TP ~0.26-0.28%); withdrawal x0.60 provably unreachable (elif shadow); per-asset x0.75 never observed; TP_FLOOR = 46% of TP-family closes. SL/ADVSL: ADVSL live loss-cutter (1347 CLOSE + 965 PARTIAL_EXIT/30d, always fraction=1.0) via trader RETRACT pipeline; shadow table never existed (silent DDL bug) -> ADVSL sub-modes dormant; withdrawal 0.10 WIDENS floor. UV: multi-leg implemented in Rust FSM, promotion never sets ratios; venue carries-not-consumes. 16 weirdnesses to port as-is.
This commit is contained in:
615
prod/docs/uv_cert/exit_crawl/BLUE_SL_ADVSL_CRAWL.md
Normal file
615
prod/docs/uv_cert/exit_crawl/BLUE_SL_ADVSL_CRAWL.md
Normal file
@@ -0,0 +1,615 @@
|
||||
# BLUE STOP-LOSS + ADVSL crawl — parity-port reference
|
||||
|
||||
Date: 2026-07-12 (crawl). READ-ONLY audit of the live BLUE codepaths.
|
||||
Sources: code (exact file:line), live process env (`/proc/431423/environ`),
|
||||
ClickHouse `dolphin` DB (UTC), trader logs `/mnt/dolphinng5_predict/prod/logs/nautilus_trader_YYYYMMDD_v2_gold_fix_v50-v750.log`
|
||||
(UTC timestamps in-file; operator quotes CET). Line numbers are as of branch `tools/pi_wake_agent` working tree.
|
||||
|
||||
Files crawled:
|
||||
|
||||
- `/mnt/dolphinng5_predict/prod/nautilus_event_trader.py` (5419 lines) — "the trader"
|
||||
- `/mnt/dolphinng5_predict/adaptive_exit/advanced_sl.py` (1203 lines, parsed in full)
|
||||
- `/mnt/dolphinng5_predict/adaptive_exit/market_state_outputs.py`
|
||||
- `/mnt/dolphinng5_predict/adaptive_exit/post_win_long_overlay.py` (EFSM — `exit_policy_meta`)
|
||||
- `/mnt/dolphinng5_predict/adaptive_exit/meta_exit_performance.py` (`ExitMetaPerformanceTracker`)
|
||||
- `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/alpha_exit_manager.py` (339 lines, parsed in full)
|
||||
- `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/alpha_exit_v7_engine.py` (decision-dict section)
|
||||
|
||||
Note: BLUE submits **no venue orders**. Every exit in this document is in-memory
|
||||
portfolio accounting (engine position → capital update → CH journal → HZ state).
|
||||
There is no order-submission path anywhere in `nautilus_event_trader.py`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Live process environment (confirmed on PID 431423)
|
||||
|
||||
```
|
||||
DOLPHIN_CATASTROPHIC_FLOOR_PCT=0.0120 # == code default (trader:462)
|
||||
DOLPHIN_ENABLE_ADVANCED_SL_LIVE=1 # ADVSL live exits ON (trader:457, default False)
|
||||
DOLPHIN_OVERLAY_ADVSL_LIVE=1 # overlay-ADVSL ON (trader:472, default True)
|
||||
DOLPHIN_OVERLAY_ADVSL_MAE_RISK_MIN=0.50 # trader:476
|
||||
DOLPHIN_OVERLAY_ADVSL_MFE_MAX_PCT=0.0020 # trader:474
|
||||
DOLPHIN_OVERLAY_ADVSL_MIN_BARS=6 # trader:473
|
||||
DOLPHIN_OVERLAY_ADVSL_PRESSURE_MIN=1.85 # trader:475
|
||||
DOLPHIN_OVERLAY_CATASTROPHIC_FLOOR_PCT=0.0050 # trader:466
|
||||
DOLPHIN_OVERLAY_CATASTROPHIC_MAX_LOSS_USD=500 # trader:470
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Layer map — six SL-ish mechanisms, one exit funnel
|
||||
|
||||
```
|
||||
per-bar (scan ~5s), inside eng.step_bar()
|
||||
┌─ V7 live exit provider (eng.exit_decision_provider, trader:685/1231) — EXIT/RETRACT/EXTEND
|
||||
├─ AlphaExitManager.evaluate() — TP_FLOOR → [vd off] → [tf off] → FIXED_TP → STOP_LOSS → MAX_HOLD
|
||||
│ STOP_LOSS gate = st["stop_pct_override"] or self.stop_pct(=1.0, disabled)
|
||||
│ stop_pct_override is ARMED by the trader:
|
||||
│ A. catastrophic floor 1.2% (every scan, trader:4192; restore trader:2022/2287/2064/2335; entry trader:4275)
|
||||
│ B. overlay floor min(1.2%, 0.5%, $500/notional) for EFSM overlay_flip trades (trader:2496-2521)
|
||||
│ C. hibernate-protect per-bucket SL (trader:2424-2454, _BUCKET_SL_PCT trader:242)
|
||||
│ D. withdrawal-stress hard SL 0.10 (inside exit manager itself, alpha_exit_manager:183)
|
||||
└─ async, daemon thread per scan (trader:4815-4920):
|
||||
AdvancedSL.evaluate() (advanced_sl:592) ──would_exit──► RETRACT cmd fraction=1.0
|
||||
_overlay_advsl_should_exit() (trader:2523) ──True─────► RETRACT cmd fraction=1.0
|
||||
V7 action==RETRACT (trader:4495) ─────────────────────► RETRACT cmd fraction=0.50
|
||||
all → HZ DOLPHIN_CONTROL_PLANE["blue_runtime_commands"]
|
||||
drained on scan thread (trader:4551 → 3926) →
|
||||
_apply_internal_retract (trader:3696) → PARTIAL_EXIT leg(s)
|
||||
→ full close → forced exit dict → CLOSE finalizer (trader:4555+)
|
||||
```
|
||||
|
||||
Precedence facts:
|
||||
|
||||
- Engine-internal exits (TP_FLOOR/FIXED_TP/STOP_LOSS/MAX_HOLD/V7) win over queued
|
||||
RETRACTs for the same bar: `_forced_exit` is only adopted `if ... not result.get('exit')`
|
||||
(trader:4551-4553).
|
||||
- The heartbeat thread also drains the queue but with `allow_retract=False`
|
||||
(trader:2633) — RETRACTs are deliberately left queued so position mutation
|
||||
only ever happens on the scan thread.
|
||||
- The catastrophic floor is re-armed **every scan before step_bar** (trader:4191-4192),
|
||||
so it always back-stops whatever another mechanism set.
|
||||
|
||||
---
|
||||
|
||||
## 2. AlphaExitManager — the STOP_LOSS kernel
|
||||
|
||||
File: `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/alpha_exit_manager.py`
|
||||
|
||||
- Defaults (ctor, :27-29): `fixed_tp_pct=0.0099`, `stop_pct=1.0` (**-100%, never fires
|
||||
by itself** — header comment "Stop Loss: pnl_pct <= -1.0 (DISABLED)"), `max_hold_bars=120`.
|
||||
- Per-position state (`setup_position`, :100-119) carries `stop_pct_override`
|
||||
(`None` → global 1.0), `tp_pct_override`, `max_hold_override`.
|
||||
- `evaluate()` (:121-330):
|
||||
- `pnl_pct = d * (current_price - entry) / entry` (:146); `max_favorable` ratchet (:148).
|
||||
- `dynamic_sl_pct = st.get("stop_pct_override") or self.stop_pct` (:152) —
|
||||
NOTE: `or`-semantics, so an override of `0.0` falls back to global (can't disable via 0).
|
||||
- OB modulation block (:163-202):
|
||||
- cascade (`macro.cascade_count > 0`): TP ×1.40, max_hold ×0.5 (:174-178).
|
||||
- **withdrawal stress** (`macro.regime_signal == 1`): `dynamic_sl_pct = 0.10`
|
||||
hard 10% stop (:183) — this OVERWRITES any armed per-trade floor for that bar
|
||||
(widens 1.2% → 10% while the stress regime lasts); plus TP ×0.60 if in profit
|
||||
(:187) and max_hold ×0.4 if OB against us (:189-190).
|
||||
- calm+favoring: max_hold ×1.5 (:193-195); per-asset withdrawal: max_hold ≤×0.40,
|
||||
TP ×0.75 if in profit (:198-202).
|
||||
- TP diagnostics into `self.last_eval` on every call (:208-223) — consumed by
|
||||
the v7 journal and the trader's TP_EXIT log line (trader:4582).
|
||||
- Exit order: `TP_FLOOR` ratchet (:232, if `tp_floor_enabled`; live via DOLPHIN_TP_FLOOR)
|
||||
→ vd exits (:242-277, `vd_enabled=False` — dormant) → tf exits (:280-312,
|
||||
`tf_enabled=False` — dormant) → `FIXED_TP` (:315) → **`STOP_LOSS` at
|
||||
`pnl_pct <= -dynamic_sl_pct` (:320-322)** → `MAX_HOLD` (:325).
|
||||
|
||||
So BLUE's actual stop-loss = `stop_pct_override` armed by the trader (§3-§5), evaluated
|
||||
here, exit reason string `STOP_LOSS`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Catastrophic floor (base 1.2%)
|
||||
|
||||
- Config: `self._catastrophic_floor_pct = max(0, env DOLPHIN_CATASTROPHIC_FLOOR_PCT, default 0.0120)`
|
||||
(trader:460-463). Live env = 0.0120, same as default.
|
||||
- `_catastrophic_floor_for_open_position()` (trader:2496-2521):
|
||||
- non-overlay trade (`pending["overlay_flip"]` falsy) → `(0.0120, "base")` (:2505-2506).
|
||||
- overlay trade → §4.
|
||||
- `_apply_catastrophic_floor_to_open_position()` (trader:2456-2494):
|
||||
- no-ops without engine/position/trade_id or floor ≤ 0.
|
||||
- if the exit manager has no state for the trade → full `setup_position(..., stop_pct_override=floor)`
|
||||
(:2473-2479).
|
||||
- else **tighten-only ratchet** (:2486-2488): replace only when
|
||||
`current <= 0.0 or current > floor_pct`. It never *widens* an existing tighter stop.
|
||||
- logs `CATASTROPHIC_FLOOR armed: {asset} SL=x.xx% mode={base|overlay:...} trade=...`.
|
||||
- Call sites (all under `eng_lock`):
|
||||
1. every scan, immediately before `eng.step_bar` (trader:4191-4192) — the standing re-arm;
|
||||
2. on new entry after `_pending_entries` is built (trader:4274-4275);
|
||||
3. on both restore paths after the position is rebuilt (trader:2063-2064 HZ-fallback,
|
||||
2334-2335 position_state) — and on restore `setup_position` itself is already called
|
||||
with `stop_pct_override=0.0120` (trader:2017-2023 and 2282-2288), so a restored
|
||||
position is floor-armed even before the first scan.
|
||||
- Evidence (live): log 2026-07-11 has exactly one
|
||||
`CATASTROPHIC_FLOOR armed: DOGEUSDT SL=1.20% mode=base trade=…` (arm log fires only
|
||||
on state change, thanks to the ratchet). ClickHouse, last 30 days:
|
||||
`STOP_LOSS` CLOSE ×204, avg pnl_pct −1.178%, min −3.731% (gap through the floor),
|
||||
max −0.341%, avg bars_held 29.7 — i.e. the 1.2% floor is the thing that actually
|
||||
fires as `STOP_LOSS`. **Live, active.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Overlay floor 0.50% + $500 USD max-loss (EFSM overlay LONG trades only)
|
||||
|
||||
- Applies only when `pending["overlay_flip"] == True` — trades tagged LONG by the
|
||||
post-win EFSM overlay at entry (trader:4222-4234; `overlay_flip` stored :4251).
|
||||
- `_catastrophic_floor_for_open_position()` overlay branch (trader:2508-2521):
|
||||
- `floor_pct = min(base 0.0120, overlay 0.0050)` (positive candidates only);
|
||||
- USD cap: `floor_pct = min(floor_pct, max_loss_usd / notional)` with
|
||||
`notional = pending.notional_entry|notional|pos.notional`, `max_loss_usd = 500` (:2512-2518)
|
||||
— for entry notional > $100k the USD cap is the binding constraint;
|
||||
- returns `(floor, f"overlay:{overlay_reason}")`.
|
||||
- Armed through the same `_apply_catastrophic_floor_to_open_position()` ratchet → exits
|
||||
as plain `STOP_LOSS` through the exit manager (no distinct reason string).
|
||||
- Dormancy: only as live as EFSM overlay tagging; no `mode=overlay:` arm lines in the
|
||||
sampled 2026-07-10/11 logs. **Present, conditionally live, rarely exercised.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Hibernate-protect per-bucket SL
|
||||
|
||||
- Table `_BUCKET_SL_PCT` (trader:242-251), derived 2026-04-19 from AE-shadow + bucket
|
||||
analysis (comment :234-241):
|
||||
`{0: 0.015, 1: 0.012, 2: 0.015, 3: 0.025, 4: 0.008, 5: 0.018, 6: 0.030, 'default': 0.015}`.
|
||||
Bucket = KMeans assignment `self._bucket_assignments[asset]` loaded from pkl
|
||||
(fallback log trader:732 uses `default`=1.5%).
|
||||
- Trigger: posture sync detects transition to `HIBERNATE` while a position is open and
|
||||
protect not yet active (trader:4144-4150) → `_hibernate_protect_position()` (trader:2424-2454):
|
||||
- sets `em_state['stop_pct_override'] = sl_pct` **unconditionally** (:2443) — this can
|
||||
*widen* a previously-armed 1.2% floor to e.g. 2.5%/3.0% for buckets 3/6;
|
||||
- BUT the per-scan catastrophic re-arm (§3, tighten-only) runs next scan and pulls any
|
||||
bucket SL wider than 1.2% back down to 1.2%. Net effect: bucket SL only survives when
|
||||
it is TIGHTER than the floor (B4 0.008, B1 0.012-tie). The wide-bucket half of the
|
||||
table is effectively dead under the current floor. Nobody guards against this.
|
||||
- `_day_posture` intentionally NOT set to HIBERNATE while protecting (comment :4148-4149)
|
||||
so `HIBERNATE_HALT` doesn't fire; `_hibernate_protect_active = trade_id` (:2452).
|
||||
- explicitly NOT armed during restore (comment trader:2289-2292) — first scan's posture
|
||||
sync does it.
|
||||
- Exit relabel (trader:4558-4571): if the closing trade is the protected one, reason is
|
||||
mapped `{FIXED_TP→HIBERNATE_TP, STOP_LOSS→HIBERNATE_SL, MAX_HOLD→HIBERNATE_MAXHOLD}`,
|
||||
fallback `f"HIBERNATE_{orig}"`; posture then finalized.
|
||||
- Evidence: 30-day trade_events show **zero** HIBERNATE_TP/SL/MAXHOLD, but
|
||||
`HIBERNATE_HIBERNATE_HALT` ×7 on 2026-06-25 — i.e. the only recent hibernate-protected
|
||||
closes were `HIBERNATE_HALT` exits that hit the map *fallback* (double-prefix label),
|
||||
meaning the halt fired anyway while protect was active. No `HIBERNATE_PROTECT` lines in
|
||||
2026-07-10/11 logs. **Present, rarely triggered, and its wide-bucket arm is neutered by
|
||||
the floor ratchet.**
|
||||
|
||||
---
|
||||
|
||||
## 6. AdvancedSL runtime (`adaptive_exit/advanced_sl.py`) — full parse
|
||||
|
||||
### 6.1 Constants / plumbing
|
||||
|
||||
- CH endpoint: `CLICKHOUSE_HTTP_URL|USER|PASSWORD` env, defaults
|
||||
`http://localhost:8123/`, `dolphin`, `dolphin_ch_2026` (:22-24).
|
||||
- `ADVANCED_SL_DB="dolphin"`, `ADVANCED_SL_TABLE="advanced_sl_shadow"` (:26-27).
|
||||
- HZ control-plane keys (:28-31): `advanced_sl_control` (config), `advanced_sl_latest`
|
||||
(last decision), `advanced_sl_monitor_latest`, `advanced_sl_monitor_report`.
|
||||
- Snapshot files: `/mnt/dolphin_training/advanced_sl/latest_advanced_sl.json`,
|
||||
`monitor_report.json` (:33-34).
|
||||
- `_ch_query()` (:66-80): **appends `FORMAT TSVWithNames` to any SQL that lacks the word
|
||||
FORMAT** — including the CREATE TABLE DDL (see §6.7 dormancy finding).
|
||||
|
||||
### 6.2 AdvancedSLConfig (:330-368) — defaults (live config == defaults per snapshot, plus `enabled: true`)
|
||||
|
||||
| field | default | role |
|
||||
|---|---|---|
|
||||
| min_hold_bars | 20 | hard gate floor |
|
||||
| hold_fraction_of_market_state | 0.25 | effective_min_hold = max(min_hold, 0.25×market_state_max_hold_bars) |
|
||||
| pressure_min | 1.85 | hard gate: v7 exit_pressure ≥ |
|
||||
| continuation_max | 0.42 | hard gate: continuation_prob ≤ |
|
||||
| stress_min | 0.52 | hard gate: state_stress ≥ |
|
||||
| snapback_max | 0.56 | hard gate: snapback_score ≤ |
|
||||
| cut_score_min | 0.70 | score threshold |
|
||||
| recent_window | 16 | price tail length |
|
||||
| price_band_pct | 0.0015 | time_at_price band |
|
||||
| stall_floor | 0.00015 | stall log-diff floor |
|
||||
| momentum_floor | 0.00010 | momentum normalizer |
|
||||
| pressure/continuation/stress/snapback/stall weights | 0.30/0.30/0.20/0.15/0.05 | score terms |
|
||||
| support/forecast/decay weights | 0.12/0.12/0.10 | score terms (post-hoc additions; weights sum > 1) |
|
||||
| base_tp_pct / base_max_hold_bars | 0.0095 / 120 | from `market_state_outputs.py:7-8` (`DEFAULT_BASE_TP_PCT`, `DEFAULT_BASE_MAX_HOLD_BARS`) |
|
||||
| enabled | False (live: True) | **cosmetic — never consulted by evaluate() or the trader gate** |
|
||||
| catastrophic_mode_enabled | **False (live: False)** | gates the internal ASL catastrophic block — DORMANT |
|
||||
| catastrophic_floor_base/min/max | 0.0100 / 0.0080 / 0.0120 | meta-adjusted floor clamp |
|
||||
| catastrophic_emergency_floor | 0.0150 | unconditional cut level |
|
||||
| catastrophic_min_hold_bars | 20 | tp_unlikely gate |
|
||||
| catastrophic_tp_unlikely_mfe_frac / now_frac | 0.35 / 0.10 | tp_unlikely gates |
|
||||
| catastrophic_min_support / min_edge | 8 / 0.0 | meta gate |
|
||||
| catastrophic_unknown_meta_allows_base | True | support==0 ⇒ base floor allowed |
|
||||
|
||||
`from_dict` (:374-381) merges only known keys over a fallback config.
|
||||
|
||||
### 6.3 Scoring functions (exact formulas)
|
||||
|
||||
All prices below are the last `recent_window` prices (trader passes
|
||||
`self._bounce_price_path(asset)`; current price appended if missing; min length 3;
|
||||
fallback `[entry, current]`) (:610-616).
|
||||
|
||||
- `_favorable_series` (:109): `sign·(px−entry)/entry`, sign=+1 LONG / −1 SHORT (:96-97).
|
||||
- `_momentum_score(side, prices, floor)` (:116-124):
|
||||
`clamp( sign·mean(last≤5 log-diffs) / max(floor,1e-12), −1, 1 )`.
|
||||
- `_stall_fraction(prices, floor)` (:127-133): fraction of |log-diffs| < floor.
|
||||
- `_to_band` → `time_at_price` (:100-106): fraction of window within ±price_band_pct of
|
||||
the window's FIRST price. Computed, journaled, **never used in the decision**.
|
||||
- `_mean_reversion_snapback_score` (:136-154):
|
||||
`recovery = clamp((fav[-1]−min_fav)/|min_fav|,0,1)` if min_fav<0 else 0;
|
||||
`recent_momentum = clamp((fav[-1]−fav[-min(4,n)])/(|fav[-min(4,n)]|+1e-12),−1,1)`;
|
||||
score = `clamp(0.60·recovery + 0.25·max(0,recent_momentum) + 0.15·clamp(mr_strength,0,1), 0,1)`.
|
||||
- `_recent_curve_metrics` (:166-194) on favorable series:
|
||||
`slope = mean(last≤4 diffs)`; `accel = mean(last≤3 diff²)`;
|
||||
`drawdown_from_peak = max(0, peak−current)`; `recovery_ratio = clamp(current/peak,0,1)`;
|
||||
`decay_bars` = trailing run of negative diffs.
|
||||
- `_market_support_score` (:197-252) from market_state ∪ market_bundle:
|
||||
`0.28·confidence + 0.18·top_similarity + 0.14·min(target_count/5,1) + 0.14·asset_match
|
||||
+ 0.12·clamp((cont+1)/2,0,1) − 0.08·bounce − 0.04·entropy − 0.04·stall + label_boost`,
|
||||
label_boost = +0.06 CALM/DIRECTED_MEAN_REVERT, −0.06 CHOPPY; clamp [0,1].
|
||||
- `_v7_forecast_score` (:255-305) (inputs from v7 decision + curve metrics):
|
||||
`0.40 + 0.12·tanh(sign·vel_div_now/0.004) + 0.08·tanh(sign·ob_imbalance/0.75)
|
||||
+ 0.06·clamp(rv_comp/3,0,1) − 0.10·clamp(|bounce_score|,0,1) − 0.06·clamp(bounce_risk,0,1)
|
||||
− 0.10·clamp(mae_risk/2,0,1) − 0.08·clamp(mfe_risk/2,0,1) − 0.10·clamp(exit_pressure/3,0,1)
|
||||
− 0.08·drawdown_score − 0.05·slope_score − 0.05·accel_score + 0.08·recovery_score
|
||||
+ 0.14·clamp(market_support,0,1) + 0.04·clamp(current_fav/tp_scale,−1,1)`, clamp [0,1];
|
||||
where drawdown_score=clamp(dd/tp,0,1), slope_score=clamp(max(0,−slope)/(0.75·tp),0,1),
|
||||
accel_score=clamp(max(0,−accel)/(0.50·tp),0,1). `tp_scale = market_state_tp_pct`.
|
||||
- `_pnl_decay_score` (:308-313):
|
||||
`clamp(0.40·drawdown_score + 0.30·slope_score + 0.20·accel_score + 0.10·clamp(decay_bars/5,0,1), 0,1)`.
|
||||
- `_derived_exf_stress` (:316-327):
|
||||
`0.30·clamp(|funding|·5000,0,1) + 0.25·clamp((dvol−50)/50,0,1)
|
||||
+ 0.25·clamp((40−fear)/40,0,1) + 0.20·clamp((0.9−taker)/0.4,0,1)`.
|
||||
- `_market_state_from_inputs` (:545-590):
|
||||
`state_stress = clamp(0.34·choppy + 0.22·clamp(dd_pressure,0,1) + 0.18·(1−trend_persistence)
|
||||
+ 0.12·(1−compression) + 0.14·derived_exf_stress, 0,1)`;
|
||||
also returns market max_hold_bars (default 120 only when state present, else 0), profile
|
||||
label, confidence, tp_pct (default 0.0095), mean_reversion_strength.
|
||||
**Bug (cosmetic): annotated `tuple[float, int, str, float, float]` (5) but returns 6 values.**
|
||||
The `stress_fields` set (:555-566) is built and never used — dead code.
|
||||
|
||||
### 6.4 evaluate() decision logic (:592-803)
|
||||
|
||||
Inputs from the trader (trader:4847-4861): `side` from pending (SHORT unless overlay LONG),
|
||||
`recent_prices=_bounce_price_path(asset)`, `ae_shadow` = live AE shadow decision (carries
|
||||
`p_continuation`), `v7_decision = self._v7_decisions[tid]` (dict from AlphaExitEngineV7,
|
||||
fields `exit_pressure, mfe, mae, mfe_risk, mae_risk, bounce_score, bounce_risk, rv_comp,
|
||||
vel_div_now?, ob_imbalance?` — engine dict has no vel_div_now/ob_imbalance keys, so those
|
||||
forecast terms read 0.0), `market_state/bundle` from MarketStateRuntime, `exf_snapshot =
|
||||
self._last_exf`, `meta_performance = efsm.exit_policy_meta(maras_ctx)`.
|
||||
|
||||
Steps, in order:
|
||||
|
||||
1. `refresh_from_control_plane()` (15s TTL) — re-reads `advanced_sl_control` JSON from HZ
|
||||
features/state maps and merges into config (:519-531). The trader itself publishes that
|
||||
key at boot from its own config (trader:2590, publish :533-543) — circular; effectively
|
||||
a hot-reconfig hook nobody currently drives externally.
|
||||
2. `pressure = v7.exit_pressure`; if ≤0 fallback
|
||||
`clamp(1.25·max(0,−favorable) + 0.75·stall + 0.75·mean_reversion, 0, 4)` (:636-639) —
|
||||
a synthesized pressure on a different scale than V7's.
|
||||
3. `p_cont = ae_shadow.p_continuation|p_cont (default 0.5)` (:640).
|
||||
4. `snapback_score = clamp(0.60·mean_reversion_score + 0.25·max(0,momentum) + 0.15·mr_strength, 0,1)`
|
||||
(:648-654) — NOTE this **re-blends** `_mean_reversion_snapback_score`, which already
|
||||
internally blended 0.60/0.25/0.15; mr_strength and momentum are double-counted.
|
||||
5. `effective_min_hold = max(min_hold_bars(20), int(market_hold_bars·0.25))` (:655-658).
|
||||
6. `continuation_prob = clamp(0.50 + 0.35·favorable − 0.35·stress − 0.30·snapback
|
||||
+ 0.15·max(0,momentum) + 0.05·confidence, 0,1)` (:659-668); if ae_shadow has
|
||||
`p_continuation` the heuristic is REPLACED by it (:669-670), then unconditionally
|
||||
re-blended `0.55·prob + 0.45·p_cont` (:671-672) — so with ae_shadow present the final
|
||||
value is `0.55·p_cont + 0.45·p_cont = p_cont`… no: it's `0.55·(replaced=p_cont) +
|
||||
0.45·p_cont = p_cont`. With ae_shadow absent it's `0.55·heuristic + 0.45·0.5`.
|
||||
7. Meta/catastrophic block (:693-720), **dormant** (`catastrophic_mode_enabled=False` in
|
||||
live config — confirmed via snapshot file):
|
||||
`catastrophic_floor = clamp(0.0100 + meta.floor_adjustment, 0.0080, 0.0120)`;
|
||||
`tp_unlikely = bars≥max(20,eff_min_hold) ∧ lifetime_mfe < 0.35·tp ∧ favorable < 0.10·tp`;
|
||||
`meta_gate = support≥8 ∧ edge≥0`; `meta_unknown_allowed = support≤0`;
|
||||
`catastrophic_cut = enabled ∧ (adverse ≥ 0.0150 ∨ (adverse ≥ floor ∧ tp_unlikely ∧ (meta_gate ∨ meta_unknown_allowed)))`.
|
||||
`lifetime_mfe = max(v7.mfe, max(favorable_series), 0)` (:688-692).
|
||||
Meta source: `ExitMetaPerformanceTracker.lookup` (meta_exit_performance.py:60-76) —
|
||||
in-memory, keyed `hash:<composite>` or `regime:<maras_regime>`; returns
|
||||
`support_n, advsl_edge, win_rate, floor_adjustment ∈ {0, ±0.0010}` (max ±0.0020);
|
||||
exposed via `PostWinExecutionFSM.exit_policy_meta` (post_win_long_overlay.py:299-302).
|
||||
In-memory only ⇒ resets on every restart; support rarely reaches 8.
|
||||
8. Score (:721-731):
|
||||
`score = 0.30·clamp(pressure/1.85, 0, 2) + 0.30·(1−continuation_prob) + 0.20·stress
|
||||
+ 0.15·(1−snapback) + 0.05·stall + 0.05·clamp(|favorable|,0,1)
|
||||
+ 0.12·(1−market_support) + 0.12·(1−forecast) + 0.10·pnl_decay`.
|
||||
(Note the un-configured extra `0.05·|favorable|` term hardcoded inline.)
|
||||
9. Hard gates (:732-738): `bars_held ≥ effective_min_hold ∧ pressure ≥ 1.85 ∧
|
||||
continuation_prob ≤ 0.42 ∧ stress ≥ 0.52 ∧ snapback ≤ 0.56`;
|
||||
`would_exit = all(gates) ∧ score ≥ 0.70` (:739).
|
||||
10. `recovery_cut` (:740-749): same first four gates, plus `favorable ≥ 0`,
|
||||
`drawdown_from_peak ≥ max(0.10·tp, 0.0005)`, `recovery_ratio ≥ 0.95`, `score ≥ 0.70`
|
||||
(snapback gate waived) → forces would_exit. Never observed firing (no
|
||||
`ASL_RECOVERY_CUT` rows anywhere) — near-contradictory conditions
|
||||
(favorable ≥ 0 pushes continuation_prob UP via +0.35·favorable).
|
||||
11. `catastrophic_cut` forces would_exit (dormant, §7).
|
||||
12. Reason ladder (:754-772), first match:
|
||||
`ASL_CATASTROPHIC_FLOOR` → `ASL_MIN_HOLD` → `ASL_LOW_PRESSURE` →
|
||||
`ASL_GOOD_CONTINUATION` → `ASL_REGIME_NOT_STRESSED` → `ASL_RECOVERY_CUT` →
|
||||
`ASL_SNAPBACK_WINDOW` → `ASL_SCORE_LOW` → `ASL_PRESSURE_CONT_STRESS`.
|
||||
In 30 days of trade_events, the ONLY exit-firing reason observed is
|
||||
`ADVSL_ASL_PRESSURE_CONT_STRESS` (the generic all-gates-passed label).
|
||||
13. Returns frozen `AdvancedSLDecision` (:384-417): all scores + `action∈{EXIT,HOLD}`,
|
||||
`reason`, `min_hold_bars`, `effective_min_hold_bars`, full `config` dict,
|
||||
market-state echo fields, `exf_stress`, `would_exit`.
|
||||
|
||||
### 6.5 Multi-leg machinery — inside AdvancedSL: NONE
|
||||
|
||||
Despite the "multi-leg" billing, `advanced_sl.py` contains **no leg sizing, no schedule,
|
||||
no per-leg PnL, no residual re-watch**. Its live decision is binary (would_exit), and the
|
||||
trader always enqueues **`fraction: 1.0`** (trader:4894) — a FULL retraction. All actual
|
||||
multi-leg mechanics live in the trader's RETRACT pipeline (§8) and are shared with V7
|
||||
(fraction 0.50) and TUI hotkeys. The module's other half (`simulate_trade`, `run_backtest`,
|
||||
`sweep_configs`, `run_monitor_once`, `main`; :946-1203) is an offline replayer/calibration
|
||||
monitor (5-min loop) that replays `dolphin.trade_events` + `adaptive_exit_shadow` +
|
||||
`v7_decision_events`; **no such monitor process is currently running** (ps checked).
|
||||
|
||||
### 6.6 Snapshot / HZ / shadow-journal plumbing
|
||||
|
||||
- `bind_hz(features_map, state_map)` (:434-436) ← trader:2589 right after HZ connect;
|
||||
then `publish_control_plane()` (trader:2590) writes the config JSON to
|
||||
`advanced_sl_control` on both maps.
|
||||
- `log_shadow(decision, pnl_pct)` (:805-855), called per evaluation (trader:4862):
|
||||
1. INSERT row into `dolphin.advanced_sl_shadow` — silently swallowed on failure (:838-841);
|
||||
2. write full payload to snapshot file + HZ key `advanced_sl_latest` (:842-855).
|
||||
- `AdvancedSLRuntime.load()` (:438-457) at trader init (:456) runs `ensure_shadow_table()`
|
||||
+ control-plane refresh.
|
||||
|
||||
### 6.7 DORMANCY FINDING — the CH shadow journal has never worked
|
||||
|
||||
- `SELECT database,name FROM system.tables WHERE name ILIKE '%advanced%' OR name ILIKE '%advsl%'`
|
||||
→ **empty**. `dolphin.advanced_sl_shadow` does not exist in any database.
|
||||
- Root cause: `ensure_shadow_table()` (:459-499) sends its CREATE TABLE DDL through
|
||||
`_ch_query()`, which appends `FORMAT TSVWithNames` to any statement lacking the word
|
||||
FORMAT (:68-69). `CREATE TABLE … TTL … FORMAT TSVWithNames` is a ClickHouse syntax
|
||||
error → exception → `except Exception: pass` (:496-499). Permissions are NOT the issue
|
||||
(`SHOW GRANTS FOR dolphin` includes CREATE).
|
||||
- Consequence: **every** `log_shadow` CH insert and every monitor REPORT insert
|
||||
(:1127-1158) fails silently. The only durable traces of ADVSL decisions are:
|
||||
the one-row snapshot JSON (last decision overwrites; verified fresh — 2026-07-11 UTC,
|
||||
DOGEUSDT HOLD `ASL_GOOD_CONTINUATION`, pressure=3.0, continuation_prob=0.955, score=0.933,
|
||||
effective_min_hold=27), the HZ `advanced_sl_latest` key, the enqueue log lines, and the
|
||||
resulting trade_events/trade_exit_legs rows. There is no queryable per-decision history.
|
||||
|
||||
---
|
||||
|
||||
## 7. ADVSL live-exit gate + trader-side wiring
|
||||
|
||||
- Load: trader:56-58 (`from adaptive_exit.advanced_sl import AdvancedSLRuntime`, None on
|
||||
ImportError); instance :456; live flag :457 (`DOLPHIN_ENABLE_ADVANCED_SL_LIVE`, default
|
||||
False, **live =1**); if live, `config = replace(config, enabled=True)` :458-459 —
|
||||
cosmetic, `enabled` is never read as a gate. Boot log says
|
||||
`AdvancedSL: loaded (shadow prototype)` (:513) — stale label, it is a live exit source.
|
||||
- Per scan with open pendings, a **daemon thread** `_ae_eval` is spawned (trader:4815-4920):
|
||||
for each pending trade → AE shadow evaluate → `advanced_sl.evaluate(...)` (:4847) →
|
||||
`log_shadow` (:4862) → `_overlay_advsl_should_exit(...)` (:4866) →
|
||||
if `(advsl_live ∧ _adv.would_exit) ∨ overlay_exit` (:4876-4879):
|
||||
read-modify-write HZ `blue_runtime_commands` queue, append
|
||||
`{command_id: advsl-exit-<hex16>, trade_id, action: RETRACT, fraction: 1.0,
|
||||
reason: OVERLAY_ADVSL_<detail> | ADVSL_<reason>, source: "advanced_sl", ts, asset,
|
||||
chain_root/head/prev/seq/token from pending}` (:4890-4904), queue capped at last 200,
|
||||
log `AdvancedSL live exit enqueue: …` (:4908-4913).
|
||||
- **No dedup at the emitter**: while would_exit stays true, a fresh command (fresh
|
||||
command_id) is enqueued EVERY scan (~5s). Downstream absorbs the spam (§8 guards):
|
||||
`dolphin.hotkey_audit` lifetime RETRACT results: `NO_POSITION` ×48,257 (!),
|
||||
`PARTIAL_OK` ×2,513, `FULL_CLOSE` ×2,147, plus hundreds of
|
||||
`TRADE_MISMATCH open=<new> cmd=<old>` rows — commands landing after the position
|
||||
already closed or rolled to the next trade. Functional, but noisy by design.
|
||||
- Exceptions anywhere in the thread body are swallowed (`except Exception: pass`).
|
||||
|
||||
### Overlay-ADVSL gate `_overlay_advsl_should_exit` (trader:2523-2560)
|
||||
|
||||
Applies only to `overlay_flip` (EFSM LONG) trades, env-gated `DOLPHIN_OVERLAY_ADVSL_LIVE`
|
||||
(default True, live =1). Logic:
|
||||
|
||||
```
|
||||
bars_held ≥ 6 (OVERLAY_ADVSL_MIN_BARS)
|
||||
favorable = side-signed pnl pct; adverse = max(0, −favorable)
|
||||
lifetime_mfe = max(0, v7.mfe); pressure = v7.exit_pressure; mae_risk = v7.mae_risk
|
||||
floor_pct, floor_label = _catastrophic_floor_for_open_position() # overlay floor §4
|
||||
no_meaningful_mfe = lifetime_mfe ≤ 0.0020 (MFE_MAX_PCT)
|
||||
pressure_gate = pressure ≥ 1.85 ∧ mae_risk ≥ 0.50
|
||||
EXIT iff adverse ≥ floor_pct > 0 ∧ (no_meaningful_mfe ∨ pressure_gate)
|
||||
→ reason detail "{floor_label}:adverse=..:mfe=..:pressure=..:mae_risk=.."
|
||||
```
|
||||
|
||||
Reason string becomes `OVERLAY_ADVSL_{detail}`. **Dormant in practice**: zero
|
||||
`OVERLAY_ADVSL%` rows in 30 days of trade_events; zero log hits 2026-07-10/11.
|
||||
|
||||
---
|
||||
|
||||
## 8. How an ADVSL decision becomes an exit — the RETRACT pipeline
|
||||
|
||||
Emitters into HZ `DOLPHIN_CONTROL_PLANE["blue_runtime_commands"]` (JSON list, cap 200):
|
||||
|
||||
1. **advanced_sl** — fraction **1.0** (trader:4890-4907), reason `ADVSL_*`/`OVERLAY_ADVSL_*`.
|
||||
2. **v7** — fraction **0.50**, reason `V7_RETRACT`, on RETRACT-edge (only when previous
|
||||
action wasn't RETRACT — edge-triggered, unlike ADVSL) (trader:4494-4519). V7's RETRACT
|
||||
action originates in `alpha_exit_v7_engine.py` (~:743-756): exit_pressure above
|
||||
threshold with `mae ≥ composite_pressure_mae_floor`, reason `V7_RISK_DOMINANT`.
|
||||
3. **tui_hotkey** — operator partial/full retraction, reason `HOTKEY_RETRACT`
|
||||
(external writer; only test writers exist in-repo besides the trader:
|
||||
`prod/tests/test_malformed_open_distal.py`).
|
||||
|
||||
Drain: scan thread each bar, after step_bar and the V7 block —
|
||||
`_drain_runtime_commands(prices_dict)` (trader:4551, def :4004-4019, serialized by
|
||||
`_runtime_command_lock`) → `_process_runtime_commands` (:3926-4002): queue is atomically
|
||||
swapped to `[]` (or RETRACTs deferred when `allow_retract=False` — the heartbeat path
|
||||
:2633); idempotency via `_processed_retract_set` (command_id, deque 5000, :496-497) with
|
||||
`IDEMPOTENT_REPLAY` audit rows; each RETRACT → `_apply_internal_retract` + `hotkey_audit`
|
||||
row; `SET_CAPITAL/CAPITAL_UPDATE` also handled here.
|
||||
|
||||
`_apply_internal_retract(cmd, prices_dict)` (trader:3696-3924), under `eng_lock`:
|
||||
|
||||
- Guards (each returns a status string journaled to hotkey_audit): `NO_POSITION`,
|
||||
`NO_TRADE_ID`, `TRADE_MISMATCH`, `BAD_ENTRY_PRICE`, `ZERO_NOTIONAL`, `BAD_FRACTION`
|
||||
(frac must be in (0,1]), **chain-linkage gate**: command must carry chain_token +
|
||||
chain_head_leg_id + chain_root_trade_id matching the CURRENT
|
||||
`_chain_state_for_pending` digest (`NO_CHAIN_LINK` / `CHAIN_ROOT_MISMATCH` /
|
||||
`CHAIN_MISMATCH` / `CHAIN_SEQ_MISMATCH`, :3719-3736). This is what makes the
|
||||
every-scan enqueue spam safe: after one leg applies, the chain head/seq/token advance
|
||||
and every stale duplicate is rejected.
|
||||
- Leg economics (:3737-3767): `reduce_notional = open_notional·frac`;
|
||||
`pnl_pct_now = direction·(cur−entry)/entry`; `net_pnl_leg = pnl_pct_now·reduce_notional`;
|
||||
capital applied IMMEDIATELY per leg via `_apply_trade_capital_update` (:3747);
|
||||
`pos.notional -= reduce_notional`; pending updated:
|
||||
`retraction_legs += 1`, `realized_pnl_legs_total += net_pnl_leg`,
|
||||
`notional/quantity` reduced (qty recomputed `remaining_notional/entry_price`, 6dp),
|
||||
`notional_entry` preserved.
|
||||
- New chain leg id `f"{tid}:x{seq:03d}"`, chain state re-digested
|
||||
(`_build_chain_state` trader:352-394 — sha256 over the sorted anchor dict).
|
||||
- Journals, for EVERY leg including the terminal one (:3794-3881):
|
||||
`dolphin.trade_exit_legs` (full leg record incl. `exit_notional`,
|
||||
`remaining_notional/qty`, `pnl_pct_leg`, `pnl_leg`, `pnl_realized_total`),
|
||||
`dolphin.trade_events` with `event_type="PARTIAL_EXIT"` (even for the terminal leg),
|
||||
`dolphin.trade_reconstruction` (`FULL_RETRACT_EXIT` or `PARTIAL_EXIT`).
|
||||
- Full close when `remaining_notional ≤ POSITION_DUST_NOTIONAL_USD ∨ remaining_qty ≤ 0`
|
||||
(:3793): engine position cleared, exit-manager state popped, and a forced exit dict is
|
||||
built (`_build_retract_exit` :3510-3523) with
|
||||
`pnl_pct = realized_total/notional_entry`, `net_pnl = realized_total`, and
|
||||
`capital_already_realized: True` so the CLOSE finalizer does NOT re-book capital
|
||||
(checked via `_resolved_capital_apply_pnl`, log "close capital delta suppressed").
|
||||
- Partial remainder ("residual re-watch"): position simply stays open at reduced size —
|
||||
the exit-manager per-trade state (incl. stop_pct_override) is untouched, the
|
||||
catastrophic floor re-arms next scan, ADVSL/V7 keep evaluating the same trade_id, and
|
||||
the remainder is re-persisted through the canonical OPEN gate `_ps_write_open`
|
||||
(:3901-3915; refusal logged as accounting anomaly).
|
||||
- The forced exit dict re-enters the normal CLOSE finalizer (trader:4552-4770):
|
||||
full `trade_events` CLOSE row (event_id `{tid}:close`, `pnl = realized_pnl`,
|
||||
`pnl_realized_total` from pending), `trade_reconstruction` CLOSE,
|
||||
`_ps_write_closed`, EFSM/market-state/advisor outcome updates, announcements.
|
||||
**Net observable shape of one full ADVSL exit: 1 trade_exit_legs row + 1 PARTIAL_EXIT
|
||||
trade_events row + 1 CLOSE trade_events row** (same reason string on all three).
|
||||
|
||||
### Multi-leg event fields (trader:850-901) — emitters and consumers
|
||||
|
||||
Fields: `retraction_legs`, `retraction_realized_total`, `pnl_leg`, `pnl_realized_total`,
|
||||
`exit_leg_id`, `exit_seq`, `exit_notional`, `remaining_notional`, `remaining_qty`,
|
||||
plus the `chain_*` family.
|
||||
|
||||
- EMITTED by `_trade_event_payload` at: PARTIAL_EXIT (:3829), CLOSE (:4715), and the
|
||||
SUBDAY_EXIT path (`SUBDAY_ACB_NORMALIZATION`, ~:5070); by `trade_exit_legs` put (:3798);
|
||||
by `trade_reconstruction` puts (:3860, :4742); mirrored into the HZ state push
|
||||
(:5245-5251) and `advanced_sl.snapshot_dict` into state (:5293).
|
||||
- CONSUMED by: (a) BLUE's own restore — both restore paths read
|
||||
`retraction_legs`/`realized_pnl_legs_total` back out of the latest
|
||||
`trade_reconstruction` payload (`_load_chain_ledger_state` :3602, seeded at :1992-1993
|
||||
and :2258-2259) so a restarted BLUE continues the chain with correct seq/token;
|
||||
(b) the chain-token verification itself (every future RETRACT is validated against the
|
||||
digest of these fields); (c) downstream parity stack — VIOLET DDL
|
||||
`prod/clickhouse/violet/03_trade_exit_legs.sql`, `prod/launch_dolphin_violet.py`,
|
||||
`prod/clean_arch/persistence/pink_clickhouse.py`, dita_v2 docs/tests, and the UV cert
|
||||
docs (`prod/docs/uv_cert/UV_EXIT_DOCTRINE_PARITY_PASS_20260712.md`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Precedence / interaction matrix
|
||||
|
||||
| # | Mechanism | Acts through | Threshold (live) | Wins over / loses to |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Withdrawal-stress hard SL | exit_manager, per-bar, overwrites `dynamic_sl_pct=0.10` | −10% while `macro.regime_signal==1` | WIDENS the armed floor for the stress bar(s) — floor value in `st` untouched, restored next non-stress bar |
|
||||
| 2 | Catastrophic floor (base) | `stop_pct_override` → STOP_LOSS | −1.2% | tighten-only ratchet, re-armed every scan; overrides wide bucket SLs |
|
||||
| 3 | Overlay floor + USD cap | same override | −min(1.2%, 0.5%, 500/notional) | overlay_flip trades only |
|
||||
| 4 | Hibernate bucket SL | same override, unconditional write | −0.8%…−3.0% by bucket | wide buckets clawed back to 1.2% by #2 next scan; tight buckets (0.8%) survive |
|
||||
| 5 | ADVSL live exit | RETRACT frac 1.0, async queue | 5 hard gates + score ≥ 0.70 | loses same-bar to any engine exit (4552); chain-token race-safe |
|
||||
| 6 | Overlay-ADVSL | RETRACT frac 1.0 | adverse ≥ overlay floor + mfe/pressure gate | overlay trades only; observed never |
|
||||
| 7 | V7 RETRACT | RETRACT frac 0.50 | exit_pressure > threshold, edge-triggered | halves position; ADVSL/floor keep watching remainder |
|
||||
| 8 | ASL catastrophic block | would_exit inside evaluate() | −1.0%..−1.5% meta-adjusted | **dormant** (`catastrophic_mode_enabled=false`) |
|
||||
| 9 | exit_manager global stop_pct | evaluate() fallback | −100% | never fires |
|
||||
|
||||
---
|
||||
|
||||
## 10. Dormancy scoreboard (evidence-backed)
|
||||
|
||||
| Mechanism | Status | Evidence |
|
||||
|---|---|---|
|
||||
| STOP_LOSS via catastrophic floor | **LIVE, firing** | 204 CLOSEs / 30d, avg −1.178% ≈ floor; `CATASTROPHIC_FLOOR armed … SL=1.20% mode=base` in 2026-07-11 log |
|
||||
| ADVSL live full-retract exits | **LIVE, firing heavily** | trade_events 30d: `ADVSL_ASL_PRESSURE_CONT_STRESS` CLOSE ×1347 + PARTIAL_EXIT ×965 (since 2026-06-15); trade_exit_legs 14d: 686 legs, **Σ pnl_leg = −$53,425.56** (it is a loss-cutter); 5 enqueue log lines on 2026-07-10, 0 on 2026-07-11 |
|
||||
| V7_RETRACT half-legs | LIVE | 1301 PARTIAL_EXIT / 30d; 735 legs / 14d, Σ −$14,920.55 |
|
||||
| HOTKEY_RETRACT | LIVE (operator) | 279 CLOSE + 248 PARTIAL_EXIT / 30d; 204 legs / 14d, Σ +$7,788.56 |
|
||||
| ADVSL CH shadow journal | **DEAD since inception** | table absent everywhere; FORMAT-append DDL bug (§6.7) |
|
||||
| ADVSL reasons other than PRESSURE_CONT_STRESS firing an exit | never | only reason string in trade_events; `ASL_RECOVERY_CUT`/`ASL_CATASTROPHIC_FLOOR` absent |
|
||||
| ASL catastrophic mode | dormant | `catastrophic_mode_enabled: false` in live snapshot config |
|
||||
| Overlay-ADVSL | armed, never fired | 0 `OVERLAY_ADVSL%` rows / 30d; 0 log hits |
|
||||
| Overlay floor / USD cap | armed, no observed arms | no `mode=overlay:` log lines in sample |
|
||||
| Hibernate protect | rare + partially broken | only `HIBERNATE_HIBERNATE_HALT` ×7 (2026-06-25); no protect lines in July logs |
|
||||
| vd / tf exits in exit manager | dormant | `vd_enabled=False`, `tf_enabled=False` defaults |
|
||||
| exit_meta floor_adjustment | effectively inert | in-memory only, resets on restart, min_support=8 |
|
||||
| ADVSL calibration monitor (`main --once` loop) | not running | ps; REPORT inserts would silently fail anyway |
|
||||
| TP_FLOOR (context) | LIVE | 1510 CLOSEs / 30d |
|
||||
|
||||
Curiosity: `EXP_VOL_GATE_RELAX_DO_NOT_ACCOUNT|<reason>` prefixed variants of ADVSL /
|
||||
STOP_LOSS / TP_FLOOR reasons appear only on 2026-07-04 (a gate-relax experiment day) —
|
||||
port must tolerate prefixed reason strings.
|
||||
|
||||
---
|
||||
|
||||
## 11. Weirdness ledger (do NOT idealize — port the behavior, flag the mess)
|
||||
|
||||
1. **Silent-dead CH journal** (§6.7): `_ch_query` appends `FORMAT TSVWithNames` to the
|
||||
CREATE TABLE DDL → table never created → every shadow insert fails inside
|
||||
`except: pass`. The system's only decision history is a single overwritten JSON file.
|
||||
2. **Double event per ADVSL exit**: terminal retract leg writes `PARTIAL_EXIT` trade_events
|
||||
+ forced CLOSE writes a second row. Capital is NOT double-booked
|
||||
(`capital_already_realized`), but naive row counting double-counts exits
|
||||
(1347 CLOSE vs 965 PARTIAL_EXIT — the mismatch is pre-2026-06-18 code that skipped
|
||||
terminal-leg events, per the comment at trader:3794-3797).
|
||||
3. **Emitter spam absorbed downstream**: ADVSL enqueues a fresh full-retract command every
|
||||
scan while would_exit holds (no per-trade edge trigger, unlike V7). 48k `NO_POSITION`
|
||||
+ chain-token rejections are the working-as-coded garbage collector.
|
||||
4. **`enabled` config flag is decorative**; the real gate is the trader-side env bool.
|
||||
Boot log still calls ADVSL a "shadow prototype".
|
||||
5. **Reason ladder mostly labels HOLDs**; the only exit label in the wild is the generic
|
||||
`ASL_PRESSURE_CONT_STRESS`. Score weights sum to ≈1.39 (three bolt-on terms + a
|
||||
hardcoded `0.05·|favorable|`), so `cut_score_min=0.70` is not on a 0-1 scale.
|
||||
6. **Snapback double-blend** (§6.4 step 4) — mr_strength/momentum counted twice.
|
||||
7. **`continuation_prob` triple pipeline** (§6.4 step 6) — heuristic, replaced by
|
||||
p_continuation if present, then re-blended with the same p_cont (identity when
|
||||
ae_shadow present).
|
||||
8. **`_market_state_from_inputs` returns 6 values against a 5-tuple annotation**; its
|
||||
`stress_fields` set is dead code; `time_at_price` is computed and journaled but unused.
|
||||
9. **v7 dict keys mismatch**: `_v7_forecast_score` reads `vel_div_now`/`ob_imbalance`/
|
||||
`bounce_risk`/`rv_comp` — `rv_comp`, `bounce_risk` exist in the V7 dict, but
|
||||
`vel_div_now`/`ob_imbalance` do not (journal rows have them, the in-memory decision
|
||||
dict does not) → those two forecast terms are always 0 live.
|
||||
10. **Withdrawal-stress bar wins over the floor**: `dynamic_sl_pct = 0.10` overwrite at
|
||||
alpha_exit_manager:183 temporarily widens an armed 1.2% floor to 10% during macro
|
||||
withdrawal-stress bars.
|
||||
11. **Wide bucket SLs are unreachable**: hibernate-protect writes them unconditionally
|
||||
(trader:2443) but the every-scan tighten-only floor re-arm claws anything >1.2% back.
|
||||
12. **`HIBERNATE_HIBERNATE_HALT`**: relabel map (trader:4561) lacks `HIBERNATE_HALT`, so
|
||||
the fallback double-prefixes; the 7 such closes also show protect did not prevent the
|
||||
halt.
|
||||
13. **`stop_pct_override` uses `or`**, so `0.0` cannot disable the stop (falls back to 1.0).
|
||||
14. **Daemon-thread evaluation**: one fire-and-forget thread per scan; slow CH/HZ can stack
|
||||
threads; all failures invisible.
|
||||
15. **Meta layer is amnesiac**: `ExitMetaPerformanceTracker` is process-memory only —
|
||||
`floor_adjustment` never survives a restart and effectively never leaves 0.
|
||||
16. **Control-plane self-loop**: the trader publishes `advanced_sl_control` from its own
|
||||
defaults at boot, then re-reads it every 15s; an external writer COULD hot-retune every
|
||||
threshold in §6.2 (including enabling catastrophic mode) with no audit trail.
|
||||
|
||||
---
|
||||
|
||||
## 12. Parity-port checklist (UV)
|
||||
|
||||
- Port `AlphaExitManager.evaluate` ordering exactly (TP_FLOOR before everything; SL after
|
||||
FIXED_TP; MAX_HOLD last) with the OB modulation table, incl. the 0.10 withdrawal stop.
|
||||
- Port floor arming as *state on the exit manager*, re-asserted every decision tick with
|
||||
tighten-only semantics; entry + both restore paths must arm it (restore also passes it
|
||||
directly to setup_position).
|
||||
- Port the RETRACT command shape (command_id/trade_id/action/fraction/reason/source/ts/
|
||||
asset/chain_* fields), chain digest (`sha256` over the sorted anchor dict, trader:346-394),
|
||||
and all guard statuses — UV replay tooling keys off `hotkey_audit.result` strings.
|
||||
- ADVSL evaluate() must reproduce the formulas in §6.3-6.4 bit-for-bit if RESIDUAL=0 is the
|
||||
target — including the double-blends, the dead terms, and the >1 weight sum. Do not
|
||||
"fix" them in the port; fix upstream first, then re-vendor.
|
||||
- Reasons to expect in fills/journals: `STOP_LOSS`, `ADVSL_ASL_PRESSURE_CONT_STRESS`,
|
||||
`V7_RETRACT`, `HOTKEY_RETRACT`, `TP_FLOOR`, `HIBERNATE_*` (incl. the double-prefix),
|
||||
optional `OVERLAY_ADVSL_*`, `EXP_…|`-prefixed variants.
|
||||
- If the port stands up its own `advanced_sl_shadow`, use the DDL at advanced_sl.py:460-495
|
||||
but do NOT route it through `_ch_query` (the FORMAT bug); decide deliberately whether to
|
||||
reproduce the missing-table behavior or repair it.
|
||||
585
prod/docs/uv_cert/exit_crawl/BLUE_TP_CRAWL.md
Normal file
585
prod/docs/uv_cert/exit_crawl/BLUE_TP_CRAWL.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# BLUE TAKE-PROFIT CODEPATH CRAWL — exhaustive, parity-port grade
|
||||
|
||||
- **Date:** 2026-07-12 (UTC)
|
||||
- **Method:** parser-grade read of every import/call that touches TP, from scan ingress to the
|
||||
comparison that fires `FIXED_TP`, plus empirical dormancy verification (live logs + ClickHouse).
|
||||
- **Scope:** READ-ONLY crawl. Nothing was modified, restarted, or written to any DB.
|
||||
- **Live process verified:** pid 431423, `/home/dolphin/siloqy_env/bin/python3
|
||||
/mnt/dolphinng5_predict/prod/nautilus_event_trader.py`, cwd `/mnt/dolphinng5_predict/prod`,
|
||||
`DOLPHIN_LOG_ROOT=/tmp/dolphin_logs`. Live stdout log:
|
||||
`/tmp/dolphin_logs/supervisor/nautilus_trader.log` (starts 2026-06-20T11:29Z, active at crawl
|
||||
time 2026-07-12T10:52Z). **Trap:** `/root/dolphin_logs/supervisor/nautilus_trader.log` is a
|
||||
STALE copy ending 2026-05-16 (pre-TP-diagnostics era) — do not use it as evidence.
|
||||
Note: both logs are ISO-stamped `+00:00` (UTC), same clock frame as ClickHouse `ts`.
|
||||
|
||||
Files crawled (canonical, as imported by the live process — `PYTHONPATH` includes
|
||||
`/mnt/dolphinng5_predict/nautilus_dolphin`):
|
||||
|
||||
| Role | Path |
|
||||
|---|---|
|
||||
| Live trader (scan loop) | `/mnt/dolphinng5_predict/prod/nautilus_event_trader.py` |
|
||||
| Soft TP curve | `/mnt/dolphinng5_predict/prod/clean_arch/tp_curve.py` |
|
||||
| OBF mid-price injection helper | `/mnt/dolphinng5_predict/prod/clean_arch/obf_tp_observation.py` |
|
||||
| Exit manager (TP comparison) | `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/alpha_exit_manager.py` |
|
||||
| Orchestrator / engine base (`NDAlphaEngine`) | `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/esf_alpha_orchestrator.py` |
|
||||
| Engine factory + subclass ladder | `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/proxy_boost_engine.py` |
|
||||
| OB feature engine (cascade/withdrawal macro) | `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/ob_features.py` |
|
||||
| Live OB provider (Hazelcast) | `/mnt/dolphinng5_predict/nautilus_dolphin/nautilus_dolphin/nautilus/hz_ob_provider.py` |
|
||||
|
||||
---
|
||||
|
||||
## 1. Where the TP constant is born and how it reaches the exit manager
|
||||
|
||||
### 1.1 `ENGINE_KWARGS` (trader :130-143)
|
||||
|
||||
```python
|
||||
ENGINE_KWARGS = dict(
|
||||
initial_capital=25000.0, ...
|
||||
fraction=0.20, fixed_tp_pct=0.0020, stop_pct=1.0, max_hold_bars=250, # TP research 2026-05-11: 0.95→0.20%
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
- `fixed_tp_pct = 0.0020` (0.20%). `stop_pct = 1.0` (100% — SL structurally disabled at this layer;
|
||||
real stops come from override machinery, §5). `max_hold_bars = 250`.
|
||||
- Trader caches the base at construction: `self._tp_base_pct = float(ENGINE_KWARGS.get("fixed_tp_pct", 0.0020))`
|
||||
(trader :455). This cached value — NOT the engine's current threshold — is the anchor for the soft
|
||||
curve (§2), so runtime tightening never compounds.
|
||||
|
||||
### 1.2 Engine construction chain (trader `_build_engine` :599-623)
|
||||
|
||||
```
|
||||
trader:606 self.eng = create_d_liq_engine(**engine_kwargs)
|
||||
proxy_boost_engine:443-462 create_d_liq_engine() -> LiquidationGuardEngine(
|
||||
extended_soft_cap=8.0, extended_abs_cap=9.0,
|
||||
mc_leverage_ref=5.0, margin_buffer=0.95,
|
||||
adaptive_beta=True, **engine_kwargs)
|
||||
class ladder: LiquidationGuardEngine(:392) -> ExtendedLeverageEngine(:311)
|
||||
-> AdaptiveBoostEngine(:209) -> ProxyBaseEngine(:110) -> NDAlphaEngine
|
||||
NDAlphaEngine = class at esf_alpha_orchestrator.py:97 (constructor :100-269)
|
||||
```
|
||||
|
||||
- `fixed_tp_pct` flows untouched through every subclass `**kwargs` into
|
||||
`NDAlphaEngine.__init__(fixed_tp_pct=0.0020)` (orchestrator :115, default there is the legacy
|
||||
champion 0.0099 — overridden by ENGINE_KWARGS).
|
||||
- Orchestrator :193-202 constructs the exit manager:
|
||||
|
||||
```python
|
||||
self.exit_manager = AlphaExitManager(
|
||||
fixed_tp_pct=fixed_tp_pct, stop_pct=stop_pct, max_hold_bars=max_hold_bars,
|
||||
tf_exhaust_ratio=..., tf_flip_ratio=..., tf_consec_bars=..., tf_min_hold_frac=...,
|
||||
tf_enabled=tf_enabled, # default False; ENGINE_KWARGS does not set it → False
|
||||
)
|
||||
```
|
||||
|
||||
- `AlphaExitManager.__init__` (aem :25-80): defaults `fixed_tp_pct=0.0099`, `stop_pct=1.0`,
|
||||
`max_hold_bars=120`, `vd_enabled=False`, `tf_enabled=False`, `tp_floor_enabled=False`,
|
||||
`self.ob_engine = None` (aem :74 — **the attribute always exists**, see §7),
|
||||
`self.last_eval = {}` (aem :80 — per-decision TP diagnostics).
|
||||
|
||||
### 1.3 TP_FLOOR enable (trader :607-614)
|
||||
|
||||
```python
|
||||
# trader:612
|
||||
self.eng.exit_manager.tp_floor_enabled = _env_bool("DOLPHIN_TP_FLOOR", True)
|
||||
```
|
||||
|
||||
- Class default OFF (backtest/champion parity, aem :57); **live default ON** unless env
|
||||
`DOLPHIN_TP_FLOOR=0`. Boot log line `TP profit-floor: ON` confirmed 53× in the live log (one per
|
||||
restart) — currently ON.
|
||||
- **PARITY TRAP:** `NDAlphaEngine.reset()` (orchestrator :697-755) rebuilds `AlphaExitManager`
|
||||
(:726-740) forwarding `fixed_tp_pct/stop_pct/max_hold/tf_*/vd_*` but **NOT `tp_floor_enabled`**
|
||||
— a reset would silently revert TP_FLOOR to OFF. Mitigation in practice: the live trader never
|
||||
calls `eng.reset()` (grep: 0 call sites in `nautilus_event_trader.py`). A port that resets
|
||||
per-day must re-apply the flag.
|
||||
|
||||
---
|
||||
|
||||
## 2. Runtime TP threshold: the leverage-conditioned soft curve
|
||||
|
||||
### 2.1 `tp_curve.py` — transcribed verbatim (constants :13-16, functions :33-64)
|
||||
|
||||
```python
|
||||
DEFAULT_SOFT_TP_LEVERAGE_REF = 2.0
|
||||
DEFAULT_SOFT_TP_CURVE_POWER = 3.0
|
||||
DEFAULT_SOFT_TP_TIGHTEN_AT_REF = 0.00013
|
||||
DEFAULT_SOFT_TP_FLOOR_PCT = 0.00187
|
||||
|
||||
def compute_our_leverage(*, notional, capital) -> float:
|
||||
notional_f = abs(_safe_float(notional, 0.0))
|
||||
capital_f = _safe_float(capital, 0.0)
|
||||
if capital_f <= 0.0: return 0.0
|
||||
value = notional_f / capital_f
|
||||
return value if math.isfinite(value) and value >= 0.0 else 0.0
|
||||
|
||||
def compute_soft_tp_pct(base_tp_pct, our_leverage, *,
|
||||
reference_leverage=2.0, curve_power=3.0,
|
||||
tighten_at_reference=0.00013, floor_pct=0.00187) -> float:
|
||||
base = max(0.0, _safe_float(base_tp_pct, 0.0))
|
||||
lev = max(0.0, _safe_float(our_leverage, 0.0))
|
||||
ref = max(1e-9, _safe_float(reference_leverage, 2.0))
|
||||
power = max(1.0, _safe_float(curve_power, 3.0))
|
||||
tighten = max(0.0, _safe_float(tighten_at_reference, 0.00013))
|
||||
x = _clamp(lev / ref, 0.0, 1.0)
|
||||
effective = base - tighten * (x ** power)
|
||||
return _clamp(effective, max(0.0, floor_pct), base if base > 0.0 else float("inf"))
|
||||
```
|
||||
|
||||
- Formula: `tp_eff = clamp(0.0020 − 0.00013·min(lev/2, 1)³, 0.00187, 0.0020)`.
|
||||
"Leverage" here = `position_notional / capital` (system sizing leverage, NOT exchange leverage).
|
||||
Range is tiny by design: 0.20% at flat/low leverage, bending to 0.187% at lev ≥ 2.0.
|
||||
`_safe_float` maps NaN/inf/non-numeric → default; negative leverage → 0.
|
||||
|
||||
### 2.2 `_tp_curve_context` (trader :805-832)
|
||||
|
||||
- `capital = eng.capital`; `notional`: explicit arg if given, else open position's
|
||||
`pos.notional` (fallback `size*entry_price`), else 0.0 (no position → `lev=0` → `tp_eff=base`).
|
||||
- Returns `{tp_base_pct, tp_effective_pct, our_leverage, market_state_bundle_json, **maras_ctx}`.
|
||||
- Called from 3 places: `_sync_tp_threshold` (:914, notional=None → live position), entry snapshot
|
||||
(:4257, notional=entry notional), close/retract journal enrichment (:4714 and the RETRACT leg
|
||||
path, notional=pending notional).
|
||||
|
||||
### 2.3 `_sync_tp_threshold` (trader :906-926) — the ONLY runtime writer of the engine threshold
|
||||
|
||||
```python
|
||||
ctx = self._tp_curve_context()
|
||||
tp_pct = float(ctx.get("tp_effective_pct", 0.0) or 0.0)
|
||||
if tp_pct <= 0: return
|
||||
with self.eng_lock:
|
||||
old = self.eng.set_live_tp_pct(tp_pct)
|
||||
if abs(old - tp_pct) > 1e-6:
|
||||
log(f"TP threshold: {old*100:.2f}% → {tp_pct*100:.2f}% (soft curve, lev={...:.2f}x)")
|
||||
```
|
||||
|
||||
- Docstring mentions an HZ key `DOLPHIN_FEATURES["live_tp_threshold"]` — **stale doc**: the body
|
||||
reads no HZ key; the value comes purely from the soft curve. Whole body is `try/except: pass`.
|
||||
- Forwarding chain: `NDAlphaEngine.set_live_tp_pct` (orchestrator :860-866) →
|
||||
`AlphaExitManager.set_live_tp_pct` (aem :82-98):
|
||||
|
||||
```python
|
||||
def set_live_tp_pct(self, tp_pct: float) -> float:
|
||||
tp_pct = float(tp_pct)
|
||||
if tp_pct <= 0: return self.fixed_tp_pct
|
||||
old = self.fixed_tp_pct
|
||||
self.fixed_tp_pct = tp_pct
|
||||
for st in self._positions.values():
|
||||
st["tp_pct_override"] = tp_pct # mirror onto every open position
|
||||
return old
|
||||
```
|
||||
|
||||
- So in live operation `fixed_tp_pct` and every open position's `tp_pct_override` are always the
|
||||
same soft-curve value. Called once per scan (§3), i.e. every ~11 s bar.
|
||||
- Empirical: 420 `TP threshold:` transitions in the live log since 2026-06-20, oscillating
|
||||
0.20% ↔ 0.19% with `lev` up to 1.80x. Journal `tp_base_pct` distribution (CH): 0.00200
|
||||
(267,453 rows) dominant, then 0.00191 (25,175), 0.00199, 0.00195, … — matches the curve's
|
||||
0.0020→0.001905 range at lev≈1.8. The 0.00187 floor requires lev ≥ 2.0.
|
||||
|
||||
---
|
||||
|
||||
## 3. Scan loop → TP comparison: the full call chain
|
||||
|
||||
All in `_process_scan` (trader :4051-4925), submitted from the HZ reactor via
|
||||
`on_scan` :4043-4049 to a worker executor (:4049).
|
||||
|
||||
```
|
||||
on_scan(event) trader:4043 (reactor thread → executor)
|
||||
└─ _process_scan(event, listener_time) trader:4051
|
||||
├─ dedup ratchet / NG7 normalize :4056-4088
|
||||
├─ _wire_obf(assets) [first scan only] :4094-4095 → §7.1
|
||||
├─ prices_dict = dict(zip(assets, prices)) :4102 (stablecoins stripped :4105-4106)
|
||||
├─ ob_eng.step_live(ob_assets, bar_idx) :4124-4125 (live OB features for THIS bar) → §7.2
|
||||
├─ posture sync (HIBERNATE protect arm) :4128-4161 → §5.3
|
||||
├─ _sync_esof_size_gate() :4164
|
||||
├─ _sync_tp_threshold() :4165 ← soft curve applied BEFORE step_bar
|
||||
├─ _apply_runtime_direction() :4168
|
||||
├─ if eng.position and prices_dict:
|
||||
│ prices_dict = _inject_obf_midprice(prices_dict) :4187-4188 → §4
|
||||
├─ with eng_lock:
|
||||
│ _apply_catastrophic_floor_to_open_position() :4192 (SL-side only, §5.5)
|
||||
│ result = eng.step_bar(bar_idx, vel_div, prices_dict, vol_ok, v50, v750) :4193-4196
|
||||
│ bar_idx += 1
|
||||
├─ entry handling: pending_entries[tid].update(_tp_curve_context(notional=entry_notional)) :4257-4258
|
||||
├─ V7 open-scan evaluate + journal (tp diag from exit_manager.last_eval) :4440-4549, §6
|
||||
├─ _drain_runtime_commands(prices_dict) → forced exits bypass TP entirely :4551-4553
|
||||
└─ exit handling: TP_EXIT diagnostics log + trade_events payload :4555-4588, §6.2
|
||||
```
|
||||
|
||||
Inside the engine (`step_bar`, orchestrator :272-321 — wrapper that appends price histories,
|
||||
updates `regime_size_mult`, then `process_bar` :323-355 with the PERSISTENT `self._global_bar_idx`;
|
||||
note the trader-passed `bar_idx` is ignored by the core in favour of `_global_bar_idx`):
|
||||
|
||||
```
|
||||
process_bar orchestrator:323
|
||||
├─ if position: _manage_position(bar_idx, prices, vel_div, v50, v750) :343-346
|
||||
│ ├─ pos.current_price = prices[pos.asset] :362-363 ← OBF-injected mid lands HERE
|
||||
│ ├─ if _day_posture == 'HIBERNATE': decision = EXIT/HIBERNATE_HALT :365-371 (bypasses TP)
|
||||
│ ├─ provider = self.exit_decision_provider :373 (V7 live exit hook, §6.1)
|
||||
│ │ decision = provider(...) :375-386
|
||||
│ │ only a provider EXIT is accepted; HOLD/RETRACT/None falls through :387-389
|
||||
│ ├─ decision = exit_manager.evaluate(pos.trade_id, pos.current_price, bar_idx,
|
||||
│ │ asset=pos.asset, current_timestamp=float(bar_idx),
|
||||
│ │ regime_size_mult=..., vel_div=..., v50_vel=..., v750_vel=...) :390-395
|
||||
│ └─ if EXIT: _execute_exit(reason, bar_idx, pnl_pct_raw, bars_held) :397-404
|
||||
└─ if no position and bar_idx > _last_exit_bar and not regime_dd_halt
|
||||
and _bar_count >= lookback and vol_ok: _try_entry(...) :348-353
|
||||
```
|
||||
|
||||
`_execute_exit` (orchestrator :406-503) — TP-relevant mechanics:
|
||||
|
||||
- `FIXED_TP` slippage 0.0002 applied to `pos.current_price` (:416-418, :432); pnl **recomputed**
|
||||
from that slipped exit price (:433-434), then SP maker refunds (+0.0002 with p=0.62 entry /
|
||||
p=0.50 exit, :437-441), then OB edge bonus if `ob_engine.get_placement(...).depth_quality > 0.5`
|
||||
(+`fill_probability*5bps`, :444-448), then blended fees (:459-466).
|
||||
- `exit_manager.reset_position(trade_id)` (:491) clears per-trade override state.
|
||||
- **The returned exit dict (:495-503) drops all of evaluate()'s TP diag keys** — which is why the
|
||||
trader re-reads `exit_manager.last_eval` at :4582 (§6.2).
|
||||
- Exit-reason counters: `FIXED_TP` → `tp_exits` (:416-418 / :428).
|
||||
|
||||
---
|
||||
|
||||
## 4. What price the TP comparison sees — OBF mid-price injection
|
||||
|
||||
### 4.1 `_inject_obf_midprice` (trader :928-951)
|
||||
|
||||
- Runs only when a position is open and prices arrived (:4187). Reads HZ features map key
|
||||
`"obf_universe_latest"` (`self.features_map.blocking().get(...)`, :942) — the OBF universe
|
||||
service's live WebSocket book (~0.5 s resolution, full precision) — and calls the pure helper
|
||||
with `max_age_s=3.0, now_s=time.time()`. Entire body `try/except → return prices_dict` unchanged.
|
||||
|
||||
### 4.2 `inject_obf_midprice` (obf_tp_observation.py :51-96) — exact semantics
|
||||
|
||||
1. No position asset / no payload / JSON-parse failure / non-dict → return copy unchanged.
|
||||
2. Staleness: payload timestamp from first present of `_snapshot_utc, updated_at, timestamp, ts,
|
||||
iso` (:42-48; ints/floats used raw, ISO strings parsed, naive → UTC). If found and
|
||||
`now − ts > 3.0 s` → unchanged. **If no timestamp field found, the payload is treated as fresh.**
|
||||
3. Per-asset blob must be a dict with `best_bid > 0` and `best_ask > 0` (poison-safe `_safe_float`,
|
||||
:88-91), else unchanged.
|
||||
4. `out[position_asset] = (best_bid + best_ask)/2` — **only the position's asset** is overridden;
|
||||
the scan price (quantized ~4 dp) is used for everything else. The TP *threshold* is untouched;
|
||||
only the observation is refreshed.
|
||||
|
||||
Consequence: the value compared against the TP gate is
|
||||
`pnl_pct = direction · (OBF_mid − entry_price)/entry_price`, and the same OBF mid becomes the base
|
||||
of the slipped exit price in `_execute_exit`. Entry prices, by contrast, come from the raw scan
|
||||
tape (`_try_entry` :622 `entry_price = prices.get(trade_asset, 0)` — no injection, no entry slip
|
||||
per :621).
|
||||
|
||||
---
|
||||
|
||||
## 5. Per-trade override machinery — every writer of `tp_pct_override` / `stop_pct_override` / `max_hold_override`
|
||||
|
||||
`AlphaExitManager.setup_position` (aem :100-119) initializes per-trade state:
|
||||
|
||||
```python
|
||||
self._positions[trade_id] = {
|
||||
"entry_price":..., "direction":..., "entry_bar":..., "max_favorable": 0.0,
|
||||
"vd_inv_count":0, "vd_exh_count":0, "entry_tf_spread": v50−v750, "tf_exh_count":0, "tf_flip_count":0,
|
||||
"stop_pct_override": stop_pct_override, # None = global self.stop_pct
|
||||
"tp_pct_override": tp_pct_override, # exp15
|
||||
"max_hold_override": max_hold_override, # exp15
|
||||
}
|
||||
```
|
||||
|
||||
### 5.1 Entry-time overrides (exp15 machinery) — orchestrator :246-249, :642-656
|
||||
|
||||
```python
|
||||
# consumed in _try_entry:
|
||||
_stop_ov = getattr(self, '_pending_stop_override', None)
|
||||
_tp_ov = getattr(self, '_pending_tp_override', None)
|
||||
_hold_ov = getattr(self, '_pending_max_hold_override', None)
|
||||
self._pending_* = None # one-shot consume
|
||||
self.exit_manager.setup_position(trade_id, entry_price, trade_direction, bar_idx,
|
||||
entry_v50=v50_vel, entry_v750=v750_vel,
|
||||
stop_pct_override=_stop_ov,
|
||||
tp_pct_override=_tp_ov, max_hold_override=_hold_ov)
|
||||
```
|
||||
|
||||
- **`_pending_tp_override` / `_pending_max_hold_override` are set NOWHERE in production code.**
|
||||
Exhaustive grep across `nautilus_dolphin/` finds writers only in
|
||||
`esf_alpha_orchestrator_AGENT_fork.py` (:218-219, :580-584) — a research fork not imported by
|
||||
the live trader, and the trader itself contains zero `tp_pct_override` references. The exp15 TP
|
||||
override machinery is **plumbing-only / DORMANT in live**: at entry, `tp_pct_override` is always
|
||||
`None`.
|
||||
- `_pending_stop_override` IS set live: `LiquidationGuardEngine._try_entry`
|
||||
(proxy_boost_engine :416-420) sets it to `_liq_stop_pct = (1/9)·0.95 = 0.10556` (:413) before
|
||||
every entry — the 10.6% liquidation-floor stop. TP-side untouched.
|
||||
|
||||
### 5.2 Runtime writer (the only live one): `set_live_tp_pct`
|
||||
|
||||
aem :96-97 (§2.3) stamps `tp_pct_override = tp_pct` on every tracked position whenever the soft
|
||||
curve moves. Hence per-trade TP override in live == current soft-curve threshold, refreshed every
|
||||
scan where the value changed (guarded `tp_pct <= 0` no-op).
|
||||
|
||||
### 5.3 HIBERNATE protect (trader :2424-2454, armed from posture sync :4128-4153)
|
||||
|
||||
When posture flips to HIBERNATE with an open position, instead of letting `_manage_position` fire
|
||||
`HIBERNATE_HALT`, the trader patches the LIVE exit-manager state:
|
||||
`em_state['stop_pct_override'] = _BUCKET_SL_PCT[bucket]` (per-bucket 0.8%-3.0%, trader :242-251)
|
||||
and keeps `_day_posture` at the previous value. TP side: **unchanged** — it logs
|
||||
`tp_pct = eng.exit_manager.fixed_tp_pct` (:2438) and lets the normal (possibly OB-widened) FIXED_TP
|
||||
close the trade; the exit is then re-labelled `HIBERNATE_TP`/`HIBERNATE_SL`/`HIBERNATE_MAXHOLD`
|
||||
(:4559-4571). If the trade is missing from the exit manager, it re-runs `setup_position` with only
|
||||
`stop_pct_override` (:4447-4450) — note this would reset `max_favorable` (TP_FLOOR arming) for
|
||||
that trade.
|
||||
|
||||
### 5.4 Catastrophic floor (trader :2456-2500+, called at :4192 and :4274-4275)
|
||||
|
||||
Ratchets `stop_pct_override` down to `DOLPHIN_CATASTROPHIC_FLOOR_PCT` (default 0.0120; overlay
|
||||
variant 0.0050) — only ever tightens (`current <= 0 or current > floor`). **SL-side only; never
|
||||
touches TP fields.** Listed here because it shares the same `_positions[tid]` dict the TP override
|
||||
lives in.
|
||||
|
||||
### 5.5 Restore path
|
||||
|
||||
On restart-restore the trader re-registers open positions via `setup_position` (BINGX rehydrate /
|
||||
restore code); any re-setup re-initializes `max_favorable = 0.0` and `tp_pct_override = None`
|
||||
until the next `_sync_tp_threshold` change re-stamps it — a small parity nuance: after a restart,
|
||||
`tp_pct_override` may be `None`, and `evaluate` then falls back to `self.fixed_tp_pct`
|
||||
(same value, so behaviourally neutral; see §6 `or` semantics).
|
||||
|
||||
---
|
||||
|
||||
## 6. `AlphaExitManager.evaluate()` — the TP gate itself (aem :121-330)
|
||||
|
||||
Called per bar with `current_price` (OBF-injected mid), `current_bar` = engine global bar idx,
|
||||
`current_timestamp = float(bar_idx)`, `asset = pos.asset`.
|
||||
|
||||
### 6.0 Pre-gate arithmetic (:138-159)
|
||||
|
||||
```python
|
||||
st = self._positions[trade_id] # missing → HOLD/NO_STATE :138-139
|
||||
pnl_pct = d * (current_price - entry) / entry # entry>0 guard, :146
|
||||
if pnl_pct > st["max_favorable"]: st["max_favorable"] = pnl_pct # :148-149
|
||||
|
||||
dynamic_tp_pct = st.get("tp_pct_override") or self.fixed_tp_pct # :151 ← `or`, not `is None`
|
||||
dynamic_sl_pct = st.get("stop_pct_override") or self.stop_pct # :152
|
||||
dynamic_max_hold = st.get("max_hold_override") or self.max_hold_bars # :153
|
||||
base_tp_pct = dynamic_tp_pct # :159 captured BEFORE OB modulation
|
||||
```
|
||||
|
||||
- `or`-semantics trap: an override of exactly `0` falls back to the global (harmless in live —
|
||||
`set_live_tp_pct` refuses `<= 0`).
|
||||
|
||||
### 6.1 OB modulation block (:163-202) — the "dormancy" question
|
||||
|
||||
Guard (:163): `if hasattr(self, 'ob_engine') and self.ob_engine is not None and asset is not None:`
|
||||
— `hasattr` is **trivially true** (attr created in `__init__` :74); the real gate is
|
||||
`ob_engine is not None`, i.e. whether §7 attached an engine.
|
||||
|
||||
```python
|
||||
ob_signal = self.ob_engine.get_signal(asset, current_timestamp) # :164
|
||||
macro = self.ob_engine.get_macro() # :165 (no arg → LATEST live macro)
|
||||
eff_imb = -ob_signal.imbalance_ma5 if d == -1 else ob_signal.imbalance_ma5 # :171
|
||||
|
||||
# TAIL AVOIDANCE 1 — cascade (market panic): :174-178
|
||||
if getattr(macro, 'cascade_count', 0.0) > 0.0:
|
||||
dynamic_tp_pct *= 1.40 # WIDEN TP → 0.28% typ.
|
||||
dynamic_max_hold = int(self.max_hold_bars * 0.5) # 125 bars
|
||||
|
||||
# TAIL AVOIDANCE 2 — withdrawal stress: :181-190
|
||||
elif macro.regime_signal == 1:
|
||||
dynamic_sl_pct = 0.10 # hard 10% stop
|
||||
if pnl_pct > 0.0: dynamic_tp_pct *= 0.60 # TIGHTEN TP → 0.12% typ.
|
||||
if eff_imb < -0.10: dynamic_max_hold = int(self.max_hold_bars * 0.4)
|
||||
|
||||
# CONVEXITY — calm & trending for us: :193-195
|
||||
elif macro.regime_signal == -1 and eff_imb > 0.15:
|
||||
dynamic_max_hold = int(self.max_hold_bars * 1.5)
|
||||
|
||||
# Per-asset withdrawal (only when macro NOT in cascade/stress): :198-202
|
||||
if (getattr(macro, 'cascade_count', 0) == 0 and macro.regime_signal != 1
|
||||
and ob_signal.withdrawal_velocity < -0.20):
|
||||
dynamic_max_hold = min(dynamic_max_hold, int(self.max_hold_bars * 0.40))
|
||||
if pnl_pct > 0.0: dynamic_tp_pct *= 0.75 # TIGHTEN TP → 0.15% typ.
|
||||
```
|
||||
|
||||
Structural exclusivity: cascade / withdrawal / convexity are an `if/elif/elif`; the per-asset
|
||||
block additionally requires `cascade_count == 0 AND regime_signal != 1`, so **the multipliers can
|
||||
never stack** — `tp_mod_factor ∈ {1.40, 0.60, 0.75, 1.00}` exactly.
|
||||
|
||||
### 6.2 Per-decision diagnostics (:204-223) — logged on EVERY decision, HOLD included
|
||||
|
||||
```python
|
||||
_floor_armed = bool(base_tp_pct > 0 and st["max_favorable"] >= base_tp_pct) # :208-210
|
||||
diag = {"tp_base_pct": base_tp_pct, "dynamic_tp_pct": dynamic_tp_pct,
|
||||
"tp_mod_factor": dynamic_tp_pct/base_tp_pct if base_tp_pct>0 else 1.0,
|
||||
"cascade_count": _cascade_count, "ob_regime_signal": _ob_regime_signal,
|
||||
"tp_floor_armed": _floor_armed} # :211-218
|
||||
self.last_eval = {"trade_id":..., "bar":..., "pnl_pct":..., "max_favorable":..., **diag} # :219-223
|
||||
```
|
||||
|
||||
Consumers:
|
||||
- v7 journal `_record_v7_decision` (trader :1204-1225): merges `decision[key]` else
|
||||
`exit_manager.last_eval[key]` (trade-id-matched, :1209-1214) into row columns
|
||||
`tp_base_pct, dynamic_tp_pct, tp_mod_factor, cascade_count, ob_regime_signal, tp_floor_armed`
|
||||
of `dolphin.v7_decision_events` (DDL trader :1119-1124; `ENGINE MergeTree`, TTL 180d).
|
||||
Journaled from two sources per bar while a position is open: `source="live_exit"` (provider hook,
|
||||
:1316-1328 — last_eval one bar stale there) and `source="scan_eval"` (:4482-4493 — last_eval
|
||||
fresh from this bar's `evaluate`).
|
||||
- TP exit console line (trader :4574-4588): for reasons `FIXED_TP/HIBERNATE_TP/TP_FLOOR/
|
||||
HIBERNATE_TP_FLOOR`, logs `TP_EXIT: tp_pct=… dyn_tp=… mod=…x cascade=… bars_held=… pnl_pct=…`,
|
||||
pulling `dynamic_tp_pct/tp_mod_factor/cascade_count` from `last_eval` because `_execute_exit`
|
||||
strips diag keys (§3).
|
||||
- `trade_events` payload (trader :879-884): `tp_threshold = eng.exit_manager.fixed_tp_pct`
|
||||
(read at event-build time — the *unmodulated* live threshold), plus per-trade
|
||||
`tp_base_pct/tp_effective_pct/our_leverage` from the pending snapshot (soft-curve context
|
||||
captured at entry :4257 and refreshed at close :4714).
|
||||
|
||||
### 6.3 Check order — what can preempt FIXED_TP (in exact evaluation order)
|
||||
|
||||
Above the exit manager entirely: (a) HIBERNATE_HALT (orchestrator :365-371, only when posture
|
||||
committed), (b) the V7 provider (`exit_decision_provider`, wired at trader :685 whenever
|
||||
`AlphaExitEngineV7` loaded — `_v7_live_exit_enabled = self._v7_exit_engine is not None`, :683):
|
||||
a provider **EXIT** (reasons like `V7.1_MAE_SL_VOL_NORM`) short-circuits `evaluate()` for that bar
|
||||
(orchestrator :387-395); HOLD/RETRACT falls through. (c) Forced exits from
|
||||
`_drain_runtime_commands` (trader :4551) bypass TP.
|
||||
|
||||
Inside `evaluate()`:
|
||||
|
||||
1. **TP_FLOOR ratchet** (:225-234) — FIRST, deliberately before everything so no modulation can
|
||||
mask it (the LINK 5e05eeeb lesson):
|
||||
`if tp_floor_enabled and _floor_armed and pnl_pct <= base_tp_pct → EXIT "TP_FLOOR"`.
|
||||
Arming is on **base** (unmodulated, soft-curve) TP via `max_favorable`; while pnl stays above
|
||||
base, the widened FIXED_TP gate keeps chasing upside.
|
||||
2. vel_div adverse-turn exits `VD_INVALIDATION`/`VD_EXHAUSTION` (:242-277) — **`vd_enabled=False`
|
||||
in live** (never set anywhere); dead code, OB-subject debounced logic.
|
||||
3. TF-spread recovery `TF_FLIP`/`TF_EXHAUSTION` (:280-312) — **`tf_enabled=False` in live**; dead.
|
||||
4. **FIXED_TP** (:314-317): `if dynamic_tp_pct > 0 and pnl_pct >= dynamic_tp_pct → EXIT "FIXED_TP"`.
|
||||
This is the comparison that fires the take-profit: `>=`, against the OB-modulated
|
||||
`dynamic_tp_pct`, using OBF-mid-based `pnl_pct`.
|
||||
5. STOP_LOSS (:320-322): `pnl_pct <= -dynamic_sl_pct`.
|
||||
6. MAX_HOLD (:325-327): `bars_held >= dynamic_max_hold`.
|
||||
7. HOLD (:329-330).
|
||||
|
||||
Numerically, in live: base ∈ [0.187%, 0.20%] (soft curve) and the effective FIXED_TP gate is
|
||||
almost always ×1.40 → ≈ 0.262%-0.28% (see §8), with TP_FLOOR exiting at base on give-backs.
|
||||
|
||||
---
|
||||
|
||||
## 7. ob_engine attachment — is the modulation block reachable? (code answer: YES)
|
||||
|
||||
### 7.1 Wiring (trader `_wire_obf` :5135-5148, invoked from `_process_scan` :4094-4095 on the first
|
||||
scan carrying an asset list)
|
||||
|
||||
```python
|
||||
from nautilus_dolphin.nautilus.hz_ob_provider import HZOBProvider
|
||||
live_ob = HZOBProvider(hz_cluster="dolphin", hz_host="127.0.0.1:5701", assets=assets)
|
||||
self.ob_eng = OBFeatureEngine(live_ob) # REAL engine, REAL provider
|
||||
self.eng.set_ob_engine(self.ob_eng) # :5147
|
||||
```
|
||||
|
||||
- `NDAlphaEngine.set_ob_engine` (orchestrator :833-842) propagates to
|
||||
`signal_gen.ob_engine`, `asset_selector.ob_engine`, and **`exit_manager.ob_engine`** (:842).
|
||||
`reset()` re-propagates it (:753-755).
|
||||
- `MockOBProvider` is imported at trader :36 but **never instantiated** anywhere in the file
|
||||
(grep: exactly 1 occurrence, the import). The provider is `HZOBProvider` — live Hazelcast
|
||||
OB snapshots (`asset_*_ob` keys from `obf_prefect_flow.py`), lazy-connect, push-listener cached.
|
||||
Boot log `OBF wired: HZOBProvider, 50 assets (LIVE mode)` appears once per restart.
|
||||
|
||||
### 7.2 Live macro production (`ob_features.py`)
|
||||
|
||||
- Per scan, trader :4124-4125 calls `ob_eng.step_live(ob_assets, bar_idx)` (ob_features :535-671):
|
||||
fetches a snapshot per asset, computes per-asset imbalance/depth features, per-asset
|
||||
`withdrawal_velocity = compute_velocity_nb(depth_1pct_history, 60)` (60-snapshot lookback), and
|
||||
cross-asset macro `process_macro_regime_nb(imb, vel, CASCADE_THRESHOLD=-0.10)` (:620-622,
|
||||
constants :349-351):
|
||||
- `cascade_count = Σ(velocity_i < −0.10)` (:640) — count of assets whose 60-step depth-drain
|
||||
rate is below −10%.
|
||||
- `regime_signal` (:248-265): `1` (STRESS) iff `cascade ≥ max(2, n/2)` AND `mean_vel < −0.10`;
|
||||
`−1` (CALM) iff `mean_vel > 0.10`; else `0`.
|
||||
- `get_macro()` with no arg (aem :165) → latest live macro (:716-731); missing → `NEUTRAL_MACRO`
|
||||
(cascade 0, regime 0 → modulation no-op). `get_signal(asset, bar)` (:687-695) resolves the bar
|
||||
directly in live mode with **fallback to the latest known bar** (:763-771); missing asset →
|
||||
`NEUTRAL_SIGNAL` (imbalance 0, withdrawal 0). Staleness guard (:644-653) only logs after 3
|
||||
empty fetches — features silently persist from the last good bar (cache holds 500 bars).
|
||||
|
||||
So statically: guard is live, engine is real, macro is real. The question is which *branches* fire.
|
||||
|
||||
---
|
||||
|
||||
## 8. DORMANCY VERIFICATION — per-branch verdicts (empirical)
|
||||
|
||||
Evidence sources:
|
||||
- **Log:** `/tmp/dolphin_logs/supervisor/nautilus_trader.log` (2026-06-20 → 2026-07-12, UTC).
|
||||
- **CH:** `dolphin.v7_decision_events` (per-decision diag; columns landed 2026-06-12) and
|
||||
`dolphin.trade_events` — `curl -u dolphin:dolphin_ch_2026 http://localhost:8123/`. CH `ts` UTC.
|
||||
|
||||
Measurements (reproduced verbatim):
|
||||
|
||||
```
|
||||
-- TP_EXIT lines in live log (since 2026-06-20):
|
||||
grep TP_EXIT … | grep -o "mod=…x" | sort | uniq -c → 198 mod=1.40x / 2 mod=1.00x
|
||||
|
||||
-- All-time distinct tp_mod_factor in dolphin.v7_decision_events:
|
||||
0 143,754 (legacy rows pre-2026-06-12 + rows with diag missing → column DEFAULT 0)
|
||||
1.0 288
|
||||
1.4 313,870
|
||||
(NO 0.6, NO 0.75, NO stacked values — ever)
|
||||
|
||||
-- Last 30 days: mod=1.4 → 306,939 decisions; mod=1.0 → 262; (0 → 9,490 diag-missing)
|
||||
|
||||
-- Rows with ob_regime_signal = 1 (STRESS observed 5,821 times since 2026-06-11):
|
||||
tp_mod_factor = 1.4 for ALL 5,821 → cascade always co-occurred; the elif never reached ×0.60
|
||||
|
||||
-- Rows with cascade_count = 0 (diag era): tp_mod_factor = 1.0 only (288 rows) → ×0.75 never hit
|
||||
|
||||
-- cascade_count on diag rows (since 2026-06-11): p05=10 median=18 p95=25 max=37 (of ~50 assets)
|
||||
|
||||
-- trade_events CLOSE reasons since 2026-06-11:
|
||||
FIXED_TP 1,784 · TP_FLOOR 1,522 · ADVSL_ASL_PRESSURE_CONT_STRESS 1,347 · MAX_HOLD 384 ·
|
||||
HOTKEY_RETRACT 279 · STOP_LOSS 238 · EXP_VOL_GATE_RELAX|… variants 115 · HIBERNATE_HIBERNATE_HALT 7
|
||||
|
||||
-- tp_floor_armed=1 on 12,604 decisions since 2026-06-12; TP threshold soft-curve transitions: 420 log lines.
|
||||
```
|
||||
|
||||
### Verdicts
|
||||
|
||||
| Branch | Multiplier | Verdict | Evidence |
|
||||
|---|---|---|---|
|
||||
| Cascade (aem :174-178) | TP ×1.40, max_hold ×0.5 | **FIRES-LIVE — effectively ALWAYS-ON (saturated)** | 313,870 journal decisions at mod=1.4 vs 288 at 1.0 all-time; 198/200 TP exits in the live log widened to dyn_tp 0.26-0.28%; cascade_count median 18/50 assets, p05=10 → `cascade_count > 0` is true on ~99.9% of decisions. |
|
||||
| Withdrawal stress (aem :181-190) | TP ×0.60 (pnl>0), SL→10%, hold ×0.4 | **NEVER-OBSERVED (structurally shadowed)** | mod=0.60 appears 0 times all-time. Reachability: it is the `elif` after cascade; every one of the 5,821 STRESS (`regime_signal=1`) decisions ALSO had `cascade_count > 0`, so the branch was never entered — consistent with the regime-1 definition itself requiring `cascade ≥ max(2, n/2)`, which guarantees `cascade_count > 0`. **`regime_signal==1 ⇒ cascade_count>0` by construction (ob_features :259), so the ×0.60/10%-SL/0.4-hold branch is dead code under the current macro definition.** |
|
||||
| Per-asset withdrawal (aem :198-202) | TP ×0.75 (pnl>0), hold ≤×0.40 | **NEVER-OBSERVED** | mod=0.75 appears 0 times all-time; requires `cascade_count == 0` (p05=10 → almost never true) AND `withdrawal_velocity < −0.20` AND `pnl>0`. The only cascade=0 diag rows (288) all show mod=1.0. |
|
||||
| No modulation | ×1.00 | Rare | 288 decisions / 2 TP exits — brief windows where zero assets breached the −0.10 velocity threshold. |
|
||||
|
||||
**Answer to the operator's "OB TP coefficient might be dormant" warning: it is the OPPOSITE of
|
||||
dormant.** The wiring is real (HZOBProvider → OBFeatureEngine → `set_ob_engine` → `exit_manager
|
||||
.ob_engine`), the `hasattr` guard is trivially satisfied, and the cascade ×1.40 widening is in
|
||||
force on essentially every decision — the live effective TP gate is ≈0.262-0.28%, not 0.20%. The
|
||||
"take profit at 0.20%" behaviour survives only because TP_FLOOR (armed on the base threshold)
|
||||
exits regressions — which is why TP_FLOOR accounts for 46% of all TP-family closes (1,522 vs
|
||||
1,784 FIXED_TP). The two tightening branches (×0.60, ×0.75) have never fired and — given the
|
||||
cascade/regime definitions — cannot fire in practice: ×0.60 is provably unreachable
|
||||
(regime 1 implies cascade > 0), ×0.75 requires a market state (zero cascading assets while one
|
||||
asset drains >20%) that has not occurred in >450k logged decisions.
|
||||
|
||||
Calibration note for the port (observation, not a change request): `CASCADE_THRESHOLD = −0.10`
|
||||
against a 60-snapshot depth-velocity computed per ~11 s live scan means "any 1 of ~50 assets lost
|
||||
>10% of near-book depth over ~11 min" — permanently true in this universe. In the 30 s-snapshot
|
||||
backtest frame the same constant was far more selective. A parity port must replicate the LIVE
|
||||
behaviour (×1.40 essentially always + TP_FLOOR backstop), not the intended semantics.
|
||||
|
||||
---
|
||||
|
||||
## 9. Complete TP-write/read site index (for the port checklist)
|
||||
|
||||
Writers of the effective gate, in precedence order at comparison time:
|
||||
|
||||
1. `ENGINE_KWARGS.fixed_tp_pct = 0.0020` — trader :134 (birth).
|
||||
2. Soft curve `compute_soft_tp_pct(0.0020, notional/capital)` — tp_curve :43-64, applied every
|
||||
scan via trader :4165 → :906-926 → orchestrator :860-866 → aem :82-98 (sets BOTH
|
||||
`fixed_tp_pct` and every open trade's `tp_pct_override`). Bounds [0.00187, 0.0020].
|
||||
3. Per-trade `tp_pct_override` at entry — always `None` in live (exp15 dormant, §5.1).
|
||||
4. OB modulation ×1.40 / ×0.60 / ×0.75 — aem :174-202, transient per evaluation (never persisted).
|
||||
5. TP_FLOOR — aem :232-234, exits at BASE (post-soft-curve, pre-modulation) once
|
||||
`max_favorable ≥ base`; enabled via `DOLPHIN_TP_FLOOR` default ON (trader :612).
|
||||
|
||||
Readers/consumers: FIXED_TP comparison aem :315; TP_FLOOR comparison aem :232; hibernate-protect
|
||||
log trader :2438; trade_events `tp_threshold` trader :879; TP_EXIT log trader :4576-4588;
|
||||
v7 journal columns trader :1218-1225; pending-entry snapshot fields
|
||||
`tp_base_pct/tp_effective_pct/our_leverage` trader :4257 (entry) / :4714 (close) / retract legs.
|
||||
|
||||
Known dead/disabled TP-adjacent paths (do NOT port as live behaviour): `vd_enabled` exits
|
||||
(aem :242-277), `tf_enabled` exits (aem :280-312), exp15 entry-time TP/hold overrides (§5.1),
|
||||
`_sync_tp_threshold`'s docstring HZ key (stale doc), `esf_alpha_orchestrator_AGENT_fork.py`
|
||||
(research fork), `NDAlphaEngine.reset()`'s exit-manager rebuild dropping `tp_floor_enabled`
|
||||
(orchestrator :726-740 — latent trap, unused live).
|
||||
514
prod/docs/uv_cert/exit_crawl/UV_EXIT_CRAWL.md
Normal file
514
prod/docs/uv_cert/exit_crawl/UV_EXIT_CRAWL.md
Normal file
@@ -0,0 +1,514 @@
|
||||
# UV EXIT-DOCTRINE CRAWL — current code, for BLUE parity comparison
|
||||
|
||||
Date: 2026-07-12. READ-ONLY crawl of `/root/uv-wt/t0-integration/prod/clean_arch/violet/uv/`
|
||||
(+ `prod/clean_arch/dita_v2/` for the kernel seam). All `file:line` references are against
|
||||
that worktree. No code was modified.
|
||||
|
||||
Files crawled in full:
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `violet/uv/brain_tpsl.py` (339 lines) | Doctrine constants, Position, TpSlHandler (tick → decision → EXIT intent) |
|
||||
| `violet/uv/tpsl_ticker.py` (413 lines) | 1 s BingX mark-price poll thread, MAX_HOLD on_bar path, kernel reconcile |
|
||||
| `violet/uv/max_hold_modulation.py` (183 lines) | Certified BLUE MAX_HOLD 5-branch port + OBF ingress provider |
|
||||
| `violet/uv/blue_prime/runner.py` (493 lines) | TPSL construction / receipt-gated registration / ON_BAR invocation |
|
||||
| `violet/uv/blue_prime/promotion.py` (590 lines) | Decision → KernelIntent, EXIT passthrough path |
|
||||
| `violet/uv/exec/asex_kernel_executor.py` (303 lines) | Receipt statuses (CLOSED / exit_complete) |
|
||||
| `dita_v2/contracts.py`, `dita_v2/rust_backend.py`, `dita_v2/_rust_kernel/src/lib.rs`, `dita_v2/bingx_venue.py`, `dita_v2/real_zinc_plane.py` | `exit_leg_ratios` consumer inventory |
|
||||
|
||||
---
|
||||
|
||||
## 1. Exit-doctrine constants ("do as BLUE does" — contract values, not knobs)
|
||||
|
||||
`brain_tpsl.py:23-31` (operator-confirmed 2026-07-10; comment: "Change here, with a test, or not at all"):
|
||||
|
||||
| Constant | Value | Meaning | Line |
|
||||
|---|---|---|---|
|
||||
| `DOCTRINE_TP_PCT` | `0.0035` | fixed TP ceiling (+0.35 % favorable move) | brain_tpsl.py:26 |
|
||||
| `DOCTRINE_SL_PCT` | `0.012` | BLUE contract stop (−1.2 %) | brain_tpsl.py:27 |
|
||||
| `DOCTRINE_TRAIL_PCT` | `0.002` | trailing TP: exit on 0.20 % retrace from best | brain_tpsl.py:28 |
|
||||
| `DOCTRINE_TRAIL_ACT_PCT` | `0.002` | trail arms once favorable move ≥ +0.20 % | brain_tpsl.py:29 |
|
||||
| `DOCTRINE_MAX_HOLD_BARS` | `120` | bar cutoff — **fallback only** (see §6) | brain_tpsl.py:30 |
|
||||
| `DOCTRINE_POLL_S` | `1.0` | interim tick cadence (OBF/WS sub-ms ticks = future C11) | brain_tpsl.py:31 |
|
||||
|
||||
MAX_HOLD companion constants, `max_hold_modulation.py:15-16`:
|
||||
|
||||
| Constant | Value |
|
||||
|---|---|
|
||||
| `BLUE_BASE_MAX_HOLD_BARS` | `250` (the BLUE base the 5-branch modulation scales) |
|
||||
| `FALLBACK_MAX_HOLD_BARS` | `120` (defined; the ticker actually falls back to its own `_max_hold_bars`, seeded from `DOCTRINE_MAX_HOLD_BARS=120` — same value, two homes) |
|
||||
|
||||
Production always runs the doctrine values: `maybe_build_ticker()` (`tpsl_ticker.py:388-413`) takes
|
||||
**no** tp/sl/trail parameters — "No env vars, no arming knobs (2026-07-10 ruling)" (tpsl_ticker.py:395-400).
|
||||
Constructor params on `TpSlTicker.__init__` exist "for TESTS, not ops" (tpsl_ticker.py:105-107).
|
||||
Doctrine banner logged at startup: `tpsl_ticker.py:408-412`
|
||||
(`"TPSL DOCTRINE: tp=%.2f%% sl=%.2f%% trail=%.2f%%@%.2f%% max_hold=%d bars poll=%.1fs"`).
|
||||
Note the module docstring `tpsl_ticker.py:11-13` still describes env `UV_TP_PCT`/`UV_SL_PCT` and
|
||||
`UV_TPSL_ENABLE=1` opt-in (lines 4-5) — **stale docstring**; the code path is unconditional doctrine.
|
||||
Same for the runner comment at `runner.py:287` ("None when UV_TPSL_ENABLE!=1") — `maybe_build_ticker`
|
||||
never returns None.
|
||||
|
||||
---
|
||||
|
||||
## 2. Position validation (poison-proof ingress)
|
||||
|
||||
`Position` dataclass, `brain_tpsl.py:42-71`. TP/SL are **required** fields with no class defaults
|
||||
(D5, brain_tpsl.py:50-52). `__post_init__` (brain_tpsl.py:54-71) raises `InvalidPositionError` on:
|
||||
empty `trade_id`/`asset`; `side` not in `{"SHORT","LONG"}`; non-finite or ≤0 `entry_price`,
|
||||
`quantity`, `leverage`, `take_profit_pct`, `stop_loss_pct`. Tick-side poison guard (D6):
|
||||
`PoisonTickError` class exists (brain_tpsl.py:38-39) but `on_tick` handles poison inline —
|
||||
non-finite/≤0 price increments `poison_ticks` stat and returns `[]` (brain_tpsl.py:175-178).
|
||||
|
||||
---
|
||||
|
||||
## 3. TpSlHandler — tick evaluation (the decision brain)
|
||||
|
||||
Class at `brain_tpsl.py:113-338`. Holds `_positions: dict[trade_id → Position]`,
|
||||
`_best_move: dict[trade_id → best favorable pct_move]` (trailing state), `_pending_exits:
|
||||
dict[trade_id → trigger_type]`, and a stats dict (brain_tpsl.py:114-133).
|
||||
|
||||
### 3.1 `on_tick` (brain_tpsl.py:165-199)
|
||||
|
||||
Per tick: poison guard (§2) → for each position on `tick.symbol`, skip if `pending_exit` (line 185-186)
|
||||
→ `_evaluate` → if triggered, `_fire_exit` + `_handle_exit_submission`; decision appended to results
|
||||
only when state == `"closed"`. D4: rejected exits keep the position monitored.
|
||||
|
||||
### 3.2 `_evaluate` — priority order (brain_tpsl.py:201-247)
|
||||
|
||||
Favorable-move formula (brain_tpsl.py:203-206):
|
||||
|
||||
```
|
||||
SHORT: pct_move = (entry_price − tick.price) / entry_price
|
||||
LONG : pct_move = (tick.price − entry_price) / entry_price
|
||||
```
|
||||
|
||||
Evaluation order (D7 — loss-protection wins ties):
|
||||
|
||||
1. **Trailing state update first** (brain_tpsl.py:211-217): if `trail_pct > 0`,
|
||||
`best = max(best_move[tid], pct_move)`; `act = trail_activation_pct or trail_pct`;
|
||||
`_trail_hit = best >= act and (best − pct_move) >= trail_pct`. Trailing measures
|
||||
**retrace-from-best**, not absolute level. The flag is only computed here; firing is deferred.
|
||||
2. **SL** (brain_tpsl.py:220-225): `pct_move <= −stop_loss_pct` → `trigger_type="stop_loss"`.
|
||||
3. **Trailing TP** (brain_tpsl.py:228-233): `_trail_hit` → `trigger_type="trailing_tp"`.
|
||||
Deliberately before fixed TP: "locks retraces the fixed level misses" (line 227).
|
||||
4. **Fixed TP** (brain_tpsl.py:236-241): `pct_move >= take_profit_pct` → `trigger_type="take_profit"`.
|
||||
5. Otherwise `triggered=False, trigger_type="none"` (brain_tpsl.py:243-247).
|
||||
|
||||
`ExitPriority` enum (brain_tpsl.py:99-110) reserves `CATASTROPHIC = 0` and `ADVSL = 3` as
|
||||
**future** slots — named but unimplemented (the `_PRIORITY_MAP` at 107-110 only maps
|
||||
stop_loss/take_profit and is itself currently unused by any consumer in this file).
|
||||
|
||||
### 3.3 Pending-exit lifecycle (partial-fill safety)
|
||||
|
||||
`_handle_exit_submission` (brain_tpsl.py:249-274):
|
||||
- submission not accepted → `exit_rejected` stat, position stays watched, returns `"rejected"`.
|
||||
- receipt present and `receipt.exit_complete` falsy → mark `_pending_exits[trade_id] = trigger_type`,
|
||||
log `"TPSL: exit pending"` with status/fill/remaining, return `"pending"`. While pending, `on_tick`
|
||||
skips the position (brain_tpsl.py:185-186) — no duplicate exit spam.
|
||||
- receipt None or `exit_complete` → `remove_position` + `_count_terminal_trigger`, return `"closed"`.
|
||||
|
||||
`clear_pending_exit` (brain_tpsl.py:152-154) re-enables a retry "after kernel truth proves no exit
|
||||
remains in flight" — invoked only from the ticker's reconcile (§5.3).
|
||||
|
||||
### 3.4 Kernel-truth retirement
|
||||
|
||||
`confirm_closed` (brain_tpsl.py:156-160): "Retire a watch only after kernel/Zinc truth reports
|
||||
terminal state" — pops the pending trigger_type and counts it (`tp_fired`/`sl_fired`/`trail_fired`,
|
||||
brain_tpsl.py:276-282). `replace_position` (brain_tpsl.py:144-147) swaps in reconciled kernel truth
|
||||
without touching `_best_move` (trailing memory survives reconciles; note: it also survives with the
|
||||
OLD entry_price baseline if the kernel reports a different fill price — the pct_move baseline shifts
|
||||
but `_best_move` is not rebased. Minor parity wrinkle to remember when porting AdvancedSL).
|
||||
|
||||
### 3.5 `_fire_exit` — decision → EXIT intent dict (brain_tpsl.py:284-332)
|
||||
|
||||
Builds the EXIT decision dict:
|
||||
|
||||
```python
|
||||
{"action": "EXIT", "has_entry": False, "asset", "side": pos.side, # D3: real side
|
||||
"trade_id": pos.trade_id, # D2: ORIGINAL trade_id (kernel correlation)
|
||||
"target_size": pos.quantity, # D2: full close quantity
|
||||
"entry": {asset, side, trade_id, price=decision.price, leverage, size}}
|
||||
```
|
||||
|
||||
Delivery: prefers `bridge.try_promote_result(decision, ctx=None)` (brain_tpsl.py:316-322,
|
||||
atomic intent+receipt), falls back to `bridge.try_promote` + `bridge.last_receipt`
|
||||
(brain_tpsl.py:323-325). No bridge → logged would-exit, `ExitSubmission(False)` (brain_tpsl.py:308-314).
|
||||
|
||||
**Always a single full-quantity close.** `exit_leg_ratios` is never set → KernelIntent default
|
||||
`(1.0,)` (contracts.py:319). No partial/multi-leg exits are ever emitted by UV (§9).
|
||||
|
||||
---
|
||||
|
||||
## 4. Price source and cadence
|
||||
|
||||
`tpsl_ticker.py:39-79`:
|
||||
- Hosts: `_VST_HOST = https://open-api-vst.bingx.com`, `_LIVE_HOST = https://open-api.bingx.com`,
|
||||
selected by `DOLPHIN_BINGX_ENV` (default VST) (tpsl_ticker.py:39-40, 67-68).
|
||||
- Endpoint: `GET /openApi/swap/v2/quote/premiumIndex?symbol=BTC-USDT` → `data.markPrice`
|
||||
(tpsl_ticker.py:41, 65-76). Symbol mapping `BTCUSDT → BTC-USDT` at tpsl_ticker.py:59-62.
|
||||
- Fail-soft: any fetch failure returns `0.0` = skipped tick (tpsl_ticker.py:77-79).
|
||||
- Cadence: `DOCTRINE_POLL_S = 1.0`, floor 0.5 s (`max(0.5, poll_s)`, tpsl_ticker.py:115). The
|
||||
docstring's `UV_TPSL_POLL_S` env (line 8) is not read anywhere — cadence is the doctrine constant.
|
||||
- Ticker thread `_run` (tpsl_ticker.py:356-385): each cycle = `_sync_from_kernel()` → fetch price per
|
||||
watched asset → `handler.on_tick(TickEvent)` under lock → heartbeat every 60 polls
|
||||
(tpsl_ticker.py:374-382) → `_stop.wait(poll_s)`. Catches everything: "the ticker must never die
|
||||
silently" (tpsl_ticker.py:383-384).
|
||||
|
||||
So: **TP / SL / trailing are driven by 1 s REST mark price; MAX_HOLD is driven by the ~6 s scan bar**
|
||||
(on_bar). Two clocks, one handler.
|
||||
|
||||
Trailing wiring: `tpsl_ticker.py:113-114` sets `handler.trail_pct` / `handler.trail_activation_pct`
|
||||
from the doctrine constants — trailing is always armed in production.
|
||||
|
||||
---
|
||||
|
||||
## 5. TpSlTicker — registration, MAX_HOLD, kernel reconcile
|
||||
|
||||
### 5.1 `register_entry` (tpsl_ticker.py:138-153)
|
||||
|
||||
Runner-thread API. Under `_lock`: `handler.add_position(Position(..., take_profit_pct=self._tp_pct,
|
||||
stop_loss_pct=self._sl_pct))` + `_bars[trade_id] = 0`. Logs `"TPSL WATCH"`. Thread contract
|
||||
(tpsl_ticker.py:85-88): `on_tick` only on ticker thread; `register_entry`/`stop` from runner thread.
|
||||
|
||||
### 5.2 `on_bar` — the MAX_HOLD path (tpsl_ticker.py:170-213)
|
||||
|
||||
Called by the runner **once per processed scan** (= one bar, doctrine). Per watched position:
|
||||
- skip if pending exit (line 180-181);
|
||||
- `bars = _bars[tid] + 1` (line 182-183);
|
||||
- `max_hold_bars, journal_row = _resolve_max_hold(pos, bars)` (line 184) — recomputed **every scan**
|
||||
from the same scan snapshot BLUE's exit manager sees;
|
||||
- `_journal_max_hold(journal_row)` (line 185) — every evaluation journaled, hit or not;
|
||||
- if `bars >= max_hold_bars`: price = `_last_price.get(asset) or _fetch(asset) or entry_price`
|
||||
(line 188 — entry_price fallback means a dead feed still force-closes, at 0 recorded move);
|
||||
builds `TpSlDecision(trigger_type="max_hold", ...)` (lines 189-195) and pushes it through the
|
||||
same `_fire_exit` / `_handle_exit_submission` machinery as tick exits (lines 196-200).
|
||||
`"MAX_HOLD EXIT"` on close (with branch), `"MAX_HOLD EXIT REJECTED ... still watched"` on reject
|
||||
(lines 203-213). Note `"max_hold"` is not counted by `_count_terminal_trigger`
|
||||
(brain_tpsl.py:276-282 counts only tp/sl/trail) — max_hold closes are visible in logs/journal but
|
||||
not in the fired-stats counters.
|
||||
|
||||
### 5.3 `_resolve_max_hold` — ingress + loud fallback (tpsl_ticker.py:215-255)
|
||||
|
||||
- If an ingress provider is wired and returns a `MaxHoldIngress`: `blue_max_hold_yield(ingress)`
|
||||
→ `(result.max_hold_bars, result.journal_fields(bars_held))` (lines 219-226).
|
||||
- Any provider exception is logged (`LOGGER.exception`) and falls through (lines 227-228).
|
||||
- **No-ingress fallback**: `_max_hold_fallbacks += 1`; first occurrence per trade logs LOUD
|
||||
`"MAX_HOLD NO-INGRESS FALLBACK: ... fixed_limit=120"` (lines 230-238); returns
|
||||
`(120, journal_row{branch: "NO_INGRESS_FALLBACK", fallback: 1, all-zero OBF fields})`
|
||||
(lines 239-255). Comment at 126-129: "No-ingress FALLBACK only (doctrine constant, no env —
|
||||
2026-07-10 ruling). Armed BLUE-parity operation injects the complete OBF ingress and recomputes
|
||||
100/125/250/375 every scan."
|
||||
- Journaling failure is CRITICAL-logged (`"MAX_HOLD INGRESS NOT PERSISTED"`) and counted, never
|
||||
blocks the exit (tpsl_ticker.py:257-272). Journal sink = `PrimeJournal.write_max_hold`
|
||||
(blue_prime/journal.py:330).
|
||||
|
||||
### 5.4 `_sync_from_kernel` — Zinc slot truth reconcile (tpsl_ticker.py:284-353)
|
||||
|
||||
Runs at the top of every ticker cycle. "The ticker never owns an order book or position ledger"
|
||||
(tpsl_ticker.py:287-290). Against `position_provider()` slots (= `zinc_plane.read_slots`,
|
||||
runner.py:245):
|
||||
- terminal (`slot.closed` or `fsm_state ∈ {IDLE, POSITION_CLOSED, CLOSED, TRADE_TERMINAL_WRITTEN}`,
|
||||
set at tpsl_ticker.py:43-48) → `confirm_closed` + drop bar counter (lines 306-310);
|
||||
- live slot → `replace_position` with kernel-truth entry_price/size/side/leverage, keeping the
|
||||
original tp/sl pcts (lines 311-323);
|
||||
- pending exit but slot no longer `EXIT_WORKING` and no `active_exit_order` → `clear_pending_exit`
|
||||
("retry enabled", lines 324-327);
|
||||
- **restart rehydration**: kernel slot open (`fsm_state ∈ _OPEN_STATES`, tpsl_ticker.py:49-56) with
|
||||
valid size/price/leverage/side but not watched → `add_position` with doctrine tp/sl,
|
||||
`_bars.setdefault(tid, 0)`, `"TPSL REHYDRATE"` (lines 329-353). Rehydrated positions restart the
|
||||
hold clock at 0 — bars_held is not recoverable from the slot (BLUE-parity gap for restarts).
|
||||
|
||||
---
|
||||
|
||||
## 6. MAX_HOLD modulation — the certified BLUE port
|
||||
|
||||
`max_hold_modulation.py` — "Bit-faithful BLUE MAX_HOLD modulation... branch order mirrors
|
||||
`AlphaExitManager.evaluate` in the read-only BLUE source" (lines 1-6). Pure function; acquisition
|
||||
and journaling at the edges.
|
||||
|
||||
### 6.1 Ingress contract (`MaxHoldIngress`, max_hold_modulation.py:19-44)
|
||||
|
||||
Fields: `trade_id, asset, side, evaluation_bar, cascade_count, regime_signal, imbalance_ma5,
|
||||
withdrawal_velocity, base_max_hold_bars=250, source="blue_obf"`. Validation rejects empty ids,
|
||||
bad side, negative bar/cascade, non-positive base, non-finite floats.
|
||||
|
||||
### 6.2 `blue_max_hold_yield` — formulas (max_hold_modulation.py:65-103)
|
||||
|
||||
```
|
||||
effective_imbalance = −imbalance_ma5 if side==SHORT else imbalance_ma5 (:69-73)
|
||||
base = 250
|
||||
|
||||
if cascade_count > 0: → int(base*0.5) = 125 CASCADE (:77-79)
|
||||
elif regime_signal == 1: WITHDRAWAL_STRESS (:80-81)
|
||||
and effective_imbalance < −0.10: → int(base*0.4) = 100 WITHDRAWAL_STRESS_ADVERSE (:82-84)
|
||||
elif regime_signal == −1
|
||||
and effective_imbalance > 0.15: → int(base*1.5) = 375 FAVORABLE_EXTENSION (:85-87)
|
||||
|
||||
# independent override (can cut the favorable extension):
|
||||
if cascade_count == 0 and regime_signal != 1
|
||||
and withdrawal_velocity < −0.20: → min(current, int(base*0.40)) = 100
|
||||
PER_ASSET_WITHDRAWAL (:89-96)
|
||||
else → 250 NEUTRAL
|
||||
```
|
||||
|
||||
"Order is doctrinal. A positive cascade preempts regime_signal entirely" (:76). The resulting cap
|
||||
set is exactly BLUE's {100, 125, 250, 375}. `MaxHoldYield.journal_fields` (:47-62) flattens
|
||||
ingress + `bars_held`, `effective_imbalance`, `max_hold_bars`, `branch`, `fallback`.
|
||||
|
||||
### 6.3 `build_engine_ingress_provider` (max_hold_modulation.py:120-182)
|
||||
|
||||
Captures "the same OBF variables the embedded BLUE manager sees per scan":
|
||||
- `engine.exit_manager.ob_engine` (fallbacks `engine.ob_engine`/`ob_eng`); None → no ingress (:124-129).
|
||||
- Requires `manager.max_hold_bars` and `engine._global_bar_idx` hydrated; **hard-raises** if the
|
||||
embedded BLUE base ≠ 250 (`"embedded BLUE max_hold_bars=...; expected 250"`, :131-139).
|
||||
- `evaluation_bar = _global_bar_idx − 1`; ≤0 → no ingress (:140-143).
|
||||
- Clock-drift guard (:145-152): if the OBF engine exposes `_live_mode`/`_live_bar_idx`, they must be
|
||||
`True` and == evaluation_bar — otherwise return None rather than let `get_signal()` serve a stale
|
||||
cache entry ("hide clock drift"). This is the hook-02 snapshot contract.
|
||||
- Pulls `ob_engine.get_signal(asset, evaluation_bar)` + `ob_engine.get_macro()`; requires
|
||||
`cascade_count, regime_signal, imbalance_ma5, withdrawal_velocity` all present (`_required_value`
|
||||
never invents a neutral, :109-117, :154-169); builds the `MaxHoldIngress` (:170-180).
|
||||
- Any missing dependency → None → §5.3 loud 120 fallback.
|
||||
|
||||
**This provider is the ingress pattern (`MaxHoldIngressProvider = Callable[[position, bars_held],
|
||||
MaxHoldIngress | None]`, :106) that the PORT-SURFACE section names as the template.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Runner wiring (blue_prime/runner.py)
|
||||
|
||||
- Imports: `build_engine_ingress_provider` + `maybe_build_ticker` (runner.py:203-204).
|
||||
- Executor/venue arm path (runner.py:240-260): non-dark mode builds
|
||||
`build_launcher_bundle(max_slots=10, prefix="uv_exec", venue_mode="BINGX")`,
|
||||
`ASExKernelExecutor(bundle.kernel)`, `position_provider = bundle.zinc_plane.read_slots`,
|
||||
E-capital provider, E-feed.
|
||||
- Bridge (runner.py:279-286): `PromotionBridge(gate, intent_source, journal,
|
||||
submitter=_exec_executor, tradable_assets, capital_provider)`.
|
||||
- **Ticker construction** (runner.py:288-293):
|
||||
|
||||
```python
|
||||
tpsl_ticker = maybe_build_ticker(
|
||||
bridge,
|
||||
position_provider=_position_provider,
|
||||
max_hold_ingress_provider=build_engine_ingress_provider(harness.engine),
|
||||
max_hold_journal=journal.write_max_hold,
|
||||
)
|
||||
```
|
||||
|
||||
- **ON_BAR invocation** (runner.py:332-343): immediately after `harness.step(...)` (the engine
|
||||
step_bar) and **before** promotion — "Existing positions see the same post-OBF scan as BLUE's exit
|
||||
manager... a position entered on this scan starts its hold clock on the next scan, exactly as
|
||||
BLUE." Wrapped so an on_bar exception can never kill the scan loop (`"MAX_HOLD ON_BAR ERROR"`).
|
||||
- **Receipt-gated registration** (runner.py:355-383): after `bridge.try_promote_result(decision=out,
|
||||
ctx)`, registration requires ALL of: promoted; sized intent present (T19-FIX: register from
|
||||
`bridge` result's SIZED intent, "never the raw entry dict — the dict lacks bridged quantity/price,
|
||||
which watched ghost positions (qty=0) on 2026-07-10 first-arming"); `receipt.entry_protectable`
|
||||
(ENTER + has_fill + avg price > 0, see §8); `action == "ENTER" and target_size > 0`. Registration
|
||||
uses **receipt truth**: `entry_price=float(_receipt.average_fill_price)`,
|
||||
`quantity=float(_receipt.fill_quantity)` (runner.py:366-373). Promoted-but-not-protectable →
|
||||
`"ENTRY NOT WATCHED YET"` warning with status/fill/remaining (runner.py:376-383); the §5.4
|
||||
reconcile later rehydrates it from the slot if it fills.
|
||||
- Shutdown (runner.py:460-484): `_e_feed.stop()` → `tpsl_ticker.stop()` → `hz.shutdown()` →
|
||||
`_exec_executor.close()` → `_exec_bundle.close()`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Promotion — how a TpSl decision becomes an EXIT intent
|
||||
|
||||
`blue_prime/promotion.py`.
|
||||
|
||||
### 8.1 Action resolution (promotion.py:119-136)
|
||||
|
||||
`_resolve_action`: explicit `action=="ENTER"/"EXIT"` wins; unknown explicit action → suppress;
|
||||
no action + `has_entry` → ENTER (legacy); no action + no entry → **None** ("NEVER inferred as EXIT;
|
||||
that inference was the R1 regression: every no-entry scan spammed the kernel with zero-size exits").
|
||||
|
||||
### 8.2 EXIT gate path (`_try_promote_locked`, promotion.py:401-509)
|
||||
|
||||
Serialized under `_promote_lock` RLock (promotion.py:356-360, 393) — the tick thread and scan thread
|
||||
promote through the same critical section. Sequence:
|
||||
1. Two-man arming gate (env `UV_PROMOTED==1` + arming file, re-evaluated every call —
|
||||
promotion.py:60-107, 409-410). Not armed → suppressed+journaled.
|
||||
2. `_action is None` → suppress `no_entry` (:415-417).
|
||||
3. **D8 zero-qty guard** (:418-426): explicit EXIT with `target_size` (or entry.size) ≤0/non-finite
|
||||
→ `LOGGER.error("PROMOTION: EXIT with no positive quantity — suppressed")`, reason `exit_zero_qty`.
|
||||
4. Universe gate (:428-439): applies to ENTER only — "**EXITs always pass: closing must never be
|
||||
blocked by the gate**" (:431).
|
||||
5. Capital + translation `build_kernel_intent_from_decision` (:441-448).
|
||||
6. Delivery: `submitter.submit(intent)` (ASExKernelExecutor) when armed, else
|
||||
`intent_source.enqueue(intent)` shadow queue (:450-457).
|
||||
7. Receipt-status audit fix (:459-479): `KERNEL_REJECTED`/`VENUE_REJECTED` → journaled SUPPRESSED
|
||||
(`receipt:<status>`), returns False ("unsuppressed phantom rows were the 25-row reconciliation gap
|
||||
in the 2026-07-10 audit"). Also re-checked at :502-503.
|
||||
8. Success → journal `kind='BRIDGE'` row incl. receipt fields (:527-560), `last_intent` set (:483).
|
||||
|
||||
### 8.3 EXIT translation (`build_kernel_intent_from_decision`, promotion.py:139-308)
|
||||
|
||||
For `action is KernelCommandType.EXIT` (promotion.py:235-242):
|
||||
|
||||
```python
|
||||
target_size = decision.target_size or entry.size or entry.target_size # position qty passthrough
|
||||
leverage = int(decision.leverage or entry.leverage or 1) # position's leverage
|
||||
_bridge_note = "exit_passthrough" # sizing bridge NEVER run
|
||||
```
|
||||
|
||||
"re-sizing a close would orphan part of the position" (:237). `trade_id` correlation (:268-271):
|
||||
decision-level original trade_id first — "a fresh id would orphan the open position in the kernel."
|
||||
`reference_price` = entry.entry_price/reference_price/price (:197-203) = the trigger tick price
|
||||
placed by `_fire_exit`. `slot_id` = decision/entry slot_id or **0** (:273) — all UV intents share
|
||||
slot 0 (see EXIT_ASSET_MISMATCH guard, §9.3).
|
||||
|
||||
**`exit_leg_ratios` is never populated** — the constructed `KernelIntent` (promotion.py:295-308)
|
||||
omits it, so the contract default `(1.0,)` (contracts.py:319) rides through. UV's exits are always
|
||||
single-leg 100 % closes.
|
||||
|
||||
### 8.4 Full call chains
|
||||
|
||||
**Tick exit (sub-second path):**
|
||||
```
|
||||
TpSlTicker._run (1 s poll, tpsl_ticker.py:356)
|
||||
→ _sync_from_kernel (Zinc truth) tpsl_ticker.py:284
|
||||
→ TpSlHandler.on_tick(TickEvent) brain_tpsl.py:165
|
||||
→ _evaluate: SL → trailing → TP brain_tpsl.py:201
|
||||
→ _fire_exit → dict{action:"EXIT", target_size=qty} brain_tpsl.py:284
|
||||
→ PromotionBridge.try_promote_result promotion.py:386
|
||||
→ build_kernel_intent_from_decision (exit_passthrough) promotion.py:238
|
||||
→ ASExKernelExecutor.submit(intent) asex_kernel_executor.py:275
|
||||
→ single-writer ASExWorker → kernel.process_intent asex_kernel_executor.py:238
|
||||
→ Rust FSM EXIT (lib.rs:1418) → venue.submit → fills → CLOSED
|
||||
→ ExecutionReceipt(status, exit_complete) asex_kernel_executor.py:117-118
|
||||
→ _handle_exit_submission: closed | pending | rejected brain_tpsl.py:249
|
||||
→ (pending) later reconcile via _sync_from_kernel → confirm_closed
|
||||
```
|
||||
|
||||
**MAX_HOLD exit (bar path):**
|
||||
```
|
||||
runner scan loop → harness.step() → tpsl_ticker.on_bar() runner.py:328-337
|
||||
→ bars+1 → _resolve_max_hold tpsl_ticker.py:184,215
|
||||
→ build_engine_ingress_provider(engine)(pos, bars) max_hold_modulation.py:120
|
||||
→ blue_max_hold_yield → {100,125,250,375} | 120 fallback max_hold_modulation.py:65
|
||||
→ journal.write_max_hold(row) tpsl_ticker.py:257 / journal.py:330
|
||||
→ bars >= cap → TpSlDecision("max_hold") → same _fire_exit path as above
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Kernel seam — receipts and `exit_leg_ratios` inventory
|
||||
|
||||
### 9.1 Receipt statuses (exec/asex_kernel_executor.py)
|
||||
|
||||
`ReceiptStatus` (asex_kernel_executor.py:30-37): `KERNEL_REJECTED, VENUE_REJECTED, ACKED,
|
||||
PARTIAL_FILL, FULL_FILL, CLOSED`. Derivation `_receipt_from_outcome` (:127-206):
|
||||
- rejected if any emitted event kind ∈ {ORDER_REJECT, RATE_LIMITED} → VENUE_REJECTED (:136-140, 180-181);
|
||||
- `not outcome.accepted` → KERNEL_REJECTED (:182-183);
|
||||
- **CLOSED** iff `intent.action is EXIT` **and** a FULL_FILL event exists **and** final FSM state ∈
|
||||
`{IDLE, POSITION_CLOSED, CLOSED, TRADE_TERMINAL_WRITTEN}` (:174-189);
|
||||
- else FULL_FILL / PARTIAL_FILL / ACKED (:190-195).
|
||||
- `fill_quantity` = Σ fill-event sizes; `average_fill_price` = size-weighted over priced fills;
|
||||
`remaining_quantity` from last fill event (:141-169).
|
||||
|
||||
Exit-relevant properties: `exit_complete = status is CLOSED` (:116-118) — the exact flag
|
||||
`_handle_exit_submission` gates retirement on; `entry_protectable = ENTER ∧ has_fill ∧
|
||||
average_fill_price > 0` (:108-114) — the runner's registration gate. Idempotency:
|
||||
`_intent_fingerprint` (:47-61) hashes economic content; same intent_id + different content →
|
||||
`IntentIdentityCollision`; same id + same content → cached receipt replay (:225-235). Single-writer:
|
||||
only the ASExWorker thread calls `kernel.process_intent` (:1-6, 209-249).
|
||||
|
||||
### 9.2 `KernelIntent.exit_leg_ratios` — every consumer in dita_v2 (prod code)
|
||||
|
||||
Definition: `contracts.py:319` (`KernelIntent.exit_leg_ratios: Tuple[float, ...] = (1.0,)`);
|
||||
slot-side state `contracts.py:211-212` (`TradeSlot.exit_leg_ratios`, `active_leg_index`) with
|
||||
`next_exit_ratio` (:244-248, clamped [0,1], default 1.0 past end) and `consume_exit_leg`
|
||||
(:250-253, advances `active_leg_index`); serialized in `TradeSlot.to_dict` (:293-294).
|
||||
|
||||
| Consumer | file:line | What it does |
|
||||
|---|---|---|
|
||||
| Python→Rust intent payload | rust_backend.py:484 (`_intent_to_payload`) | carries `list(intent.exit_leg_ratios)` into the Rust FSM |
|
||||
| Intent finiteness guard | rust_backend.py:446-449 (`_first_invalid_intent_field`) | rejects non-finite ratios (INVALID_INTENT) at the kernel boundary |
|
||||
| Slot round-trip Rust→Python | rust_backend.py:421-422 (`_slot_from_payload`) | restores `exit_leg_ratios` + `active_leg_index` on slot snapshots |
|
||||
| Slot view leg ops | rust_backend.py:616-623 (`KernelSlotView.next_exit_ratio` / `consume_exit_leg`) | Python mirror that mutates the backend slot |
|
||||
| **Rust FSM — leg state** | `_rust_kernel/src/lib.rs:339,462` (struct fields), `lib.rs:425-438` (`next_exit_ratio`/`consume_exit_leg`) | canonical leg machinery |
|
||||
| **Rust FSM — ENTER stamps ratios** | lib.rs:1356-1361 | slot.exit_leg_ratios = intent's ratios (or `[1.0]`), `active_leg_index = 0` |
|
||||
| **Rust FSM — EXIT sizes the leg** | lib.rs:1434-1444 | `exit_ratio = slot.next_exit_ratio(); exit_size = max(initial_size, size)*ratio`; stamped as the attached exit order's `intended_size` |
|
||||
| **Rust FSM — fill-side leg progression** | lib.rs:2154-2198 | on non-partial exit fill: `all_legs_done_pre` computed BEFORE `consume_exit_leg()` (G4 fix); `should_close = size<=1e-12 ∨ (!partial ∧ all_legs_done)`; else → back to `POSITION_OPEN` awaiting next leg; partial → `EXIT_WORKING` |
|
||||
| Venue adapter (BingX) | bingx_venue.py:409 | copies `intent.exit_leg_ratios` onto the LegacyIntent — **but submits `target_size`**, ratios are carried, not consumed |
|
||||
| Zinc plane persistence | real_zinc_plane.py:102-103 (slot read), :256 (intent row `exit_leg_ratios` column) | persisted/journaled, not decisioned |
|
||||
| Slot hydration elsewhere | flat_and_start_pink.py:284,336,402; monitor_pink.py:104,130,177 | ops tooling, always `(1.0,)` |
|
||||
| Test/gen-only | gen2.py, gen_live_tests.py, _gen_test.py, _build_pink_bodies.py, test_asex_account.py:23, test_pink_persistence.py:413-424,752-766, test_flaws.py:1119 (G4 regression), test_ditav2_chaos_fuzz_adversarial.py:346 | exercise multi-leg (0.5/1.0, 0.3/1.0, 0.25/0.25/1.0, 0.25/0.5/1.0, 0.33/0.33/1.0) |
|
||||
|
||||
### 9.3 Verdict: multi-leg exit IS implemented in the kernel FSM — and UV never uses it
|
||||
|
||||
The Rust FSM genuinely implements multi-leg: ENTER stamps the ladder (lib.rs:1356-1361), each EXIT
|
||||
intent derives the leg's `intended_size` from `initial_size × next_exit_ratio` (lib.rs:1434-1436),
|
||||
and fill handling advances/closes the ladder with the G4-corrected ordering (lib.rs:2154-2198),
|
||||
returning the slot to `POSITION_OPEN` between legs. Two caveats for a port:
|
||||
1. **The venue order quantity is still `intent.target_size`** (bingx_venue.py:404 →
|
||||
`target_size=float(intent.target_size)`); the FSM's ratio-derived `exit_size` shapes the attached
|
||||
order's accounting, so the caller must send per-leg quantities consistent with the declared
|
||||
ratios (exactly what the dita_v2 generated live tests do).
|
||||
2. **UV's promotion path never sets `exit_leg_ratios`** (promotion.py:295-308 omits it) and
|
||||
`_fire_exit` always sends `target_size = pos.quantity` — so in UV today every exit is a single
|
||||
100 % leg and the kernel ladder machinery sits idle but ready. Additional guard relevant to
|
||||
exits: `_exit_asset_mismatch_outcome` (rust_backend.py:764-792) rejects an EXIT naming a
|
||||
different asset than slot 0 holds (2026-07-10/11 phantom-pair fix).
|
||||
|
||||
---
|
||||
|
||||
## 10. WHAT IS MISSING relative to BLUE
|
||||
|
||||
BLUE's live exit stack (nautilus_event_trader + alpha_exit_manager + tp_curve.py + AdvancedSL):
|
||||
|
||||
| # | BLUE mechanism | BLUE behavior (reference) | UV today | Status |
|
||||
|---|---|---|---|---|
|
||||
| M1 | **Cubic TP curve** (`tp_curve.py`) | TP level is a cubic function of conviction/signal strength, not a constant | flat `DOCTRINE_TP_PCT = 0.0035` for every trade (brain_tpsl.py:26; stamped per-position at tpsl_ticker.py:148) | **MISSING** — no per-trade TP computation anywhere in `violet/uv/` |
|
||||
| M2 | **OB TP modulation** (order-book cascade widening, `alpha_exit_manager.py:153` ×1.40 class) | TP silently widened/modulated by OB cascade state each bar | none — `Position.take_profit_pct` is immutable after registration; `replace_position` preserves it (tpsl_ticker.py:321) | **MISSING** (note: BLUE's unlogged ×1.40 was itself the LINK −$1.2K diagnosis; port must include TP_FLOOR + logging) |
|
||||
| M3 | **TP_FLOOR ratchet** | Hard floor under any modulated TP so widening can't erase the fixed TP | none — no floor concept; nothing modulates TP yet, so nothing enforces a floor either | **MISSING** |
|
||||
| M4 | **Catastrophic SL** | Immediate unconditional exit at extreme adverse move, priority above everything | `ExitPriority.CATASTROPHIC = 0` reserved (brain_tpsl.py:102) but never evaluated; only the single −1.2 % contract stop exists | **MISSING** (named placeholder only) |
|
||||
| M5 | **Overlay SL** (EFSM post-win LONG overlay risk logic) | Direction-overlay-aware stop handling on BLUE's live LONG side | UV models base direction only; EFSM mirror exists in runner (`EfsmMirror`, runner.py:211) but feeds no exit logic | **MISSING** |
|
||||
| M6 | **Bucket SL** | Per-bucket (cohort/asset-class) stop aggregation | none — exits are strictly per-position; no cross-position state in TpSlHandler | **MISSING** |
|
||||
| M7 | **Withdrawal SL** | Withdrawal-velocity-driven stop tightening | only the MAX_HOLD `PER_ASSET_WITHDRAWAL` branch (cap→100 bars) uses withdrawal_velocity (max_hold_modulation.py:89-96); no price-stop analogue | **PARTIAL** (hold-time only, not stop-level) |
|
||||
| M8 | **AdvancedSL multi-leg runtime** | Staged partial exits (leg ladders) driven by a runtime SL/TP state machine | kernel FSM fully supports leg ladders (§9.3) but UV never emits `exit_leg_ratios ≠ (1.0,)` and has no leg planner; `ExitPriority.ADVSL = 3` reserved unused (brain_tpsl.py:104) | **MISSING** (kernel-ready, brain-absent) |
|
||||
| — | Trailing TP | retrace-from-best 0.20 % after +0.20 % arm | implemented (brain_tpsl.py:211-233) | present |
|
||||
| — | Fixed TP / contract SL | +0.35 % / −1.2 % | implemented (brain_tpsl.py:220-241) | present |
|
||||
| — | MAX_HOLD OBF modulation | 5-branch 100/125/250/375 | certified port (max_hold_modulation.py) + per-scan journal | present |
|
||||
|
||||
Cross-cutting gaps also worth flagging for parity:
|
||||
- **Price cadence**: 1 s REST mark-price vs BLUE's event-driven bars/ticks; sub-ms OBF/WS is C11
|
||||
(tpsl_ticker.py:8-9, brain_tpsl.py:31).
|
||||
- **bars_held not persisted**: rehydrated positions restart the hold clock at 0 (§5.4).
|
||||
- **Trailing best not rebased** on kernel-truth entry-price reconcile (§3.4).
|
||||
- **"max_hold" trigger not counted** in terminal stats (§5.2).
|
||||
- Stale docstrings claiming env-gated TPSL (tpsl_ticker.py:4-13, runner.py:287) vs the actual
|
||||
always-on doctrine factory.
|
||||
|
||||
---
|
||||
|
||||
## 11. PORT-SURFACE — where each BLUE mechanism attaches
|
||||
|
||||
The proven template is the **MaxHoldIngress pattern** (max_hold_modulation.py): a frozen validated
|
||||
ingress dataclass + a pure `blue_*_yield()` function (bit-faithful branch order, property-testable)
|
||||
+ a `Callable[[position, bars_held], Ingress | None]` provider built over `harness.engine`'s
|
||||
embedded BLUE objects (`build_engine_ingress_provider`) + loud journaled fallback in the ticker +
|
||||
wiring through `maybe_build_ticker(...)` kwargs in `runner.py:288-293`. Every port below should
|
||||
follow it: pure yield module, engine-ingress provider, per-evaluation journal row, no-ingress LOUD
|
||||
fallback to the current doctrine constant.
|
||||
|
||||
| BLUE mechanism | Attach point (class/function/seam) | Shape of the port |
|
||||
|---|---|---|
|
||||
| **M1 cubic TP curve** | `TpSlTicker.register_entry` (tpsl_ticker.py:138-149) — the single place `take_profit_pct` is stamped onto a `Position` | new `tp_curve_modulation.py` with `blue_tp_yield(TpIngress) -> tp_pct`; a `tp_ingress_provider(position_like) -> TpIngress|None` kwarg on `TpSlTicker.__init__`/`maybe_build_ticker`; fallback = `DOCTRINE_TP_PCT`, journaled. Runner passes conviction/signal from the SIZED intent (`_promoted_intent.metadata["promo_raw_conviction"]`, promotion.py:285) at registration time |
|
||||
| **M2 OB TP modulation** | `TpSlTicker.on_bar` (tpsl_ticker.py:170-186) — the per-scan hook that already iterates positions with the fresh OBF snapshot; mutate via a new `TpSlHandler.retarget_tp(trade_id, tp_pct)` (sibling of `replace_position`, brain_tpsl.py:144) | same OBF acquisition as `build_engine_ingress_provider` (max_hold_modulation.py:120-182 — reuse the `_live_mode/_live_bar_idx` clock guard verbatim); per-scan recompute + journal, exactly like `_resolve_max_hold`/`_journal_max_hold` (tpsl_ticker.py:215-272) |
|
||||
| **M3 TP_FLOOR ratchet** | inside the M1/M2 yield function itself (clamp before return), enforced a second time in `TpSlHandler._evaluate` before the TP check (brain_tpsl.py:236) | doctrine constant `DOCTRINE_TP_FLOOR_PCT` in brain_tpsl.py:26-31 block; journal the pre-clamp vs post-clamp value (the BLUE LINK lesson: the widening was *unlogged*) |
|
||||
| **M4 catastrophic SL** | `TpSlHandler._evaluate` (brain_tpsl.py:201-247) — add the check ABOVE the SL check, honoring `ExitPriority.CATASTROPHIC = 0` (brain_tpsl.py:102); new `trigger_type="catastrophic"` + `_count_terminal_trigger` arm (brain_tpsl.py:276) | pure threshold constant (doctrine block); tick-driven, no ingress needed; also add `"max_hold"`/`"catastrophic"` to the stats counter while there |
|
||||
| **M5 overlay SL** | two seams: side/overlay truth enters at `runner.py` where `EfsmMirror` already lives (runner.py:211, 314) and at registration (runner.py:366-373 — pass overlay flag into `register_entry`); evaluation joins `_evaluate` as a side-aware stop branch | extend `Position` with an `overlay` field (validated in `__post_init__`); ingress = EFSM state provider following the MaxHoldIngress pattern |
|
||||
| **M6 bucket SL** | new aggregate pass in `TpSlTicker._run` between `_sync_from_kernel()` and the per-asset tick loop (tpsl_ticker.py:360-372) — the only place all watched positions + fresh prices coexist | bucket map provider kwarg on `maybe_build_ticker`; fires the same `_fire_exit` per member position (D4 semantics free) |
|
||||
| **M7 withdrawal SL** | `TpSlTicker.on_bar` (bar-cadence, OBF-driven) as an SL-tightening sibling of M2: `TpSlHandler.retarget_sl(trade_id, sl_pct)` | reuses `withdrawal_velocity` already flowing through `MaxHoldIngress` (max_hold_modulation.py:27, 159) — the ingress provider can be shared, one acquisition, two yields |
|
||||
| **M8 AdvancedSL multi-leg** | (a) leg plan at entry: `build_kernel_intent_from_decision` (promotion.py:295-308) gains `exit_leg_ratios=` on the ENTER intent — the Rust FSM stamps the ladder at lib.rs:1356-1361; (b) leg-sized exits: `TpSlHandler._fire_exit` (brain_tpsl.py:284-306) sends `target_size = leg quantity` (per §9.3 the venue submits `target_size`; keep it consistent with the declared ratios); (c) leg progression truth: `_sync_from_kernel` already reads `active_exit_order`/size back (tpsl_ticker.py:311-327) — extend to read `active_leg_index` from the slot | kernel is ready today (lib.rs:1418-1459, 2154-2198 incl. G4 fix); the whole port is brain-side: a leg planner in TpSlHandler + partial-exit `_pending_exits` semantics (current pending logic already tolerates `FULL_FILL`-per-leg receipts — a leg fill returns status FULL_FILL, not CLOSED, so `exit_complete=False` keeps it pending until reconcile; that reconcile path must learn "leg done ≠ trade done") |
|
||||
| **Future sub-ms price plane (C11)** | `TpSlTicker.__init__ price_fetch` seam (tpsl_ticker.py:100, 118) and `_run` loop (tpsl_ticker.py:356) | swap REST poll for OBF/WS tick feed; `TickEvent` already carries `source_ts_ns`/`ingest_mono_ns` (brain_tpsl.py:74-79) for latency accounting |
|
||||
|
||||
**Rule of thumb for all ports**: constants go in the brain_tpsl.py doctrine block (contract values,
|
||||
change-with-a-test); acquisition goes in a `build_*_ingress_provider` over `harness.engine`; math
|
||||
goes in a pure `blue_*_yield` module mirroring the BLUE source's predicate order; every evaluation
|
||||
is journaled via a `journal.write_*` sink; and no ingress ⇒ LOUD logged fallback to the current
|
||||
doctrine constant — never a silent invented neutral (`_required_value`, max_hold_modulation.py:109-117).
|
||||
|
||||
*End of crawl.*
|
||||
Reference in New Issue
Block a user