MK2 trailing-stop + scale-out implementation

Implements the corpus-validated strategy reversal documented in 2026-05-14-trailing-stop-corpus-validation. Companion to that page - this one captures the as-built engineering.

Commit: dc73132 on cDSe2403/-MK2.1 + main, 2026-05-14 17:35 CT. Codex handoff: plans/2026-05-14-codex-handoff-trailing-stop-mk2.md Task: #104 Reverses: memory feedback_no_hwm_trailing_language

What shipped

Behavior is gated. Default TRAILING_ENABLED=false keeps the engine byte-identical to prior main (single-shot +10% LMT TP). Setting TRAILING_ENABLED=true engages scale-out + trailing.

When trailing is ON

  1. TP LMT sizing (mixins.py:_place_tp_limit): place LMT for max(1, int(remaining_qty * SCALE_OUT_PCT)). Default 50% of contracts. The other half stays exposed to the trail.

  2. HWM tracking (manager.py:_process_price_tick): when option_mid >= avg_cost * (1 + TRAIL_ENGAGEMENT_PCT) (default +5%), start tracking high-water mark. Persist pos.high_water_mark and pos.hwm_pct on each new high.

  3. Trail exit fire: when option_mid <= pos.high_water_mark * (1 - TRAIL_PCT) (default 5% give-back), fire MarketOrder SELL via _close_remaining(pos, option_price, "TRAIL_EXIT").

  4. Engagement gate prevents noise triggers: HWM does not engage until option mid has crossed +5% above entry. Before engagement, trail logic is dormant.

  5. TP fill handling: filled half-TP does NOT finalize the whole position. Engine continues tracking HWM and trail-managing the remainder. (This was a bug-class the original implementation could have hit; codex caught and handled it.)

Downstream propagation

TRAIL_EXIT is a new exit_reason value, propagated to:

  • scripts/build_daily_report.py - label, color (#4fa3a5), counter
  • scripts/reporting_common.py - CSS class for HTML reports
  • src/cortana/alerts/telegram.py - human label “Trail”
  • src/cortana/storage/decision_logger.py - hit_tp = 1 for TRAIL_EXIT (trail only fires after profitable engagement, so it’s a TP-equivalent for ML labeling)

Config (env vars override config.json defaults)

KeyDefaultRangeMeaning
TRAILING_ENABLEDfalseboolMaster switch
TRAIL_PCT0.05(0, 1)Give-back from HWM that triggers exit
SCALE_OUT_PCT0.5(0, 1)Fraction of position to LMT at +10%
TRAIL_ENGAGEMENT_PCT0.05(0, 2)How far above entry option mid must reach before HWM starts tracking

Required logging (validation hooks)

At INFO level on every engaged trade:

  • HWM_ENGAGED position=X entry=Y option_mid=Z - first engagement
  • HWM_UPDATE position=X new_hwm=Y pct_above_entry=Z% - new high
  • TRAIL_TRIGGERED position=X hwm=Y current=Z trail_pct=W% trigger_price=V - exit fire

These are how Friday’s session will be validated against the corpus prediction.

Tests (all passing)

  • tests/test_position_manager.py - 98 tests, includes 4 trail-specific scenarios:
    • regression with TRAILING_ENABLED=false (byte-identical to prior behavior)
    • trail engagement sequence (entry → HWM → trail fire)
    • SL fires before HWM engagement (trail bypassed)
    • TRAIL_EXIT distinct from PRICE_STOP
  • tests/test_config.py - 28 tests including env-var binding for the 4 new keys

Combined run: 99 passed in 63s (pytest tests/test_position_manager.py tests/test_config.py -q).

Deploy + revert

Friday 2026-05-15 deploy (target):

  1. Set TRAILING_ENABLED=true in scripts/com.cortanaroi.mk2.plist env block
  2. Run E2E pre-flight at 08:08 CT
  3. Engine kicks 08:10 CT
  4. First entry should show TP LMT for ~50 contracts (half of nominal 100)
  5. Monitor first 3-5 trades for HWM_ENGAGED / TRAIL_TRIGGERED log lines

Revert path (single flag):

  1. Set TRAILING_ENABLED=false
  2. Restart engine
  3. Instant return to fixed +10% LMT behavior, no code rollback needed

Residual risk (from Codex’s own report, 8/10 confidence)

Main residual risk is broker event timing around partial TP fills during cancel races, but the code now handles the normal filled half-TP path and keeps the trail-managed remainder live.

This is the same class of race that bit position #356 (inverted-position story) on 2026-05-11. The implementation handles the normal path. If a cancel-race happens, watch for EMERGENCY_EXIT_MANUAL_REQUIRED logs as the existing safety net - flatten_all.py is still available as the manual force-close.