{
  "schema_version": "1.0.0",
  "bot_id": "3.4",
  "bot_name": "Neg-Risk Sum Arb",
  "slug": "neg-risk-sum-arb",
  "layer": "Strategy",
  "layer_key": "strat",
  "bot_class": "Alpha Strategy",
  "authority": [
    "Trade"
  ],
  "status": "live",
  "readiness": "General live",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Strategy",
    "bot_class": "Alpha Strategy",
    "authority": "Trade",
    "runs_before": "Risk guardrail pipeline",
    "runs_after": "Market scanner / opportunity feed",
    "applies_to": "Negative-risk (multi-outcome) markets where the sum of YES token best asks across all outcomes (excluding 'Other') falls below 1.00 pUSD",
    "default_mode": "general_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Strategy pod"
  },
  "purpose": "Neg-Risk Sum Arb exploits pricing dislocations on Polymarket\u2019s negative-risk (multi-outcome) markets, where the sum of YES token prices across N outcomes must equal $1.00 at resolution. When sum(YES asks across outcomes) < $1.00 net of fees, the bot buys the underpriced YES tokens and \u2014 where profitable \u2014 routes the position through the NegRiskAdapter on Polygon to convert NO tokens across the set into pUSD. The 'Other' outcome is always excluded from the conversion path. This is a user-controlled execution tool that targets a structural pricing constraint specific to negative-risk event design on Polymarket. It is the multi-outcome counterpart to sum-to-one-arb.",
  "why_it_matters": [
    {
      "failure": "Sum computed without excluding 'Other' outcome",
      "consequence": "The 'Other' token is illiquid and reprices discontinuously. Including it in the sum produces false edge signals that cannot be closed via the NegRiskAdapter path."
    },
    {
      "failure": "NegRiskAdapter path not available (market not on negRisk contract)",
      "consequence": "Buying all YES tokens across N outcomes without a conversion path leaves open positions on all legs; settlement risk is uncapped until individual markets resolve."
    },
    {
      "failure": "feeRateBps hardcoded on signed order (V1 pattern)",
      "consequence": "CLOB V2 rejects orders with feeRateBps. Fees are operator-set at match time. All signed orders must omit this field."
    },
    {
      "failure": "Partial fill on one outcome leg leaves unbalanced multi-leg exposure",
      "consequence": "In a 4-outcome event, buying 3 of 4 YES tokens does not produce a guaranteed $1 settlement and creates directional risk on the unfilled outcome."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Outcome token list and YES token IDs for the neg-risk event",
      "source": "gamma (Gamma API \u2014 negRisk/enableNegRisk flag + condition ID)",
      "required": true,
      "use": "Enumerate all N outcome tokens; identify and exclude the 'Other' outcome token."
    },
    {
      "input": "Best ask for each YES outcome token",
      "source": "ws_market (CLOB WebSocket book stream)",
      "required": true,
      "use": "Compute sum(YES asks) across N-1 outcomes (excluding Other) and measure edge against 1.00 pUSD."
    },
    {
      "input": "Top-of-book depth per outcome",
      "source": "clob_public",
      "required": true,
      "use": "Size each leg to the minimum available depth across all outcomes up to max_leg_size_usd."
    },
    {
      "input": "NegRiskAdapter contract availability and condition ID",
      "source": "onchain (NegRiskAdapter on Polygon)",
      "required": true,
      "use": "Verify the NegRiskAdapter conversion path is live for this event before committing to the arb."
    },
    {
      "input": "Platform fee rate per market category",
      "source": "onchain (CTFExchangeV2 fee config)",
      "required": true,
      "use": "Estimate per-leg fee drag; crypto \u22641.80%, sports 0.75%, geopolitical free."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Abort all intent emission if KillSwitch is active."
    },
    {
      "input": "Builder code bytes32",
      "source": "internal config",
      "required": true,
      "use": "Injected into builder field on every signed V2 order for attribution."
    }
  ],
  "raw_params": [
    "min_edge_bps \u00b7 int",
    "prefer_conversion_path \u00b7 bool",
    "max_outcomes_per_trade \u00b7 int",
    "exclude_other_outcome \u00b7 bool (locked true)"
  ],
  "parameters": [
    {
      "name": "min_edge_bps",
      "default": 20,
      "warning": 12,
      "hard": 8,
      "controls": "Minimum net edge in basis points required across the full outcome set before emitting any OrderIntents. Higher default than sum-to-one-arb to account for N-leg fee drag.",
      "why_default_matters": "With N outcomes each incurring taker fees, total fee drag grows proportionally. 20 bps default provides comfortable margin above multi-leg fees on crypto markets.",
      "threshold_logic": [
        {
          "condition": "net_edge_bps \u2265 20",
          "action": "EMIT N OrderIntents across all non-Other outcomes"
        },
        {
          "condition": "12 \u2264 net_edge_bps < 20",
          "action": "WARN NEG_RISK_SUM_ARB_EDGE_MARGINAL \u2014 emit with reduced size (50%)"
        },
        {
          "condition": "net_edge_bps < 8 (hard floor)",
          "action": "SKIP \u2014 NEG_RISK_SUM_ARB_NO_EDGE"
        }
      ],
      "dev_check": "if net_edge_bps < params.hard: return skip('NEG_RISK_SUM_ARB_NO_EDGE')",
      "user_facing": "The combined price of all outcome tokens in this multi-outcome market was not low enough to trade profitably after accounting for multi-leg fees."
    },
    {
      "name": "prefer_conversion_path",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true, bot prefers the NegRiskAdapter burn-NO-tokens\u2192pUSD conversion path over holding all YES tokens to resolution. This crystallises the arb profit without waiting for the event to resolve.",
      "why_default_matters": "The conversion path (burn NO tokens \u2192 pUSD via NegRiskAdapter) is lower-risk than holding N YES positions open to resolution. Prefer it when available.",
      "threshold_logic": [
        {
          "condition": "prefer_conversion_path=true and NegRiskAdapter available",
          "action": "Route through NegRiskAdapter after fills"
        },
        {
          "condition": "prefer_conversion_path=true and NegRiskAdapter unavailable",
          "action": "Hold YES positions; warn NEG_RISK_SUM_ARB_NO_CONVERSION_PATH"
        }
      ],
      "dev_check": "if params.prefer_conversion_path and adapter.available: EMIT NegRiskConvertRoute",
      "user_facing": "Positions will be converted to pUSD immediately after filling where possible."
    },
    {
      "name": "max_outcomes_per_trade",
      "default": 8,
      "warning": 6,
      "hard": 12,
      "controls": "Maximum number of outcome legs to include in a single arb trade. Events with more outcomes than this ceiling are skipped.",
      "why_default_matters": "More legs increase total fee drag and the risk of a partial fill leaving an unbalanced position. 8 outcomes cover the vast majority of Polymarket neg-risk events.",
      "threshold_logic": [
        {
          "condition": "N outcomes \u2264 8",
          "action": "Normal multi-leg emission"
        },
        {
          "condition": "8 < N \u2264 12",
          "action": "WARN NEG_RISK_SUM_ARB_HIGH_OUTCOME_COUNT; emit with 50% size per leg"
        },
        {
          "condition": "N > 12",
          "action": "SKIP \u2014 too many legs for safe atomic execution"
        }
      ],
      "dev_check": "if len(outcomes) > params.hard: return skip('NEG_RISK_SUM_ARB_NO_EDGE')",
      "user_facing": ""
    },
    {
      "name": "exclude_other_outcome",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "Always exclude the 'Other' outcome token from the sum computation and from order emission. Locked to true \u2014 cannot be disabled.",
      "why_default_matters": "The 'Other' token covers residual probability and is illiquid and non-standard. Including it in an arb creates unquantifiable tail risk.",
      "threshold_logic": [
        {
          "condition": "Always true (locked)",
          "action": "Other outcome excluded from all calculations"
        }
      ],
      "dev_check": "outcomes = [o for o in event.outcomes if o.name.lower() != 'other']",
      "user_facing": ""
    }
  ],
  "default_config": {
    "bot_id": "strat.neg_risk_sum_arb",
    "version": "2.1.0",
    "mode": "general_live",
    "defaults": {
      "min_edge_bps": 20,
      "prefer_conversion_path": true,
      "max_outcomes_per_trade": 8,
      "exclude_other_outcome": true
    },
    "locked": {
      "exclude_other_outcome": true,
      "max_outcomes_per_trade": {
        "max": 12
      },
      "min_edge_bps": {
        "min": 8
      }
    }
  },
  "implementation_flow": [
    "Check KillSwitch active flag; if active, skip and emit no OrderIntents.",
    "FETCH Gamma API for all active neg-risk events (enableNegRisk=true); extract outcome token lists.",
    "Exclude 'Other' outcome token from each event's outcome list.",
    "If len(outcomes) > max_outcomes_per_trade, skip this event.",
    "Verify NegRiskAdapter availability for event condition ID via onchain call.",
    "Subscribe to ws_market book updates for all YES token IDs of each eligible event.",
    "On each book tick: compute sum_YES_asks = sum(best_ask[outcome] for outcome in outcomes).",
    "Compute raw_edge_bps = (1.00 - sum_YES_asks) * 10000.",
    "Deduct total fee drag across N legs; compute net_edge_bps = raw_edge_bps - N * per_leg_fee_bps - fee_buffer_bps.",
    "If net_edge_bps < min_edge_bps hard floor (8), emit DecisionReport intent_emitted=false NEG_RISK_SUM_ARB_NO_EDGE; skip.",
    "If 8 \u2264 net_edge_bps < 20, emit WARN NEG_RISK_SUM_ARB_EDGE_MARGINAL; reduce leg size 50%.",
    "Compute legSize = toPusdUnits(min(min_depth_across_outcomes, max_leg_size_usd) * sizeMultiplier).",
    "Emit one OrderIntent per outcome leg: side=buy, outcome=YES, price=best_ask, tif=FOK, builder={code, fee_bps: 25}. No feeRateBps on any signed order \u2014 fees are operator-set at match time in V2.",
    "If prefer_conversion_path and NegRiskAdapter available: EMIT NegRiskConvertRoute after fill confirmations (burn NO tokens \u2192 pUSD via NegRiskAdapter).",
    "Emit DecisionReport with intent_emitted=true, edge_bps, outcome_count, conversion_path_used."
  ],
  "decision_logic": {
    "approve": "net_edge_bps \u2265 min_edge_bps, NegRiskAdapter confirmed available, all outcome legs have depth, outcome count \u2264 max_outcomes_per_trade, KillSwitch inactive. Emit N OrderIntents.",
    "reshape_required": "Not applicable \u2014 strat bots emit OrderIntents; reshaping is handled downstream by the Risk guardrail pipeline.",
    "reject": "net_edge_bps < 8 bps hard floor; NegRiskAdapter unavailable and prefer_conversion_path=true; outcome count > 12; KillSwitch active; stale feed. Emit DecisionReport intent_emitted=false.",
    "warning_only": "net_edge_bps between 8 and 20 triggers NEG_RISK_SUM_ARB_EDGE_MARGINAL and 50% size reduction before emitting."
  },
  "decision_output_schema": "OrderIntent",
  "decision_output_example": {
    "intent_id": "oi_01HX9NGRSUM4A1B",
    "trace_id": "tr_01HX9NGRSUM4VR5",
    "market_id": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
    "outcome": "YES",
    "side": "buy",
    "price": "0.242",
    "size_pUSD": "400.00",
    "tif": "FOK",
    "post_only": false,
    "builder": {
      "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
      "fee_bps": 25
    },
    "negrisk_aware": true,
    "decision": {
      "edge_bps": 22.1,
      "outcome_count": 4,
      "conversion_path": "NegRiskAdapter",
      "reasons": [
        "NEG_RISK_SUM_ARB_EDGE_PRESENT"
      ]
    },
    "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
  },
  "developer_log": {
    "bot_id": "strat.neg_risk_sum_arb",
    "event_condition_id": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
    "outcome_count": 4,
    "sum_YES_asks": 0.978,
    "raw_edge_bps": 22.0,
    "total_fee_drag_bps": 14.2,
    "net_edge_bps": 22.1,
    "leg_size_pusd": 400.0,
    "conversion_path": "NegRiskAdapter",
    "intent_emitted": true,
    "reason": "NEG_RISK_SUM_ARB_EDGE_PRESENT",
    "emitted_at_ms": 1746789700000
  },
  "user_explanations": [
    {
      "situation": "Multi-outcome arb trade initiated",
      "message": "The combined price of all outcome tokens in this multi-outcome market was below $1. Orders were placed across all outcomes to capture this pricing gap. Positions may be immediately converted to settled value via the NegRisk contract."
    },
    {
      "situation": "No edge in multi-outcome market",
      "message": "The combined outcome prices did not create a profitable opportunity after accounting for fees on each leg. No orders were placed."
    },
    {
      "situation": "Conversion path unavailable",
      "message": "The NegRisk contract path was not available for this event. Positions are being held to settlement."
    },
    {
      "situation": "Too many outcomes to trade safely",
      "message": "This event has more outcome tokens than the configured maximum for safe multi-leg execution. The trade was skipped."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Partial fill: N-1 of N outcome legs fill FOK but one leg's price moves, leaving an unbalanced multi-outcome position that does not sum cleanly to $1.",
    "false_positive_risk": "Stale book data shows sum(YES asks) < 1.00 when the live market has already rebalanced, causing the bot to attempt an arb on prices that no longer exist.",
    "false_negative_risk": "NegRiskAdapter availability check has latency; a valid arb is skipped because the adapter check returns stale-unavailable during a brief RPC delay.",
    "safe_fallback": "If book data is stale (> 5s) or the NegRiskAdapter cannot be confirmed available, skip and emit DecisionReport intent_emitted=false. Never enter a partial multi-leg position deliberately.",
    "required_dependencies": [
      "Gamma API neg-risk event list (enableNegRisk=true)",
      "ws_market book stream (all YES token IDs per event)",
      "clob_public depth per outcome",
      "onchain NegRiskAdapter contract (Polygon)",
      "KillSwitch active flag",
      "internal builder code"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Emit N intents when sum(YES asks) = 0.978 across 4 outcomes (net_edge > 20 bps)",
        "setup": "outcomes=[0.242,0.245,0.248,0.243], fee_buffer=40, min_edge=20",
        "expected": "4 OrderIntents emitted; intent_emitted=true; negrisk_aware=true on each"
      },
      {
        "test": "Skip when sum(YES asks) = 0.992 (raw edge < 8 bps after fees)",
        "setup": "sum_YES=0.992, N=4, per_leg_fee_drag=5bps",
        "expected": "No OrderIntents; DecisionReport intent_emitted=false, reason=NEG_RISK_SUM_ARB_NO_EDGE"
      },
      {
        "test": "Always exclude 'Other' outcome from sum and from intent emission",
        "setup": "outcomes=[A, B, C, D, Other], sum without Other = 0.976",
        "expected": "4 intents (A,B,C,D); Other never included; sum check uses 4 outcomes only"
      },
      {
        "test": "Skip event with > 12 outcomes (max_outcomes_per_trade hard limit)",
        "setup": "event has 14 outcomes",
        "expected": "No OrderIntents; reason=NEG_RISK_SUM_ARB_NO_EDGE"
      },
      {
        "test": "Route NegRiskConvertRoute when prefer_conversion_path=true and adapter available",
        "setup": "prefer_conversion_path=true, adapter.available=true",
        "expected": "NegRiskConvertRoute emitted after fill confirmations"
      },
      {
        "test": "Skip when KillSwitch active",
        "setup": "killswitch.active=true",
        "expected": "No OrderIntents emitted"
      }
    ],
    "integration": [
      {
        "test": "Full cycle: Gamma API \u2192 outcome list \u2192 book ticks \u2192 N signed V2 OrderIntents \u2192 NegRiskAdapter conversion",
        "expected": "All N orders contain builder.code (bytes32), no feeRateBps, EIP-712 domain version '2'; NegRiskConvertRoute emitted post-fill"
      },
      {
        "test": "NegRiskAdapter unavailable triggers warn and hold-to-resolution path",
        "expected": "NEG_RISK_SUM_ARB_NO_CONVERSION_PATH warn emitted; intents still emitted if edge > min; positions held"
      }
    ],
    "property": [
      {
        "property": "Bot never emits a subset of outcome legs; always emits full N-outcome set or none",
        "required": "Always true \u2014 partial fills are handled by FOK on individual orders, not by design"
      },
      {
        "property": "'Other' outcome is never included in any OrderIntent or sum computation",
        "required": "Always true \u2014 exclude_other_outcome is locked"
      },
      {
        "property": "feeRateBps field is never present on any emitted OrderIntent",
        "required": "Always true \u2014 V2 fees are operator-set at match time"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Multi-outcome arb: YES tokens across N outcomes should sum to $1.",
  "legacy_pm_signals": [
    "Gamma API: outcome-token list per neg-risk event",
    "CLOB book stream on each outcome token",
    "Neg-risk conversion contract availability"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "strategy_decision"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "clob_auth",
    "ws_market",
    "onchain",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.0",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1 (USDC.e, feeRateBps on signed order, HMAC builder)",
      "to": "v2 (pUSD, fees operator-set at match time, builderCode bytes32)",
      "reason": "CLOB V2 cutover",
      "action_taken": "Switched to py-clob-client-v2. Removed feeRateBps from all signed order construction \u2014 fees are operator-set at match time by CTFExchangeV2. Updated collateral from USDC.e to pUSD. Injected builder field (bytes32) on every leg. EIP-712 Exchange domain version updated from '1' to '2'. NegRiskAdapter path updated to V2 contract address on Polygon."
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Core strategy is NegRiskAdapter convert-arb: buy YES tokens across all N non-Other outcomes when sum(YES asks) < $1, then burn NO tokens via NegRiskAdapter on Polygon to receive pUSD without waiting for resolution. feeRateBps is not present on any signed order \u2014 fees are operator-set at match time."
  },
  "reference_implementation": {
    "summary": "Scans Gamma API for neg-risk events, subscribes to book streams for all YES token IDs, computes net edge across N outcomes, and emits N FOK OrderIntents when edge exceeds threshold. Optionally routes through NegRiskAdapter for immediate pUSD conversion.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION scanNegRiskEvents():\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    RETURN\n\n  // --- 1. Fetch active neg-risk events from Gamma ---\n  events = FETCH gamma.GET('/markets?negRisk=true&active=true')\n  FOR event IN events:\n    outcomes = [o for o in event.outcomes if o.name.lower() != 'other']  // exclude Other (locked)\n    IF len(outcomes) > params.max_outcomes_per_trade:\n      SKIP event  // too many legs\n\n    // --- 2. Verify NegRiskAdapter available ---\n    adapterAvailable = FETCH onchain.NegRiskAdapter.isConditionRegistered(event.conditionId)\n\n    // --- 3. Subscribe to book streams for all YES token IDs ---\n    tokenIds = [o.yes_token_id for o in outcomes]\n    ws_market.subscribe('book', tokenIds)\n\nFUNCTION onBookTick(event, outcomes, tick):\n  // --- 4. Check freshness ---\n  IF isStale(tick, maxAgeS=5):\n    EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')\n    RETURN\n\n  // --- 5. Compute edge ---\n  asks = [tick[o.yes_token_id].best_ask for o in outcomes]\n  sum_YES_asks = SUM(asks)\n  raw_edge_bps = (1.00 - sum_YES_asks) * 10000\n\n  // --- 6. Fee drag across N legs ---\n  feeRate   = FETCH onchain.feeConfig(event.conditionId)\n  N         = len(outcomes)\n  total_fee = SUM(feeRate * asks[i] * (1 - asks[i]) * 10000 for i in range(N))\n  net_edge_bps = raw_edge_bps - total_fee - params.fee_buffer_bps\n\n  // --- 7. Hard floor ---\n  IF net_edge_bps < params.min_edge_bps_hard:  // 8 bps\n    EMIT DecisionReport(intent_emitted=false, reason='NEG_RISK_SUM_ARB_NO_EDGE')\n    RETURN\n\n  // --- 8. Warning threshold ---\n  legSizeMultiplier = 1.0\n  IF net_edge_bps < params.min_edge_bps:  // 20 bps default\n    WARN('NEG_RISK_SUM_ARB_EDGE_MARGINAL')\n    legSizeMultiplier = 0.5\n\n  // --- 9. Leg sizing ---\n  minDepth = MIN(tick[o.yes_token_id].depth_pusd for o in outcomes)\n  legSize  = toPusdUnits(min(minDepth, params.max_leg_size_usd) * legSizeMultiplier)\n\n  // --- 10. Emit N OrderIntents (V2: no feeRateBps; builder field carries code) ---\n  FOR i, outcome IN enumerate(outcomes):\n    EMIT OrderIntent(\n      market_id     = outcome.market_id,\n      outcome       = 'YES',\n      side          = 'buy',\n      price         = asks[i],\n      size_pUSD     = legSize,\n      tif           = 'FOK',\n      post_only     = false,\n      builder       = { code: config.builder_code, fee_bps: 25 },\n      negrisk_aware = true\n    )\n\n  // --- 11. NegRiskAdapter conversion path ---\n  IF params.prefer_conversion_path AND adapterAvailable:\n    EMIT NegRiskConvertRoute(\n      conditionId = event.conditionId,\n      action      = 'burn_NO_to_pUSD',  // NegRiskAdapter: burn NO across set -> pUSD\n      trigger     = 'on_all_fills'\n    )\n\n  EMIT DecisionReport(\n    intent_emitted  = true,\n    edge_bps        = net_edge_bps,\n    outcome_count   = N,\n    conversion_path = 'NegRiskAdapter' IF adapterAvailable ELSE 'hold',\n    reasons         = ['NEG_RISK_SUM_ARB_EDGE_PRESENT']\n  )",
    "sdk_calls": [
      "gamma.GET('/markets?negRisk=true&active=true')",
      "onchain.NegRiskAdapter.isConditionRegistered(conditionId)",
      "ws_market.subscribe('book', tokenIds)",
      "onchain.feeConfig(conditionId)",
      "toPusdUnits(rawFloat)",
      "buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })",
      "internal.killswitch.status()",
      "internal.builder_code"
    ],
    "complexity": "O(N) per book tick where N = number of outcomes per event"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Gamma API neg-risk event with 4 outcomes",
        "source": "gamma",
        "payload": {
          "conditionId": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
          "negRisk": true,
          "outcomes": [
            {
              "name": "Outcome A",
              "yes_token_id": "0xtoken_a"
            },
            {
              "name": "Outcome B",
              "yes_token_id": "0xtoken_b"
            },
            {
              "name": "Outcome C",
              "yes_token_id": "0xtoken_c"
            },
            {
              "name": "Outcome D",
              "yes_token_id": "0xtoken_d"
            },
            {
              "name": "Other",
              "yes_token_id": "0xtoken_other"
            }
          ]
        }
      },
      {
        "label": "ws_market book tick \u2014 sum(YES) = 0.978",
        "source": "ws_market",
        "payload": {
          "token_a_ask": "0.242",
          "token_b_ask": "0.245",
          "token_c_ask": "0.248",
          "token_d_ask": "0.243",
          "sum_YES": "0.978",
          "received_at_ms": 1746789700000
        }
      }
    ],
    "output": [
      {
        "label": "OrderIntent \u2014 Outcome A leg (one of 4; all similar)",
        "payload": {
          "intent_id": "oi_01HX9NGRSUM4A1B",
          "trace_id": "tr_01HX9NGRSUM4VR5",
          "market_id": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
          "outcome": "YES",
          "side": "buy",
          "price": "0.242",
          "size_pUSD": "400.00",
          "tif": "FOK",
          "post_only": false,
          "builder": {
            "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
            "fee_bps": 25
          },
          "negrisk_aware": true,
          "decision": {
            "edge_bps": 22.1,
            "outcome_count": 4,
            "conversion_path": "NegRiskAdapter",
            "reasons": [
              "NEG_RISK_SUM_ARB_EDGE_PRESENT"
            ]
          },
          "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
        }
      },
      {
        "label": "NegRiskConvertRoute \u2014 burn NO tokens \u2192 pUSD after fills",
        "payload": {
          "route_id": "nrc_01HX9NGRSUM4X9Z",
          "conditionId": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
          "action": "burn_NO_to_pUSD",
          "trigger": "on_all_fills",
          "adapter": "NegRiskAdapter",
          "network": "polygon"
        }
      }
    ]
  },
  "reason_codes": [
    {
      "code": "NEG_RISK_SUM_ARB_EDGE_PRESENT",
      "severity": "INFO",
      "meaning": "Net edge across all N non-Other outcome YES legs meets or exceeds min_edge_bps. N OrderIntents emitted.",
      "action": "Emit N OrderIntents and optional NegRiskConvertRoute.",
      "user_message": "A pricing gap was detected across all outcomes of this market and orders were placed to capture it."
    },
    {
      "code": "NEG_RISK_SUM_ARB_NO_EDGE",
      "severity": "INFO",
      "meaning": "Net edge across outcome legs is below the 8 bps hard floor after multi-leg fees. No trade.",
      "action": "Skip; emit DecisionReport intent_emitted=false.",
      "user_message": "The combined outcome pricing was not low enough to trade profitably after multi-leg fees."
    },
    {
      "code": "NEG_RISK_SUM_ARB_EDGE_MARGINAL",
      "severity": "WARN",
      "meaning": "Edge is between 8 and 20 bps. Trade is entered at 50% leg size.",
      "action": "Emit N OrderIntents at 50% leg size; log warning.",
      "user_message": "A small pricing gap was detected across outcomes. Reduced-size orders were placed."
    },
    {
      "code": "NEG_RISK_SUM_ARB_NO_CONVERSION_PATH",
      "severity": "WARN",
      "meaning": "NegRiskAdapter is unavailable or not registered for this condition ID. Positions will be held to resolution.",
      "action": "Emit OrderIntents if edge is present; log warning; do not emit NegRiskConvertRoute.",
      "user_message": "Immediate conversion is not available for this market. Positions are held until resolution."
    },
    {
      "code": "NEG_RISK_SUM_ARB_HIGH_OUTCOME_COUNT",
      "severity": "WARN",
      "meaning": "Outcome count is between max_outcomes_per_trade warning (8) and hard limit (12).",
      "action": "Emit at 50% leg size with warning.",
      "user_message": ""
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Book snapshot older than 5 seconds or Gamma API data is stale.",
      "action": "Skip; emit DecisionReport intent_emitted=false.",
      "user_message": "Market data was too old to act on safely."
    },
    {
      "code": "MARKET_CLOSED",
      "severity": "HARD_REJECT",
      "meaning": "Event is closed or resolved.",
      "action": "Skip immediately; no OrderIntents.",
      "user_message": "This market is no longer open for trading."
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Skip all events; no OrderIntents.",
      "user_message": "Trading is currently paused."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_strat_negrisksumarb_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "verdict",
          "reason_code"
        ],
        "meaning": "Total evaluation cycles by intent_emitted (true/false) and reason code."
      },
      {
        "name": "polytraders_strat_negrisksumarb_edge_bps",
        "type": "histogram",
        "unit": "basis_points",
        "labels": [],
        "meaning": "Distribution of net edge in bps across all evaluated neg-risk events."
      },
      {
        "name": "polytraders_strat_negrisksumarb_outcome_count",
        "type": "histogram",
        "unit": "count",
        "labels": [],
        "meaning": "Distribution of outcome counts per traded event (excluding Other)."
      },
      {
        "name": "polytraders_strat_negrisksumarb_intents_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "conversion_path"
        ],
        "meaning": "Total OrderIntent sets emitted, labelled by conversion path used (NegRiskAdapter or hold)."
      },
      {
        "name": "polytraders_strat_negrisksumarb_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Wall-clock latency from book tick to last OrderIntent emit per event."
      },
      {
        "name": "polytraders_strat_negrisksumarb_conversion_route_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "result"
        ],
        "meaning": "NegRiskConvertRoute emissions by result (success, adapter_unavailable, skipped)."
      }
    ],
    "alerts": [
      {
        "name": "NegRiskSumArbStaleFeed",
        "condition": "rate(polytraders_strat_negrisksumarb_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1",
        "severity": "warn",
        "runbook": "#runbook-negrisksumarb-stale-feed"
      },
      {
        "name": "NegRiskSumArbAdapterUnavailable",
        "condition": "rate(polytraders_strat_negrisksumarb_decisions_total{reason_code='NEG_RISK_SUM_ARB_NO_CONVERSION_PATH'}[5m]) > 0.5",
        "severity": "warn",
        "runbook": "#runbook-negrisksumarb-adapter"
      },
      {
        "name": "NegRiskSumArbHighLatency",
        "condition": "histogram_quantile(0.99, rate(polytraders_strat_negrisksumarb_eval_latency_ms_bucket[5m])) > 200",
        "severity": "warn",
        "runbook": "#runbook-negrisksumarb-latency"
      },
      {
        "name": "NegRiskSumArbKillSwitchBlocking",
        "condition": "rate(polytraders_strat_negrisksumarb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0",
        "severity": "page",
        "runbook": "#runbook-killswitch"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Strategy / NegRiskSumArb edge and outcome count distribution",
      "Grafana \u2014 Strategy / NegRiskSumArb NegRiskAdapter conversion throughput"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Per-event last-seen book tick (sum_YES_asks, per-outcome ask, timestamp_ms) and NegRiskAdapter availability flag; keyed by conditionId",
    "ttl": "60s",
    "recovery": "On cold start, state is empty; first book tick per event triggers fresh evaluation. NegRiskAdapter availability is re-checked from onchain on first evaluation.",
    "size_estimate": "~500 bytes per active neg-risk event; typically < 2 MB across all monitored events"
  },
  "concurrency": {
    "execution_model": "actor-per-market",
    "max_in_flight": 30,
    "idempotency_key": "intent_id",
    "timeout_ms": 200,
    "backpressure": "drop oldest pending tick per conditionId when queue depth > 5",
    "locking": "per-conditionId mutex for Redis state write and NegRiskAdapter check"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Checked first; blocks all intent emission when active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "risk.portfolio_guard",
        "what": "N OrderIntents per event for risk guardrail evaluation before SmartRouter."
      },
      {
        "bot_id": "gov.builder_attribution",
        "what": "builder.code bytes32 on every OrderIntent for attribution tracking."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Gamma API (neg-risk event list)",
        "sla": "99.9% (Polymarket-published)",
        "fallback": "Use cached event list; if cache > 60s old, pause new event discovery."
      },
      {
        "service": "Polymarket CLOB WebSocket (ws_market)",
        "sla": "best-effort",
        "fallback": "On disconnect, fall back to REST polling; emit STALE_MARKET_DATA if gap > 5s."
      },
      {
        "service": "NegRiskAdapter (onchain, Polygon)",
        "sla": "Polygon RPC SLA",
        "fallback": "If unavailable, emit NEG_RISK_SUM_ARB_NO_CONVERSION_PATH; hold positions rather than block trade."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": true,
    "private_key_access": "signing-only",
    "abuse_vectors": [
      "Injecting a fake Gamma API response to fabricate neg-risk events with artificially low outcome prices",
      "NegRiskAdapter reentrancy on Polygon if burn-NO flow is invoked with a manipulated condition ID",
      "Partial-fill attack: adversary moves one outcome book between leg submissions to leave an unbalanced position"
    ],
    "mitigations": [
      "Gamma API response validated against onchain conditionId before any order emission",
      "NegRiskAdapter conditionId verified onchain before emitting NegRiskConvertRoute",
      "All legs use FOK; a failed leg prevents NegRiskConvertRoute from being triggered",
      "builder.code read from immutable internal config; not user-supplied",
      "V2 order timestamp(ms) invalidates replays outside exchange acceptance window"
    ],
    "contract_calls": [
      {
        "contract": "CTFExchangeV2",
        "function": "matchOrders",
        "purpose": "Settlement of YES token purchases across N outcome legs."
      },
      {
        "contract": "NegRiskAdapter",
        "function": "convertPosition",
        "purpose": "Burn NO tokens across the outcome set to receive pUSD; crystallises arb profit without waiting for event resolution."
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "STALE_GAMMA_FEED",
      "how_to_inject": "Block TCP to gamma-api.polymarket.com for 70s (cache TTL = 60s)",
      "expected_behaviour": "New event discovery pauses; existing events continue using cached data until ws_market feed also stales",
      "recovery": "Automatic on Gamma API reconnect."
    },
    {
      "scenario": "NEGRISK_ADAPTER_UNAVAILABLE",
      "how_to_inject": "Mock onchain.NegRiskAdapter.isConditionRegistered to return false",
      "expected_behaviour": "NEG_RISK_SUM_ARB_NO_CONVERSION_PATH warn; OrderIntents still emitted if edge present; NegRiskConvertRoute not emitted",
      "recovery": "Automatic when adapter check returns true again."
    },
    {
      "scenario": "PARTIAL_LEG_FILL",
      "how_to_inject": "Mock CLOB to reject one of N FOK legs after N-1 legs fill",
      "expected_behaviour": "NegRiskConvertRoute is not triggered; partial position flagged to risk pipeline; manual review required",
      "recovery": "Risk pod manually evaluates and closes partial position."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behaviour": "No OrderIntents emitted for any event",
      "recovery": "Automatic on manual KillSwitch reset."
    },
    {
      "scenario": "HIGH_OUTCOME_COUNT",
      "how_to_inject": "Create mock event with 14 outcomes",
      "expected_behaviour": "Event skipped with NEG_RISK_SUM_ARB_NO_EDGE; no OrderIntents",
      "recovery": "Automatic; event is re-evaluated each cycle."
    }
  ],
  "runbook": {
    "summary": "NegRiskSumArb incidents typically involve NegRiskAdapter unavailability, stale Gamma API data, or partial-leg fills. Partial fills require manual risk review; adapter issues are usually transient RPC problems.",
    "oncall_actions": [
      {
        "alert": "NegRiskSumArbAdapterUnavailable",
        "first_action": "Check Polygon RPC connectivity and NegRiskAdapter contract status.",
        "escalate_to": "Infra on-call if RPC is degraded; Strategy pod lead if adapter contract is paused."
      },
      {
        "alert": "NegRiskSumArbStaleFeed",
        "first_action": "Check ws_market WebSocket and Gamma API connectivity.",
        "escalate_to": "Infra on-call if disconnected > 2 minutes."
      },
      {
        "alert": "NegRiskSumArbHighLatency",
        "first_action": "Check N-leg serialisation latency; consider reducing max_outcomes_per_trade.",
        "escalate_to": "Strategy pod lead if p99 > 400ms sustained."
      },
      {
        "alert": "NegRiskSumArbKillSwitchBlocking",
        "first_action": "Confirm KillSwitch activation was intentional.",
        "escalate_to": "Risk pod lead immediately."
      }
    ],
    "manual_overrides": [
      {
        "name": "force_skip_event",
        "how": "Add conditionId to config.excluded_conditions list",
        "when": "Event shows anomalous pricing or NegRiskAdapter is experiencing issues for a specific condition."
      },
      {
        "name": "pause_bot",
        "how": "polytraders bot pause strat.neg_risk_sum_arb",
        "when": "Elevated partial-fill rate or NegRiskAdapter instability."
      }
    ],
    "healthcheck": "GET /internal/health/neg-risk-sum-arb -> 200 if Gamma API last_seen < 60s, ws_market feed last_seen < 5s per active event, NegRiskAdapter reachable, and KillSwitch inactive."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "All unit tests pass including 'Other' exclusion invariant",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "NegRiskAdapter conditionId validation test passes",
        "how_measured": "Integration test against Polygon testnet",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "p99 eval latency < 200ms over 24h for events with N \u2264 8 outcomes",
        "how_measured": "polytraders_strat_negrisksumarb_eval_latency_ms histogram",
        "threshold": "p99 < 200ms"
      },
      {
        "gate": "Zero partial-fill incidents in shadow mode over 48h",
        "how_measured": "Fill reconciliation report",
        "threshold": "0 incidents"
      }
    ],
    "to_general_live": [
      {
        "gate": "E2E: Gamma event \u2192 N signed V2 OrderIntents \u2192 NegRiskConvertRoute on Polygon testnet",
        "how_measured": "E2E test",
        "threshold": "Pass"
      },
      {
        "gate": "feeRateBps absence verified in all N OrderIntent signed payloads",
        "how_measured": "Integration test asserting V2 order schema",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "DecisionReport"
    ],
    "topics": [
      "polytraders.reports.strategy"
    ],
    "cadence": "every-event",
    "retention_class": "2y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": []
  },
  "capital_impact": "Direct",
  "v3_status": {
    "phase": 8,
    "phase_name": "Additional strategies",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}