# ── TURBINE · RUNNERS leg (v2 supervised) ───────────────────────────
# SAME funding-fade rotation as VOLUME, but a patient DSL that lets winners run.
#
# Ported from senpi-skills/turbine (producer v3.2.2, config v3.3) to the v2
# supervised external-scanner contract. The producer daemon is gone: the runtime
# supervises ../scanners/scan.py (SHARED with the volume leg) and calls
# scan(inputs, ctx) every `interval_seconds`. Both turbine legs run the SAME
# rotation alpha — only the sizing (inputs) and DSL exit differ. See scan.py.
#
# RUNNERS is OPTIONAL: leave TURBINE_RUNNERS_WALLET unset to run pure volume
# (don't launch this recipe). The two legs are independent runtimes — no coupling.
name: turbine-runners
version: 2.0.0
group: turbine                      # agent-facing handle (BOTH legs share it)
description: >
  TURBINE RUNNERS — Identical funding-fade rotation to the VOLUME leg (same
  universe, same direction logic), but a patient DSL exit: most positions still
  exit early via Phase 1 / weak_peak, while the ~5% that catch a real directional
  move ratchet through a 5-tier Phase 2 (T0..T4) up to +50% margin ROE before
  retrace. 2 slots @ $1300 margin, 5x, 4h hard timeout. Paired with the VOLUME
  leg on a separate wallet.

strategy:
  wallet: "${TURBINE_RUNNERS_WALLET}"
  slots: 2                          # source slots.runners 2
  margin_per_slot: 100              # FALLBACK sizing — used only if a signal omits marginPct. scan.py emits per-signal marginPct (% of withdrawable, from inputs below), which WINS. TEST 100; prod source margin.runners 1300 (config v3.3)
  default_leverage: 5               # source leverage.runners 5 (clamped per-asset in scan.py)
  trading_risk: moderate            # source trading_risk balanced → nearest v2 tier moderate
  enabled: true

scanners:
  - name: position_tracker          # built-in: feeds DSL exit engine
    type: position_tracker
    interval_seconds: 10            # source interval 10s

  - name: turbine_runners_signals   # supervised external scanner (SHARED scan.py)
    type: external_scanner
    path: ./scanners                # shared rotation scanner (one dir, two recipes)
    entrypoint: scan.py
    interval_seconds: 60            # source daemon interval_seconds 60
    timeout_seconds: 45             # source tick_timeout 45
    default_signal_validity_seconds: 120   # 2x tick; pass-through gate re-scores next tick
    state_history_max_count: 200    # holds rotation index + held/last-closed/dedup records
    inputs:
      # ── rotation knobs (config v3.3; identical to volume) ──
      xyzWeight: 0.80               # source xyzWeight 0.80
      spreadMainBps: 3             # source spread.mainBps 3
      spreadXyzBps: 10             # source spread.xyzBps 10
      # ── per-leg sizing (runners) ──
      leverage: 5                  # source leverage.runners 5
      marginPct: 20                # per-signal % of withdrawable; scan.py emits it TOP-LEVEL → runtime honors it as the EFFECTIVE sizing (overrides margin_per_slot). 20 is an example default; prod source 1300 USD intent must be sized as a percent
      maxSlots: 2                  # source slots.runners 2 (per-tick emission ceiling)
    signal_data_schema:
      thesis: { type: string }
      fundingRegime: { type: string, required: false }
      fundingAnnualizedPct: { type: number, required: false }
      spreadBps: { type: number, required: false }
      isXyz: { type: boolean, required: false }

actions:
  - name: position_tracker_action   # rule-based lifecycle → DSL
    action_type: POSITION_TRACKER
    decision_mode: rule
    scanners: [position_tracker]

  - name: turbine_runners_entry     # OPEN_POSITION, rule mode (source LLM gate was pure pass-through)
    action_type: OPEN_POSITION
    decision_mode: rule
    scanners: [turbine_runners_signals]
    params:
      order_type: FEE_OPTIMIZED_LIMIT
      fee_optimized_limit_options:
        ensure_execution_as_taker: true    # source runners exit taker true (don't let a winner slip)
        execution_timeout_seconds: 60
    context:
      - type: signal
        scanner: turbine_runners_signals

# ── EXIT (DSL — patient; lets the ~5% real movers ride) ──
exit:
  engine: dsl
  interval_seconds: 30              # source exit interval_seconds 30
  order_type: FEE_OPTIMIZED_LIMIT
  fee_optimized_limit_options:
    ensure_execution_as_taker: true    # source runners exit taker true
    execution_timeout_seconds: 60
  dsl_preset:
    hard_timeout:
      enabled: true
      interval_in_minutes: 240      # source hard_timeout 240min (4h absolute cap)
    weak_peak_cut:
      enabled: true
      interval_in_minutes: 90       # source weak_peak 90min
      min_value: 3.0                # source weak_peak min 3%
    dead_weight_cut:
      enabled: true
      interval_in_minutes: 120      # source dead_weight 120min (flat 2h → cut)
    phase1:
      enabled: true
      max_loss_pct: 30.0            # source phase1 max_loss 30
      retrace_threshold: 8          # source phase1 retrace 8
      consecutive_breaches_required: 1
    phase2:
      enabled: true
      tiers:                        # source T0..T4 — carried verbatim
        - { trigger_pct: 5,  lock_hw_pct: 0  }
        - { trigger_pct: 10, lock_hw_pct: 35 }
        - { trigger_pct: 20, lock_hw_pct: 55 }
        - { trigger_pct: 35, lock_hw_pct: 75 }
        - { trigger_pct: 50, lock_hw_pct: 85 }

# ── RISK guard rails (from runtime-runners.yaml; minutes → seconds) ──
risk:
  data_retention_seconds: 259200    # 72h (was data_retention_hours 72)
  guard_rails:
    daily_loss_limit_pct: 25
    max_entries_per_day: 50
    bypass_max_entries_per_day_on_profit: true
    max_consecutive_losses: 6
    cooldown_seconds: 300           # was cooldown_minutes 5
    drawdown_halt_pct: 25
    drawdown_reset_on_day_rollover: false
    per_asset_cooldown_seconds: 600 # was per_asset_cooldown_minutes 10
