{
  "schema_version": "1.0.0",
  "bot_id": "3.11",
  "bot_name": "Late-Resolution Spread",
  "slug": "late-resolution-spread",
  "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": "Markets within max_minutes_to_resolution of their gamma.market.endDate, where price is \u2265 min_price_cents/100 and spread to $1.00 exceeds min_spread_to_1_cents",
    "default_mode": "general_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Strategy pod"
  },
  "purpose": "Late-Resolution Spread captures the time-decay component of high-probability outcomes near market maturity. As a binary or multi-outcome market approaches its endDate (from Gamma API), a token priced at $0.95\u2013$0.99 retains a positive expected settlement of $1.00 if the leading outcome holds. The spread between the current market price and $1.00 shrinks deterministically as resolution approaches. This bot monitors gamma.market.endDate to identify markets within max_minutes_to_resolution of close, checks that the price-to-$1.00 spread exceeds the configurable minimum, and emits a buy OrderIntent sized to max_clip_usd. It never averages down (never_average_down is locked true). This is a user-controlled execution tool exploiting time-value decay in near-resolution predictive markets; it is not a directional price forecast.",
  "why_it_matters": [
    {
      "failure": "endDate not fetched from Gamma API; using stale time-to-resolution",
      "consequence": "Entering a late-resolution trade with incorrect time remaining may expose the position to a full overnight resolution cycle, dramatically extending holding time and risk."
    },
    {
      "failure": "Oracle challenge window not respected",
      "consequence": "UMA Optimistic Oracle: $750 pUSD bond, 2-hour challenge window, potential 24\u201348h DVM delay. Entering a late-resolution trade when a challenge is active means the position may not settle at $1.00 within the expected window."
    },
    {
      "failure": "feeRateBps hardcoded on signed order (V1 pattern)",
      "consequence": "CLOB V2 rejects orders containing feeRateBps. Fees are operator-set at match time. The signed order must not contain this field."
    },
    {
      "failure": "never_average_down not enforced",
      "consequence": "If price drops after entry on a late-resolution market, averaging down concentrates risk just before resolution \u2014 the opposite of the strategy intent."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market endDate and resolution metadata",
      "source": "gamma (Gamma API \u2014 gamma.market.endDate, resolutionSource, negRisk flag)",
      "required": true,
      "use": "Primary time-to-resolution signal. Compute minutes_to_resolution = (endDate - now) / 60. Only consider markets within max_minutes_to_resolution."
    },
    {
      "input": "Current best ask for the leading outcome",
      "source": "clob_public",
      "required": true,
      "use": "Confirm price \u2265 min_price_cents/100 and compute spread = 1.00 - best_ask."
    },
    {
      "input": "Oracle proposal status and challenge window state",
      "source": "onchain (UMA Optimistic Oracle / Polygon)",
      "required": true,
      "use": "Skip markets with an active oracle challenge (2h window + potential DVM delay 24\u201348h) to avoid unexpected settlement delays."
    },
    {
      "input": "Market negRisk flag and multi-outcome structure",
      "source": "gamma",
      "required": true,
      "use": "For neg-risk events, verify each outcome's endDate independently and check NegRiskAdapter availability for position exit."
    },
    {
      "input": "Top-of-book depth for the leading outcome",
      "source": "clob_public",
      "required": true,
      "use": "Size order to min(depth_available, max_clip_usd) to avoid moving the book."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Abort 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."
    },
    {
      "input": "OracleRiskMonitor approval status for this market",
      "source": "OracleRiskMonitor",
      "required": true,
      "use": "Only enter when OracleRiskMonitor confirms no active challenge or DVM escalation."
    }
  ],
  "raw_params": [
    "min_spread_to_1_cents \u00b7 int",
    "max_minutes_to_resolution \u00b7 int",
    "max_clip_usd \u00b7 int",
    "never_average_down \u00b7 bool (locked true)"
  ],
  "parameters": [
    {
      "name": "min_spread_to_1_cents",
      "default": 2,
      "warning": 3,
      "hard": 1,
      "controls": "Minimum spread (in cents) between the current best ask and $1.00 required for entry. A spread of 2 cents means price \u2264 $0.98.",
      "why_default_matters": "2 cents (200 bps below $1) provides enough room to cover taker fees and slippage on a near-resolution trade. Below 1 cent (hard floor), the remaining upside is wholly consumed by fees.",
      "threshold_logic": [
        {
          "condition": "spread \u2265 2 cents",
          "action": "EMIT OrderIntent"
        },
        {
          "condition": "1\u20132 cents",
          "action": "WARN LATE_RES_SPREAD_TOO_TIGHT; skip (spread too narrow for fees)"
        },
        {
          "condition": "< 1 cent (hard floor)",
          "action": "SKIP \u2014 LATE_RES_SPREAD_TOO_TIGHT"
        }
      ],
      "dev_check": "spread_cents = (1.00 - best_ask) * 100; if spread_cents < params.hard: return skip('LATE_RES_SPREAD_TOO_TIGHT')",
      "user_facing": "The market price is already very close to $1. There is not enough room to enter profitably after fees."
    },
    {
      "name": "max_minutes_to_resolution",
      "default": 120,
      "warning": 30,
      "hard": 360,
      "controls": "Maximum time to resolution (in minutes, from gamma.market.endDate) at which the bot will consider entering. Markets further from resolution are not eligible.",
      "why_default_matters": "120 minutes captures the late-resolution window without entering so early that normal price volatility erodes the spread before settlement. The 360-minute hard ceiling prevents very early entry.",
      "threshold_logic": [
        {
          "condition": "minutes_to_resolution \u2264 120",
          "action": "Eligible to enter"
        },
        {
          "condition": "30\u201360 minutes",
          "action": "WARN LATE_RES_APPROACHING; consider reducing clip size due to lower liquidity"
        },
        {
          "condition": "> 360 minutes",
          "action": "SKIP \u2014 not in late-resolution window"
        }
      ],
      "dev_check": "minsLeft = (endDate - now_ms()) / 60000; if minsLeft > params.hard: return skip('LATE_RES_NOT_IN_WINDOW')",
      "user_facing": "This market is not close enough to its resolution time for this strategy to apply."
    },
    {
      "name": "max_clip_usd",
      "default": 300,
      "warning": 500,
      "hard": 750,
      "controls": "Maximum pUSD size per order clip. Late-resolution markets are often illiquid; small clips prevent moving the book.",
      "why_default_matters": "300 pUSD is modest enough for typical near-resolution book depth. Larger clips risk triggering order-book impact that lifts the price above the entry threshold before the order fills.",
      "threshold_logic": [
        {
          "condition": "\u2264 300 pUSD",
          "action": "Normal clip"
        },
        {
          "condition": "300\u2013750 pUSD",
          "action": "WARN; Risk guardrail will reshape if above portfolio budget"
        },
        {
          "condition": "> 750 pUSD",
          "action": "Reject config change \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if params.max_clip_usd > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "The order was sized to avoid having a large impact on this market's thin near-resolution book."
    },
    {
      "name": "never_average_down",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "Locked to true. If a position already exists on this market and the current price is below the entry price, no additional buy OrderIntent is emitted.",
      "why_default_matters": "Averaging down on a near-resolution market concentrates risk at exactly the wrong time. If the leading outcome reverses near maturity, a doubled position doubles the loss.",
      "threshold_logic": [
        {
          "condition": "Always true (locked)",
          "action": "No new buy intent if current_price < entry_price for existing position"
        }
      ],
      "dev_check": "if position.exists and current_price < position.entry_price: return skip('LATE_RES_NO_AVERAGE_DOWN')",
      "user_facing": "This strategy does not add to a losing position near market resolution."
    }
  ],
  "default_config": {
    "bot_id": "strat.late_resolution_spread",
    "version": "2.1.0",
    "mode": "general_live",
    "defaults": {
      "min_spread_to_1_cents": 2,
      "max_minutes_to_resolution": 120,
      "max_clip_usd": 300,
      "never_average_down": true
    },
    "locked": {
      "never_average_down": true,
      "max_clip_usd": {
        "max": 750
      },
      "max_minutes_to_resolution": {
        "max": 360
      }
    }
  },
  "implementation_flow": [
    "Check KillSwitch active flag; if active, emit no OrderIntents.",
    "Poll Gamma API for markets with endDate within (now, now + max_minutes_to_resolution * 60).",
    "For each candidate market: compute minutes_to_resolution = (endDate - now_ms()) / 60000.",
    "If minutes_to_resolution > max_minutes_to_resolution hard (360 min), skip market.",
    "Fetch best_ask for the leading YES outcome from clob_public.",
    "Confirm best_ask \u2265 0.90 (minimum viable price \u2014 not a penny token). Skip if below.",
    "Compute spread_cents = (1.00 - best_ask) * 100. If spread_cents < min_spread_to_1_cents hard floor (1), skip LATE_RES_SPREAD_TOO_TIGHT.",
    "Check OracleRiskMonitor status for this market. If oracle challenge active or DVM escalation in progress, skip LATE_RES_ORACLE_CHALLENGE_ACTIVE.",
    "Confirm negRisk flag from Gamma. For neg-risk markets, also check NegRiskAdapter availability for clean exit.",
    "Check existing position: if position exists and current_price < position.entry_price, skip LATE_RES_NO_AVERAGE_DOWN (never_average_down locked true).",
    "Compute clipSize = toPusdUnits(min(depth_available, max_clip_usd)).",
    "If minutes_to_resolution < 30 (warning), reduce clipSize by 20% for thin-book safety.",
    "Emit OrderIntent: market_id, outcome=YES, side=buy, price=best_ask, size_pUSD=clipSize, tif=GTC, post_only=false, builder={code, fee_bps: 25}.",
    "Note: fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order.",
    "Emit DecisionReport with intent_emitted=true, spread_cents, minutes_to_resolution, oracle_clear=true."
  ],
  "decision_logic": {
    "approve": "minutes_to_resolution \u2264 max_minutes_to_resolution, spread_cents \u2265 min_spread_to_1_cents, oracle clear (no active challenge), no existing position below entry (never_average_down), KillSwitch inactive. Emit buy OrderIntent.",
    "reshape_required": "Not applicable \u2014 strat bots emit OrderIntents; reshaping is handled downstream by the Risk guardrail pipeline.",
    "reject": "Market outside resolution window; spread < 1 cent hard floor; oracle challenge active; would average down into losing position; KillSwitch active; stale Gamma data. Emit DecisionReport intent_emitted=false.",
    "warning_only": "minutes_to_resolution < 30 (approaching) or spread between 1\u20132 cents triggers warning and size reduction."
  },
  "decision_output_schema": "OrderIntent",
  "decision_output_example": {
    "intent_id": "oi_01HX9LRSP3C1A1B",
    "trace_id": "tr_01HX9LRSP3C1VR5",
    "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
    "outcome": "YES",
    "side": "buy",
    "price": "0.976",
    "size_pUSD": "300.00",
    "tif": "GTC",
    "post_only": false,
    "builder": {
      "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
      "fee_bps": 25
    },
    "negrisk_aware": true,
    "decision": {
      "edge_bps": 14.0,
      "spread_cents": 2.4,
      "minutes_to_resolution": 87,
      "oracle_clear": true,
      "reasons": [
        "LATE_RES_SPREAD_ENTRY"
      ]
    },
    "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
  },
  "developer_log": {
    "bot_id": "strat.late_resolution_spread",
    "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
    "end_date": "2026-05-09T13:00:00Z",
    "minutes_to_resolution": 87.3,
    "best_ask": 0.976,
    "spread_cents": 2.4,
    "neg_risk": true,
    "oracle_challenge_active": false,
    "clip_size_pusd": 300.0,
    "intent_emitted": true,
    "reason": "LATE_RES_SPREAD_ENTRY",
    "emitted_at_ms": 1746789900000
  },
  "user_explanations": [
    {
      "situation": "Late-resolution trade entered",
      "message": "This market is close to its resolution time and the leading outcome is priced below $1. An order was placed to buy that outcome at a small discount, expecting it to settle at $1."
    },
    {
      "situation": "No entry \u2014 spread too small",
      "message": "The market price is already very close to $1. After fees, there is not enough potential profit to justify the trade."
    },
    {
      "situation": "No entry \u2014 oracle challenge active",
      "message": "The market's resolution is being disputed. The trade was skipped to avoid exposure during the uncertainty window."
    },
    {
      "situation": "No entry \u2014 market not near resolution",
      "message": "This market is not close enough to its scheduled resolution time for this strategy. It will be re-evaluated as the resolution time approaches."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Entering a near-resolution trade on a market where the oracle challenge window is active or the leading outcome reverses, turning a $0.95\u2013$0.99 buy into a $0.00 settlement loss.",
    "false_positive_risk": "Gamma API endDate is stale or incorrect; the bot enters a trade believing resolution is imminent when the market has actually been extended or postponed.",
    "false_negative_risk": "OracleRiskMonitor check has high latency; a valid late-resolution opportunity is skipped because the oracle status read times out, erroneously reporting a challenge as active.",
    "safe_fallback": "If Gamma API endDate is stale (> 60s), or OracleRiskMonitor status cannot be confirmed, skip and emit DecisionReport intent_emitted=false. Never enter without a confirmed endDate and a clean oracle status.",
    "required_dependencies": [
      "Gamma API market.endDate (fresh < 60s)",
      "clob_public best ask and depth",
      "onchain OracleRiskMonitor / UMA status",
      "OracleRiskMonitor internal signal",
      "KillSwitch active flag",
      "internal builder code"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Emit OrderIntent when spread=2.4 cents, minutes_to_resolution=87, oracle clear",
        "setup": "best_ask=0.976, endDate=now+87min, oracle_challenge=false",
        "expected": "One OrderIntent emitted; intent_emitted=true; spread_cents=2.4"
      },
      {
        "test": "Skip when spread = 0.8 cents (below 1 cent hard floor)",
        "setup": "best_ask=0.992",
        "expected": "No OrderIntent; reason=LATE_RES_SPREAD_TOO_TIGHT"
      },
      {
        "test": "Skip when minutes_to_resolution = 400 (above 360 min hard ceiling)",
        "setup": "endDate=now+400min",
        "expected": "No OrderIntent; reason=LATE_RES_NOT_IN_WINDOW"
      },
      {
        "test": "Skip when oracle challenge is active",
        "setup": "oracle_challenge_active=true",
        "expected": "No OrderIntent; reason=LATE_RES_ORACLE_CHALLENGE_ACTIVE"
      },
      {
        "test": "Skip when existing position is below entry price (never_average_down)",
        "setup": "position.entry_price=0.980, current_price=0.972",
        "expected": "No OrderIntent; reason=LATE_RES_NO_AVERAGE_DOWN"
      },
      {
        "test": "Reduce clip size by 20% when minutes_to_resolution < 30",
        "setup": "minutes_to_resolution=22, max_clip_usd=300",
        "expected": "OrderIntent emitted with size=240; WARN LATE_RES_APPROACHING"
      }
    ],
    "integration": [
      {
        "test": "Full cycle: Gamma poll \u2192 endDate check \u2192 oracle clear \u2192 signed V2 OrderIntent submitted",
        "expected": "OrderIntent contains builder.code (bytes32), no feeRateBps, EIP-712 domain version '2', negrisk_aware=true for neg-risk markets"
      },
      {
        "test": "Stale Gamma endDate triggers skip without order emission",
        "expected": "DecisionReport intent_emitted=false, reason=STALE_MARKET_DATA when Gamma fetch is > 60s old"
      }
    ],
    "property": [
      {
        "property": "Bot never emits a buy OrderIntent when existing position's current_price < entry_price",
        "required": "Always true \u2014 never_average_down locked"
      },
      {
        "property": "feeRateBps field is never present on any emitted OrderIntent",
        "required": "Always true \u2014 V2 fees are operator-set at match time"
      },
      {
        "property": "OracleRiskMonitor must be clear before any OrderIntent is emitted",
        "required": "Always true \u2014 fail-closed on oracle state"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Buy \u00a295\u2013\u00a299 shares minutes before resolution for the small but reliable spread to $1.",
  "legacy_pm_signals": [
    "Market price + spread to $1",
    "Time-to-resolution countdown",
    "OracleRiskMonitor approval"
  ],
  "legacy_external_feeds": [
    "NewsIngest: multiple independent sources confirm winner"
  ],
  "reporting_groups": [
    "strategy_decision"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "clob_public",
    "clob_auth",
    "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)",
      "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 OrderIntent. EIP-712 Exchange domain version updated from '1' to '2'. Gamma API endDate field confirmed as primary time-to-resolution source. OracleRiskMonitor integration updated to V2 UMA bond/challenge parameters ($750 pUSD, 2h challenge window)."
    }
  ],
  "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": "Heavily time-dependent on gamma.market.endDate; Gamma API freshness is a hard dependency. For neg-risk multi-outcome events, checks NegRiskAdapter availability for post-fill conversion. UMA oracle challenge window ($750 pUSD bond, 2h challenge, 24\u201348h DVM if escalated) is respected via OracleRiskMonitor. feeRateBps not on any signed order."
  },
  "reference_implementation": {
    "summary": "Polls Gamma API for near-resolution markets, validates oracle status and spread, and emits a buy OrderIntent when price-to-$1 spread is sufficient and oracle is clear.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION scanLateResolutionMarkets():\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    RETURN\n\n  // --- 1. Poll Gamma API for near-resolution markets ---\n  now_ms = now_ms()\n  windowEnd = now_ms + params.max_minutes_to_resolution * 60 * 1000\n  markets = FETCH gamma.GET('/markets?endDateFrom=' + now_ms + '&endDateTo=' + windowEnd + '&active=true')\n  IF isStale(markets, maxAgeS=60):\n    EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')\n    RETURN\n\n  FOR market IN markets:\n    minutesLeft = (market.endDate - now_ms) / 60000\n    IF minutesLeft > params.max_minutes_to_resolution_hard:  // 360\n      CONTINUE\n\n    // --- 2. Fetch best ask ---\n    book = FETCH clob_public.GET('/book?market=' + market.conditionId)\n    bestAsk = book.asks[0].price\n    IF bestAsk < 0.90:  // minimum viable price\n      CONTINUE\n\n    // --- 3. Spread check ---\n    spreadCents = (1.00 - bestAsk) * 100\n    IF spreadCents < params.min_spread_to_1_cents_hard:  // 1 cent\n      CONTINUE  // spread too tight\n\n    // --- 4. Oracle check (UMA: $750 pUSD bond, 2h challenge, 24-48h DVM) ---\n    oracleStatus = FETCH internal.oracleRiskMonitor.status(market.conditionId)\n    IF oracleStatus.challenge_active OR oracleStatus.dvm_escalated:\n      EMIT DecisionReport(intent_emitted=false, reason='LATE_RES_ORACLE_CHALLENGE_ACTIVE')\n      CONTINUE\n\n    // --- 5. NegRisk check ---\n    IF market.negRisk:\n      adapterAvailable = FETCH onchain.NegRiskAdapter.isConditionRegistered(market.conditionId)\n      // negRisk_aware=true on intent; adapter path available for exit\n\n    // --- 6. Never-average-down (locked true) ---\n    position = FETCH clob_auth.GET('/positions?market=' + market.conditionId)\n    IF position.exists AND bestAsk < position.entry_price:\n      EMIT DecisionReport(intent_emitted=false, reason='LATE_RES_NO_AVERAGE_DOWN')\n      CONTINUE\n\n    // --- 7. Sizing ---\n    depth = book.asks[0].size_pusd\n    clipSize = toPusdUnits(min(depth, params.max_clip_usd))\n    IF minutesLeft < 30:\n      WARN('LATE_RES_APPROACHING')\n      clipSize = toPusdUnits(clipSize * 0.8)  // thin book near close\n\n    // --- 8. Emit OrderIntent (V2: no feeRateBps; builder field; taker order) ---\n    EMIT OrderIntent(\n      market_id     = market.conditionId,\n      outcome       = 'YES',\n      side          = 'buy',\n      price         = bestAsk,\n      size_pUSD     = clipSize,\n      tif           = 'GTC',\n      post_only     = false,\n      builder       = { code: config.builder_code, fee_bps: 25 },\n      negrisk_aware = market.negRisk\n    )\n    EMIT DecisionReport(\n      intent_emitted       = true,\n      spread_cents         = spreadCents,\n      minutes_to_resolution = minutesLeft,\n      oracle_clear         = true,\n      reasons              = ['LATE_RES_SPREAD_ENTRY']\n    )",
    "sdk_calls": [
      "gamma.GET('/markets?endDateFrom=...&endDateTo=...&active=true')",
      "fetchClobPublic('/book?market=' + conditionId)",
      "internal.oracleRiskMonitor.status(conditionId)",
      "clob_auth.GET('/positions?market=' + conditionId)",
      "onchain.NegRiskAdapter.isConditionRegistered(conditionId)",
      "toPusdUnits(rawFloat)",
      "buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })",
      "internal.killswitch.status()",
      "internal.builder_code"
    ],
    "complexity": "O(M) per poll cycle where M = number of near-resolution markets"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Gamma API near-resolution market",
        "source": "gamma",
        "payload": {
          "conditionId": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
          "endDate": "2026-05-09T13:00:00Z",
          "negRisk": true,
          "active": true,
          "resolutionSource": "UMA Optimistic Oracle"
        }
      },
      {
        "label": "CLOB public book \u2014 leading outcome",
        "source": "clob_public",
        "payload": {
          "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
          "best_ask": "0.976",
          "depth_pusd": "420.00",
          "spread_cents": "2.4",
          "fetched_at_ms": 1746789900000
        }
      }
    ],
    "output": [
      {
        "label": "OrderIntent \u2014 late-resolution buy (GTC, taker, builder-attributed)",
        "payload": {
          "intent_id": "oi_01HX9LRSP3C1A1B",
          "trace_id": "tr_01HX9LRSP3C1VR5",
          "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
          "outcome": "YES",
          "side": "buy",
          "price": "0.976",
          "size_pUSD": "300.00",
          "tif": "GTC",
          "post_only": false,
          "builder": {
            "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
            "fee_bps": 25
          },
          "negrisk_aware": true,
          "decision": {
            "edge_bps": 14.0,
            "spread_cents": 2.4,
            "minutes_to_resolution": 87,
            "oracle_clear": true,
            "reasons": [
              "LATE_RES_SPREAD_ENTRY"
            ]
          },
          "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
        }
      }
    ]
  },
  "reason_codes": [
    {
      "code": "LATE_RES_SPREAD_ENTRY",
      "severity": "INFO",
      "meaning": "Market is within max_minutes_to_resolution, spread \u2265 min_spread_to_1_cents, oracle clear, no averaging down. OrderIntent emitted.",
      "action": "Emit buy OrderIntent.",
      "user_message": "A near-resolution trade was placed based on a pricing gap expected to close at settlement."
    },
    {
      "code": "LATE_RES_SPREAD_TOO_TIGHT",
      "severity": "INFO",
      "meaning": "spread_cents < min_spread_to_1_cents hard floor. Price is too close to $1.00 to trade profitably after fees.",
      "action": "Skip; emit DecisionReport intent_emitted=false.",
      "user_message": "The market price is too close to $1 to enter profitably."
    },
    {
      "code": "LATE_RES_NOT_IN_WINDOW",
      "severity": "INFO",
      "meaning": "minutes_to_resolution > max_minutes_to_resolution. Market is not yet in the late-resolution window.",
      "action": "Skip; re-evaluate on next poll cycle.",
      "user_message": "This market is not close enough to its resolution time."
    },
    {
      "code": "LATE_RES_ORACLE_CHALLENGE_ACTIVE",
      "severity": "WARN",
      "meaning": "UMA Optimistic Oracle challenge is active for this market (2h window) or DVM escalation is in progress (24\u201348h).",
      "action": "Skip; emit DecisionReport intent_emitted=false; re-evaluate when oracle is clear.",
      "user_message": "This market's resolution is being disputed. The trade was skipped."
    },
    {
      "code": "LATE_RES_NO_AVERAGE_DOWN",
      "severity": "INFO",
      "meaning": "Existing position price is above current best ask. Never-average-down rule prevents adding to the position.",
      "action": "Skip; emit DecisionReport intent_emitted=false.",
      "user_message": "An existing position is already open at a higher price. No additional order was placed."
    },
    {
      "code": "LATE_RES_APPROACHING",
      "severity": "WARN",
      "meaning": "minutes_to_resolution < 30. Book may be thin; clip size reduced by 20%.",
      "action": "Emit OrderIntent at 80% clip size; log warning.",
      "user_message": "Resolution is imminent. Order size was reduced to account for lower market liquidity."
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Gamma API endDate data is > 60s old, or CLOB book snapshot is > 5s old.",
      "action": "Skip; no OrderIntent emitted.",
      "user_message": "Market data was too old to act on safely."
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Skip all markets; no OrderIntents.",
      "user_message": "Trading is currently paused."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_strat_lateresspread_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_lateresspread_spread_cents",
        "type": "histogram",
        "unit": "cents",
        "labels": [],
        "meaning": "Distribution of spread (in cents below $1.00) at evaluation time, for entered and skipped opportunities."
      },
      {
        "name": "polytraders_strat_lateresspread_minutes_to_resolution",
        "type": "histogram",
        "unit": "minutes",
        "labels": [],
        "meaning": "Distribution of minutes-to-resolution at entry time."
      },
      {
        "name": "polytraders_strat_lateresspread_intents_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "negrisk_aware"
        ],
        "meaning": "Total OrderIntents emitted, split by neg-risk/standard market type."
      },
      {
        "name": "polytraders_strat_lateresspread_oracle_skips_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "market_id"
        ],
        "meaning": "Number of entry opportunities skipped due to oracle challenge or DVM escalation."
      },
      {
        "name": "polytraders_strat_lateresspread_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Wall-clock latency from Gamma poll to OrderIntent emit."
      }
    ],
    "alerts": [
      {
        "name": "LateResSpreadGammaStale",
        "condition": "rate(polytraders_strat_lateresspread_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1",
        "severity": "warn",
        "runbook": "#runbook-lateresponse-gamma-stale"
      },
      {
        "name": "LateResSpreadOracleSkipsHigh",
        "condition": "rate(polytraders_strat_lateresspread_oracle_skips_total[10m]) > 3",
        "severity": "warn",
        "runbook": "#runbook-lateresponse-oracle-skips"
      },
      {
        "name": "LateResSpreadHighLatency",
        "condition": "histogram_quantile(0.99, rate(polytraders_strat_lateresspread_eval_latency_ms_bucket[5m])) > 250",
        "severity": "warn",
        "runbook": "#runbook-lateresponse-latency"
      },
      {
        "name": "LateResSpreadKillSwitchBlocking",
        "condition": "rate(polytraders_strat_lateresspread_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0",
        "severity": "page",
        "runbook": "#runbook-killswitch"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Strategy / LateResSpread time-to-resolution and spread distribution",
      "Grafana \u2014 Strategy / LateResSpread oracle skip rate and entry throughput"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Per-market entry price (for never_average_down check), last oracle status, and last Gamma endDate; keyed by conditionId",
    "ttl": "12h for entry price state; 60s for oracle status cache; 60s for endDate cache",
    "recovery": "On cold start, entry prices are re-read from clob_auth positions. Oracle status is re-fetched on first evaluation per market. Gamma endDate is re-polled from Gamma API.",
    "size_estimate": "~200 bytes per active near-resolution market; typically < 1 MB total"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 20,
    "idempotency_key": "intent_id",
    "timeout_ms": 250,
    "backpressure": "drop duplicate poll results for same conditionId if queue depth > 3",
    "locking": "per-conditionId mutex for entry price and oracle state read/write"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Checked first; blocks all intent emission when active."
      },
      {
        "bot_id": "risk.oracle_risk_monitor",
        "why": "OracleRiskMonitor must confirm oracle is clear before any OrderIntent is emitted. Active challenge or DVM escalation blocks entry."
      }
    ],
    "emits_to": [
      {
        "bot_id": "risk.portfolio_guard",
        "what": "Buy OrderIntents 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 (market.endDate + negRisk flag)",
        "sla": "99.9% (Polymarket-published)",
        "fallback": "If Gamma API is stale (> 60s), skip all evaluations and emit STALE_MARKET_DATA."
      },
      {
        "service": "Polymarket CLOB (public, book + depth)",
        "sla": "99.9%",
        "fallback": "If CLOB book is stale (> 5s), skip evaluation."
      },
      {
        "service": "UMA Optimistic Oracle (onchain, Polygon)",
        "sla": "Polygon RPC SLA",
        "fallback": "If oracle status cannot be confirmed, skip and emit LATE_RES_ORACLE_CHALLENGE_ACTIVE (fail-closed)."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": true,
    "private_key_access": "signing-only",
    "abuse_vectors": [
      "Gamma API endDate manipulation: a falsified endDate could push a market artificially into the near-resolution window",
      "Oracle status spoofing: a false 'oracle clear' signal could cause entry during an active dispute",
      "Average-down bypass: a crafted position read could make entry_price appear lower than current price, bypassing the never_average_down check"
    ],
    "mitigations": [
      "Gamma endDate is cross-validated against clob_public market.closeTime before use",
      "Oracle status read is from OracleRiskMonitor (authenticated internal service), not from public feed",
      "Position entry_price is read from clob_auth (authenticated); not from public feed",
      "V2 order timestamp(ms) prevents replay of signed orders",
      "Fail-closed on oracle state: any read error treats oracle as challenged"
    ],
    "contract_calls": [
      {
        "contract": "CTFExchangeV2",
        "function": "matchOrders",
        "purpose": "Settlement of YES token purchase at near-resolution price; position settles to $1.00 pUSD on correct outcome resolution."
      },
      {
        "contract": "NegRiskAdapter",
        "function": "convertPosition",
        "purpose": "For neg-risk markets: optional post-fill conversion path to pUSD via NegRiskAdapter rather than waiting for resolution."
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "STALE_GAMMA_ENDPOINT",
      "how_to_inject": "Block TCP to gamma-api.polymarket.com for 65s (cache TTL = 60s)",
      "expected_behaviour": "All evaluations skipped with STALE_MARKET_DATA; no OrderIntents",
      "recovery": "Automatic on Gamma API reconnect."
    },
    {
      "scenario": "ORACLE_CHALLENGE_ACTIVE",
      "how_to_inject": "Set mock OracleRiskMonitor.challenge_active=true for a target conditionId",
      "expected_behaviour": "LATE_RES_ORACLE_CHALLENGE_ACTIVE; entry skipped for that market until challenge resolves",
      "recovery": "Automatic when OracleRiskMonitor clears the challenge."
    },
    {
      "scenario": "AVERAGE_DOWN_BLOCK",
      "how_to_inject": "Set position.entry_price=0.985; current best_ask=0.972",
      "expected_behaviour": "LATE_RES_NO_AVERAGE_DOWN; no OrderIntent",
      "recovery": "Automatic when price recovers above entry_price."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behaviour": "No OrderIntents for any market",
      "recovery": "Automatic on manual KillSwitch reset."
    },
    {
      "scenario": "END_DATE_FAR",
      "how_to_inject": "Set mock market.endDate to now + 500 minutes",
      "expected_behaviour": "LATE_RES_NOT_IN_WINDOW; market skipped",
      "recovery": "Automatic when endDate enters window."
    }
  ],
  "runbook": {
    "summary": "LateResSpread incidents involve Gamma API staleness (most common), oracle challenge-related skips, or occasional stale-endDate entries. Gamma staleness is resolved via infra; oracle challenges require monitoring until the UMA 2h window clears.",
    "oncall_actions": [
      {
        "alert": "LateResSpreadGammaStale",
        "first_action": "Check Gamma API connectivity at gamma-api.polymarket.com.",
        "escalate_to": "Infra on-call if Gamma API is down > 5 minutes."
      },
      {
        "alert": "LateResSpreadOracleSkipsHigh",
        "first_action": "Review which markets are being skipped and check UMA oracle challenge status on Polygon.",
        "escalate_to": "Risk pod lead if multiple high-value markets have active challenges simultaneously."
      },
      {
        "alert": "LateResSpreadHighLatency",
        "first_action": "Check Gamma poll cycle latency; reduce polling frequency if necessary.",
        "escalate_to": "Strategy pod lead if p99 > 500ms sustained."
      },
      {
        "alert": "LateResSpreadKillSwitchBlocking",
        "first_action": "Confirm KillSwitch activation was intentional.",
        "escalate_to": "Risk pod lead immediately."
      }
    ],
    "manual_overrides": [
      {
        "name": "force_skip_market",
        "how": "Add conditionId to config.excluded_conditions",
        "when": "Market has anomalous oracle activity or endDate has been postponed by the event organiser."
      },
      {
        "name": "pause_bot",
        "how": "polytraders bot pause strat.late_resolution_spread",
        "when": "Elevated oracle challenge rate or Gamma API instability."
      }
    ],
    "healthcheck": "GET /internal/health/late-resolution-spread -> 200 if Gamma API last_seen < 60s, OracleRiskMonitor reachable, KillSwitch inactive, and at least one market evaluated in last 5 minutes."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "All unit tests pass including never_average_down invariant and oracle-clear gate",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "feeRateBps absence verified in integration test signed-order payload",
        "how_measured": "Integration test asserting V2 order schema",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "p99 eval latency < 250ms over 24h",
        "how_measured": "polytraders_strat_lateresspread_eval_latency_ms histogram",
        "threshold": "p99 < 250ms"
      },
      {
        "gate": "Zero average-down incidents in 48h shadow run",
        "how_measured": "never_average_down invariant monitoring",
        "threshold": "0 incidents"
      }
    ],
    "to_general_live": [
      {
        "gate": "E2E: Gamma endDate \u2192 oracle clear \u2192 signed V2 OrderIntent \u2192 fill on Polygon testnet",
        "how_measured": "E2E test",
        "threshold": "Pass"
      },
      {
        "gate": "Oracle challenge simulation: entry blocked during 2h challenge window",
        "how_measured": "Failure injection test with mock OracleRiskMonitor",
        "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"
  }
}