Exit-paden en bescherming — feitelijk gedrag (read-only)

Feitelijke inventarisatie van runtime exit-paden in Krakenbot: orchestratie, exchange-order types, beschermingsvensters, berekeningen en afwijkingen t.o.v. commentaar/Kraken-docs.

Update (implementatie): native trailing wordt strakker gezet via Kraken amend_order met trigger_price_type=pct (amend_trailing_stop_trigger_pct); partial-fill replace plaatst eerst nieuwe trailing, daarna cancel oude; maker TP gebruikt post_only=true; execution_orders.limit_price_quote voor trailing-stop = trail bps. Zie EXIT_PATH_VALIDATION_RUNBOOK.md voor bewijs-runbook.

Update (herstelplan-leakage, maart 2026): Belangrijke wijzigingen in exit-semantiek:

  • Cancel-first exit (B2): cancel_protection_and_market_exit cancelt eerst bescherming, wacht op cancel-or-fill, daarna market exit op balance-qty. Voorkomt double-exit.
  • Staleness guards (C1/C2): alle price_cache-aanroepen gebruiken snapshot_fresh / last_price_fresh met max_age; stale data leidt tot bail, niet tot stille fallback.
  • RecvResult (F2): recv_timeout retourneert RecvResult::Message | Timeout | ChannelClosed; channel close wordt als error geëscaleerd (bail), niet als timeout behandeld.
  • Fill price zero-guard (A3): fills met prijs 0 worden afgewezen in ws_handler vóór ledger-verwerking.
  • Market order deadline (D2): market orders krijgen automatisch deadline = now + 5s.
  • OTO trailing-stop (D4): optionele conditional trailing-stop attached aan entry order; discover_oto_trailing_stop in exit_lifecycle. Kraken WS v2: bij conditional (OTO) mag cl_ord_id niet in add_order — anders EOrder:cl_ord_id is not supported on conditional close. De bot stuurt geen cl_ord_id op de wire, behoudt wel de lokale id in DB + OrderTracker, en koppelt executions via order_id + symbool (OrderTracker::resolve_cl_ord_id, zie Kraken executions / add_order).
  • Exit qty normalisatie (F3): normalize_exit_qty voorkomt dust-accumulatie bij market exits.
  • time_stop_secs=0 guard (F1): clamp naar 60s met EXIT_TIME_STOP_MISCONFIGURED warning.
  • Zie HERSTELPLAN_LEAKAGE.md voor het volledige plan.

1. Exit capability overzicht

PadModule / entrypointExchange order(s)Opmerking
Native trailing-stop (primair)src/execution/exit_lifecycle.rs run_post_fill_exit_phase, src/execution/ignition_exit.rs run_ignition_trailing_exit, src/execution/protection_flow.rs protect_exposureadd_order met order_type: "trailing-stop", triggers: reference=last, price_type=pct, price = trail_bps/100Zie src/exchange/auth_ws.rs add_trailing_stop_order
Statische stop-losssrc/exchange/auth_ws.rs add_stop_loss_orderorder_type: "stop-loss", static triggerNog aanwezig in API-laag; post-fill primary path plaatst dit niet meer (trailing-only in exit/ignition)
Maker take-profit (limit)src/execution/exit_lifecycle.rsadd_order limit + limit_price; post_only wordt als None meegegeven (~regels 412–421)Commentaar noemt post_only maker; implementatie wijkt af
Interne TP → marketZelfde monitor-loopcancel_protection_and_market_exitcancel-first: cancel bescherming, wait_for_cancel_or_fill, daarna market op balance-qty (genormaliseerd via normalize_exit_qty)Prijs-check op price_cache::last_price_fresh (staleness guard) vs tp_level uit fill
Panic PnL → marketexit_lifecycle, ignition_exitIdem cancel-first market exit; bail MARKET_FILL_TIMEOUT_EXPOSURE_RISK bij fill-timeoutDrempel uit ExitConfig.panic_pnl_bps / PANIC_PNL_BPS
Time stop → marketBeide exit-enginesCancel TP + cancel-first trailing, dan markettime_stop_secs; bij 0 wordt 60s gebruikt met EXIT_TIME_STOP_MISCONFIGURED warning (F1)
Ignition state ImmediateExitsrc/execution/ignition_exit.rscancel_sl_and_market_exitBij Quiet/Compression zonder trail_activated
Emergency protectionsrc/execution/protection_flow.rsTrailing ACK → bevestigd; anders try_market_fallback / market_close_exposurecompute_stop_price wordt nog berekend voor logging/normalisatie; submit is trailing-first (~739+)
Position monitor TP / trailsrc/execution/position_monitor.rsBij TP: cancel + market na reconcile; anders amend_stop_loss_triggerAlleen posities met DB execution_orders strategy_context IN ('emergency_stop','exit_sl') én side='sell'shorts vallen hier buiten

Orchestratie na entry-fill: src/execution/runner.rs / src/execution/live_runner.rs: bij ignition_ctxrun_ignition_trailing_exit, anders run_post_fill_exit_phase met exit_config_for_exit_strategy uit src/pipeline/strategy_selector.rs.

1a. Beschermingslaag — deadlock-recovery (doelarchitectuur)

Als private-WS snapshots (balances / own orders) te oud worden terwijl exposure_reconcile nog onbeschermde exposure ziet, kan de combinatie van fail-closed regels en retry-loops leiden tot een langdurige entry_halt zonder aantoonbaar herstelpad. Onderstaande state machine beschrijft de beoogde protection-control flow (truth refresh, gedegradeerde modus, escalatie) — zie het interne plan Protection Deadlock Reliability; implementatie is gefaseerd en wijkt tot die tijd mogelijk af van de huidige code.


2. Scenario-matrix (kern)

Legenda bescherming: volledig = trailing (of na ACK) op volledige protected_qty; gedeeltelijk = qty-mismatch of venster tussen cancel en nieuwe ACK; onbeschermd = geen geldige beschermingsorder / expliciet market-pad zonder hedge.

ScenarioPadBeschermingDuur / oorzaak
Direct na eerste fillrun_post_fill_exit of ignition_exit start direct; trailing add_trailing_stop_orderVolledig na WS ACK van trailingTot ACK: kort venster (timeout ~15s wait); bij reject → geen volledige bescherming in die fase
Partial fill (meer volume)ws_handler.rs: EntryFillInfo.quantity_base = cum_qty_after; exit_lifecycle.rs: bij hogere cum_qty cancel+replace trailing (+ TP)Gedeeltelijk tijdens replaceExpliciet: cancel oude trailing, nieuwe submit; mislukt add → log “partial remains under previous protection”
Na volledige fillZelfde als eerste fill met finale qtyVolledig na ACK
Favorable moveNative trailing volgt peak t.o.v. last (exchange); bot trackeert highest_price/lowest_price voor interne TP/panic/TSL-branchTrail distance (bps) kan strakker via amend_trailing_stop_trigger_pct (ignition + exit_lifecycle); ATR kan later worden geadopteerd als DB eerst leeg was
Adverse moveTrailing kan triggeren; panic onder drempel → marketNa trigger: positie sluit via market fill van trailing
Snelle kleine winstGeen aparte regel; TP alleen als tp_bps en (maker of interne check)Afhankelijk van configInterne TP gebruikt last uit cache
Reject / amend failureTrailing add fail → logs; partial replace fail idem; ignition qty amend faalt → warnRisk: oude order weg of qty foutRuntime-logica
Ontbrekende position/protection stateprotect_exposure: price_cache_emptyPending; truth degraded → prefer_trailing + 15m-ATR trail (fallback 50 bps). Staleness guard via price_cache::snapshot_fresh(max_age)Onbeschermd / Pending tot cache/ACKPlatform + runtime
Position resize (increase)Zie partial fill replace; ignition: amend_order qty op zelfde order idSoms als amend slaagtKraken/platform
Reconnect / startup open posexposure_reconcile + protect_exposure in runner/live_runnerTrailing of market fallbackGeen automatische hervatting van volledige exit_lifecycle voor oude trades — alleen protection + optioneel monitor
Open positie zonder actieve protectionDetectie via reconcile → protect_exposure / retry loopTot bescherming: onbeschermd
Time-based signalDeadline in loop → time stop marketCancel bescherming dan market
TP-conditieMaker fill op exchange OF last ≥ tp_level (internal) → market exit na cancel beschermingTijdens market exit: bescherming wordt gecanceld na market ACK (cancel_sl_and_market_exit)Invariant bedoeld: bescherming actief tot market ACK
Trailing strakker zetten (lifecycle)exit_lifecycle: gunstige move → trail_tighten_candidate_bps + amend_trailing_stop_trigger_pct (nooit verruimen); plus ATR-mid-trade adoptieGeen oudere trailing_stop_placed-variabele meer in deze module — zie EXIT_PATHS_VALIDATION_202603.mdExchange amend ACK-afhankelijk

3. (T)SL plaatsbaarheid matrix

ScenarioStatische SL (API)Native TSLBlokkeerders
Direct na fillNiet gebruikt als primair in post-fill enginesJa (primair)Reconcile AllowExitSubmit; WS ACK timeout
Tijdens fill (vóór ACK entry)Nee op exchange voor die entryBot start exit na fill processing
Pas na full fillZelfde als partial: cum_qty gedrevenJa
Na increaseCancel+replace trailing (exit_lifecycle); ignition: qty amendJa / somsAmend/replace fail
Na reduceNiet expliciet in geziene exit-loopImplicit via balance bij market exits
Startup bestaande positieprotect_exposure: trailing, geen static SL submit in happy pathJaprice_cache_empty → pending; trailing reject → market
Cancel/reject protectionRetry / market fallbackOpnieuw proberenInsufficient funds → doctrine breach / remediation
Geen avg_cost, wel live prijscompute_stop_price gebruikt cache + emergency % (protection_flow.rs ~1063+)Ja (trail distance uit ATR helper of degrade constant)Trailing vereist nog steeds geldige qty/side
Mismatch position vs protection sizeload_open_exit_qty_per_symbol in exposure layerGedeeltelijk beschermdRuntime + DB vs exchange
Exchange errorsGelogd; market fallbackSomsPlatform validation

Ignition “tighten trail”: current_trail_bps wordt in Rust aangepast (mode-based + policy); de exchange-distance wordt bij native trailing gezet via amend_trailing_stop_trigger_pct zodra trail_activated en de gewenste trail strakker is dan exchange_trail_bps. Optioneel amend_order voor qty bij extra fills blijft.

Exit_lifecycle native trailing amend: Strakkere trail-distance en ATR-mid-trade adoptie gebruiken dezelfde amend_trailing_stop_trigger_pct-flow na reconcile. De oudere amend_stop_loss_trigger-loop blijft alleen relevant voor het niet-native-trailing pad (use_trailing_stop == false met static SL-semantiek — type-inconsistent als sl_order_id al een trailing id is).


4. TP-gebruik analyse

  • Inputs TP-niveau: fill_price_f64 (met staleness guard price_cache::snapshot_fresh als fallback; bail bij stale/empty) en tp_bps uit ExitConfig / strategy_selector (per SelectedExitStrategy).
  • Maker TP: limit order naast trailing; timeout 30s (MAKER_TP_FILL_TIMEOUT_SECS) → cancel TP + cancel-first market exit op balance_cache qty (genormaliseerd via normalize_exit_qty).
  • Interne TP: als geen maker TP order id: vergelijk price_cache::last_price_fresh (staleness guard) met tp_level.
  • Fees/spread/ATR: niet expliciet in TP-formule; ATR (15m bars, zie trail_atr.rs) voor trail_bps, niet voor TP.
  • Parallel: position_monitor harde 200 bps TP → market voor subset van posities (zie §1 filter).

5. Docs-conformiteit vs eigen interpretatie

Conform Kraken WS v2 patronen (hoog niveau):

  • trailing-stop met triggers.reference = last en price_type = pct sluit aan bij gangbare Spot WS v2 shape (zie ook .cursor/rules/kraken-ws-implementation-checklist.mdc).
  • amend_order met trigger_price + trigger_price_type: static voor stop-loss is wat auth_ws.rs implementeert.

Eigen interpretatie / afwijkingen:

  • Trail bps v1: src/execution/trail_bps_v1.rsmax(fee_floor, α(horizon)·ATR15m, M_min) × regime, daarna clamp + optionele strategy floor. Ruwe ATR15m (ongeklemd) uit src/execution/trail_atr.rs. Zie docs/TRAIL_BPS_V1.md. Fees: 2× taker + 10 bps slip (account-tier). Horizon alleen via α op time_stop_secs (exit-config / ignition / emergency 300s).
  • Emergency trail distance: should_prefer_trailing_by_expectancy (PnL ≥ 20 bps) → zelfde 15m-ATR-helper; niet-prefer-trailing pad gebruikt nog DEGRADE_TRAIL_BPS (0,8%) — protection_flow.rs.
  • Maker TP zonder post_only: true: wijkt af van commentaar in exit_lifecycle.rs en van “post_only maker” intentie.
  • Variabelen sl_order_id / logs EXIT_SL_*: vaak native trailing order id — semantiek is misleidend t.o.v. echte stop-loss.
  • Ignition module comment “SL-only”: gedrag is native trailing, niet static SL.

6. Open risico’s / onduidelijkheden

  1. Type-mismatch: amend_stop_loss_trigger op order id dat een trailing-stop is (exit_lifecycle wanneer use_trailing_stop == false).
  2. Historische doc vs code: oude beschrijving van trailing_stop_placed / breakeven-tak komt niet meer voor in huidige exit_lifecycle.rs; trail-gedrag loopt via o.a. tighten + ATR-adoptie. Zie EXIT_PATHS_VALIDATION_202603.md.
  3. Ignition / exit_lifecycle: als amend/ACK faalt, kan gewenste trail tijdelijk alleen in-memory blijven t.o.v. exchange — retries en logs zijn runtime-afhankelijk.
  4. Position monitor: alleen side = 'sell' protection rows — short posities niet in deze monitor-query.
  5. Bescherming vóór eerste fill: exit_state.rs conceptueel ProtectedPendingFill; runtime hangt exchange-bescherming aan op (partial) fill (ws_handler + runner).
  6. Kraken gedrag: amend_order met trigger_price_type=pct voor trailing-stop distance wordt gebruikt voor strakker zetten + ATR-adoptie; platform-edge-cases blijven monitoren via ACK/logs.

7. Opgeloste risico’s (herstelplan-leakage, maart 2026)

  • Double-exit risico → opgelost via cancel-first flow (cancel_protection_and_market_exit) + classify_cancel_or_fill order_id verificatie (B2).
  • Stale price decisions → opgelost via staleness guards snapshot_fresh / last_price_fresh op alle price_cache-callsites (C1/C2).
  • Silent channel close als timeout → opgelost via RecvResult enum met expliciete ChannelClosed variant (F2).
  • Fill met prijs 0 in ledger → opgelost via zero-guard in ws_handler vóór fills_ledger (A3).
  • VWAP-corruptie bij positiereductie → opgelost via expliciete branch-logica in fills_ledger (A1).
  • Fees ontbreken in realized PnL → opgelost via compute_realized_pnl met proportionele fee-aftrek (A2).
  • REST in execution hot path → volledig geëlimineerd; price_cache (WS-fed) vervangt REST get_best_bid/ask (D1).
  • Broadcast lag ongedetecteerd → opgelost via was_lagged() + escalatie naar PRIVATE_WS_HUB_RELIABILITY_DEGRADED (B4).
  • Dust bij exit qty → opgelost via normalize_exit_qty (F3).

Primaire bronbestanden