{
  "schema_version": "1.0.0",
  "bot_id": "3.8",
  "bot_name": "News Materiality Trader",
  "slug": "news-materiality-trader",
  "layer": "Strategy",
  "layer_key": "strat",
  "bot_class": "Alpha Strategy",
  "authority": [
    "Trade"
  ],
  "status": "beta",
  "readiness": "Limited 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": "NewsIngest materiality scorer + entity-resolution",
    "applies_to": "Standard binary markets where a resolved entity match triggers a NLP materiality score >= materiality_threshold on incoming Reuters, AP, Bloomberg, or league wire events",
    "default_mode": "limited_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Strategy pod"
  },
  "purpose": "News Materiality Trader positions ahead of the book's full digestion of material news events. It receives news items from the NewsIngest pipeline (Reuters, AP, Bloomberg, league wires), scores them using a NLP materiality classifier, resolves the news entity to one or more watchlisted Polymarket markets via an entity-resolution dictionary, and emits an IOC OrderIntent when materiality_score >= materiality_threshold. A cooldown period (cooldown_s) prevents re-trading the same entity-market pair within the window. Order TTL (order_ttl_s) limits exposure if the IOC is not immediately filled. This is a user-controlled execution tool for reacting to material news before book equilibration; it does not predict resolution outcomes or make performance claims.",
  "why_it_matters": [
    {
      "failure": "Low-materiality news classified as high",
      "consequence": "The bot takes a position based on a rumour or corrected story, entering before the market corrects back, resulting in a loss when the initial price move reverses."
    },
    {
      "failure": "Entity resolution maps to wrong market",
      "consequence": "A news event about 'Team X wins championship' is resolved to a market about a different competition, causing a trade on an unrelated market."
    },
    {
      "failure": "Book depth insufficient for the news event",
      "consequence": "Filling the full IOC at the entry price depletes the top-of-book, causing significant slippage that overwhelms the news edge."
    },
    {
      "failure": "cooldown_s too short \u2014 re-trading the same event",
      "consequence": "Multiple news items from the same event (e.g., breaking news + confirmation + detail) trigger multiple entries on the same market before the book has repriced."
    },
    {
      "failure": "feeRateBps present on signed order (V1 pattern)",
      "consequence": "CTFExchangeV2 rejects orders with feeRateBps. Fees are operator-set at match time. The signed order must not contain this field."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Watchlisted markets matched by entity",
      "source": "internal (entity-resolution dictionary + Gamma API)",
      "required": true,
      "use": "Map the news event's entity to one or more open Polymarket markets. Only markets in the watchlist are eligible."
    },
    {
      "input": "Book depth and recent volatility on the target market",
      "source": "clob_public + ws_market",
      "required": true,
      "use": "Size the IOC order to min(available_depth, max_position_usd) and check recent volatility hasn't already absorbed the news."
    },
    {
      "input": "Market open/closed/resolved status",
      "source": "clob_public",
      "required": true,
      "use": "Skip markets that are closed, resolved, or approaching resolution (< 30 min to close)."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Abort all intent emission immediately if KillSwitch is active."
    },
    {
      "input": "NewsIngest materiality score + entity match + news item",
      "source": "internal (NewsIngest pipeline)",
      "required": true,
      "use": "Trigger trade when materiality_score >= materiality_threshold and entity resolves to an open watchlist market."
    },
    {
      "input": "NLP materiality classifier",
      "source": "internal",
      "required": true,
      "use": "Score each news item on a 0\u20131 scale for market-moving impact on the resolved entity's Polymarket market."
    },
    {
      "input": "Entity-resolution dictionary",
      "source": "internal",
      "required": true,
      "use": "Map news entities (persons, teams, countries, companies) to Polymarket condition IDs."
    },
    {
      "input": "Builder code bytes32",
      "source": "internal config",
      "required": true,
      "use": "Injected into builder field on every signed V2 OrderIntent."
    }
  ],
  "raw_params": [
    "materiality_threshold \u00b7 0\u20131",
    "cooldown_s \u00b7 int",
    "order_ttl_s \u00b7 int (60\u2013300)",
    "max_position_usd \u00b7 int"
  ],
  "parameters": [
    {
      "name": "materiality_threshold",
      "default": 0.72,
      "warning": 0.55,
      "hard": 0.4,
      "controls": "Minimum NLP materiality score (0\u20131) required before emitting an OrderIntent on a matched market.",
      "why_default_matters": "0.72 filters out speculation and soft news, targeting only high-confidence material events. Below 0.55 the classifier confidence is marginal; below 0.40 the bot will not trade regardless of config.",
      "threshold_logic": [
        {
          "condition": ">= 0.72",
          "action": "EMIT IOC OrderIntent"
        },
        {
          "condition": "0.55\u20130.72",
          "action": "WARN NEWS_MATERIALITY_SCORE_MARGINAL; emit at 50% size"
        },
        {
          "condition": "< 0.40 (hard floor)",
          "action": "SKIP \u2014 NEWS_MATERIALITY_TOO_LOW"
        }
      ],
      "dev_check": "if score < params.hard: return skip('NEWS_MATERIALITY_TOO_LOW')",
      "user_facing": "The news item did not score highly enough on the materiality classifier to justify a trade."
    },
    {
      "name": "cooldown_s",
      "default": 120,
      "warning": 45,
      "hard": 20,
      "controls": "Minimum seconds between trades on the same entity-market pair. Prevents re-entering on follow-up coverage of the same event.",
      "why_default_matters": "120 seconds ensures the book has time to partially reprice after the first trade before a second entry. Below 45s the book may not have moved at all; below 20s the bot will not trade regardless of config.",
      "threshold_logic": [
        {
          "condition": ">= 120s",
          "action": "Normal cooldown"
        },
        {
          "condition": "45\u2013120s",
          "action": "WARN NEWS_MATERIALITY_SHORT_COOLDOWN; may re-trade same event"
        },
        {
          "condition": "< 20s (hard floor)",
          "action": "Reject config \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if params.cooldown_s < params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": ""
    },
    {
      "name": "order_ttl_s",
      "default": 90,
      "warning": 200,
      "hard": 300,
      "controls": "Maximum seconds the IOC order's intent is considered valid. If not filled within this window, the trade is abandoned (not re-sent).",
      "why_default_matters": "90 seconds limits news-edge exposure to the window where the event is still not fully priced in. Beyond 200s the news is likely fully digested and the edge is gone.",
      "threshold_logic": [
        {
          "condition": "<= 90s",
          "action": "Normal TTL window"
        },
        {
          "condition": "90\u2013300s",
          "action": "WARN NEWS_MATERIALITY_LONG_TTL; news likely digested before fill"
        },
        {
          "condition": "> 300s (hard ceiling)",
          "action": "Reject config \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if params.order_ttl_s > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "The news trade window has passed. The order was not placed as the opportunity may no longer be valid."
    },
    {
      "name": "max_position_usd",
      "default": 300,
      "warning": 500,
      "hard": 750,
      "controls": "Maximum pUSD size per news-triggered IOC order.",
      "why_default_matters": "300 pUSD per event limits single-news exposure. Above 500 pUSD the order may consume most of the top-of-book depth and cause significant slippage.",
      "threshold_logic": [
        {
          "condition": "<= 300 pUSD",
          "action": "Normal size"
        },
        {
          "condition": "300\u2013750 pUSD",
          "action": "WARN; confirm depth supports order; slippage risk elevated"
        },
        {
          "condition": "> 750 pUSD",
          "action": "Reject config \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if params.max_position_usd > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "The news trade was sized to fit within available market liquidity."
    }
  ],
  "default_config": {
    "bot_id": "strat.news_materiality_trader",
    "version": "2.1.0",
    "mode": "limited_live",
    "defaults": {
      "materiality_threshold": 0.72,
      "cooldown_s": 120,
      "order_ttl_s": 90,
      "max_position_usd": 300
    },
    "locked": {
      "materiality_threshold": {
        "min": 0.4
      },
      "cooldown_s": {
        "min": 20
      },
      "order_ttl_s": {
        "max": 300
      },
      "max_position_usd": {
        "max": 750
      }
    }
  },
  "implementation_flow": [
    "Check KillSwitch active flag; if active, emit no OrderIntents.",
    "Subscribe to NewsIngest internal bus for materiality-scored news events.",
    "On each news event: check materiality_score >= materiality_threshold hard floor (0.40); if below, skip NEWS_MATERIALITY_TOO_LOW (sampled 1/100).",
    "Run entity-resolution: map news entity to Polymarket condition ID(s) via entity-resolution dictionary. If no match, skip NEWS_MATERIALITY_NO_MARKET_MATCH.",
    "For each matched market: confirm market is open and not approaching resolution (< 30 min to close) via clob_public.",
    "Check cooldown: if last trade on this entity-market pair was < cooldown_s seconds ago, skip NEWS_MATERIALITY_COOLDOWN_ACTIVE.",
    "If materiality_score < warning threshold (0.72): WARN NEWS_MATERIALITY_SCORE_MARGINAL; reduce size to 50% of max_position_usd.",
    "Fetch top-of-book depth from clob_public; set orderSize = min(depth, max_position_usd) * sizeMultiplier.",
    "Check recent market volatility (ws_market): if price has already moved > 50% of expected news impact, skip NEWS_MATERIALITY_ALREADY_DIGESTED.",
    "Emit OrderIntent: outcome=YES (or NO depending on event direction), side=buy, price=best_ask, size_pUSD=orderSize, tif=IOC, builder={code, fee_bps:25}.",
    "Note: fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order.",
    "Register cooldown state: last_trade_ms for entity-market pair.",
    "Emit DecisionReport with intent_emitted=true, materiality_score, entity_id, market_id, reason NEWS_MATERIALITY_TRADE_TRIGGERED."
  ],
  "decision_logic": {
    "approve": "materiality_score >= materiality_threshold, entity resolves to open market, cooldown elapsed, depth sufficient, KillSwitch inactive. Emit IOC OrderIntent.",
    "reshape_required": "Not applicable \u2014 strat bots emit OrderIntents; reshaping is handled by the downstream Risk guardrail pipeline.",
    "reject": "score < 0.40 hard floor; no entity match; cooldown active; market closed; KillSwitch active; stale feed. Emit DecisionReport intent_emitted=false.",
    "warning_only": "score between 0.40 and 0.72 triggers NEWS_MATERIALITY_SCORE_MARGINAL and 50% size reduction."
  },
  "decision_output_schema": "OrderIntent",
  "decision_output_example": {
    "intent_id": "oi_01HXNMT0000001A",
    "trace_id": "tr_01HXNMT000TR001",
    "market_id": "0xnewsmat000000000000000000000000000000000000000000000000000000001",
    "outcome": "YES",
    "side": "buy",
    "price": "0.438",
    "size_pUSD": "300.00",
    "tif": "IOC",
    "post_only": false,
    "builder": {
      "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
      "fee_bps": 25
    },
    "negrisk_aware": false,
    "decision": {
      "materiality_score": 0.81,
      "entity_id": "entity_candidate_A_primary",
      "news_source": "Reuters",
      "reasons": [
        "NEWS_MATERIALITY_TRADE_TRIGGERED"
      ]
    },
    "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
  },
  "developer_log": {
    "bot_id": "strat.news_materiality_trader",
    "market_id": "0xnewsmat000000000000000000000000000000000000000000000000000000001",
    "materiality_score": 0.81,
    "entity_id": "entity_candidate_A_primary",
    "news_source": "Reuters",
    "order_size_pusd": 300.0,
    "price_at_entry": 0.438,
    "cooldown_cleared": true,
    "depth_pusd": 520.0,
    "intent_emitted": true,
    "reason": "NEWS_MATERIALITY_TRADE_TRIGGERED",
    "emitted_at_ms": 1746790600000
  },
  "user_explanations": [
    {
      "situation": "News-triggered trade placed",
      "message": "A material news event was detected and matched to this market. An order was placed to position ahead of the expected book repricing."
    },
    {
      "situation": "News scored too low \u2014 no trade",
      "message": "The news item did not rate highly enough on the materiality classifier to justify a trade. No order was placed."
    },
    {
      "situation": "Cooldown active \u2014 no trade",
      "message": "A trade was placed recently on the same market for the same news entity. The cooldown period must elapse before another trade is considered."
    },
    {
      "situation": "News already digested \u2014 no trade",
      "message": "The market price had already moved significantly before the order was placed, suggesting the news is already priced in. No order was placed to avoid chasing the move."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "False materiality classification: the NLP classifier scores a rumour or corrected story as material, triggering a trade that reverses when the story is clarified, producing a loss.",
    "false_positive_risk": "High materiality score on irrelevant news that shares entity keywords with a target market (e.g., 'Team X wins match' when the market is about a different competition).",
    "false_negative_risk": "materiality_threshold set too high misses genuine market-moving events, especially fast-breaking sports results where the classifier has less training data.",
    "safe_fallback": "If NewsIngest feed is unavailable or clob_public market data is stale (> 5s), emit STALE_MARKET_DATA DecisionReport and skip without emitting any OrderIntent. Fail closed on feed unavailability.",
    "required_dependencies": [
      "NewsIngest internal feed (Reuters, AP, Bloomberg, league wires + NLP classifier)",
      "Entity-resolution dictionary",
      "clob_public market endpoint (status + depth)",
      "ws_market (recent price volatility check)",
      "KillSwitch active flag",
      "internal builder code"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Emit IOC when materiality_score=0.81, entity resolves, cooldown cleared",
        "setup": "score=0.81, threshold=0.72, cooldown elapsed, market open, depth=520",
        "expected": "IOC OrderIntent emitted; DecisionReport intent_emitted=true, reason=NEWS_MATERIALITY_TRADE_TRIGGERED"
      },
      {
        "test": "Skip when score < hard floor (0.40)",
        "setup": "materiality_score=0.35",
        "expected": "No OrderIntent; sampled DecisionReport reason=NEWS_MATERIALITY_TOO_LOW"
      },
      {
        "test": "Skip when cooldown active",
        "setup": "last_trade_ms = now_ms() - 30s (cooldown_s=120)",
        "expected": "No OrderIntent; reason=NEWS_MATERIALITY_COOLDOWN_ACTIVE"
      },
      {
        "test": "Reduce size 50% when score marginal (0.60)",
        "setup": "score=0.60, threshold=0.72, max_position_usd=300",
        "expected": "OrderIntent emitted with size=150; WARN NEWS_MATERIALITY_SCORE_MARGINAL"
      },
      {
        "test": "Skip when no entity match in dictionary",
        "setup": "news entity not in entity-resolution dictionary",
        "expected": "No OrderIntent; reason=NEWS_MATERIALITY_NO_MARKET_MATCH"
      },
      {
        "test": "Skip when KillSwitch active",
        "setup": "killswitch.active=true",
        "expected": "No OrderIntents emitted"
      }
    ],
    "integration": [
      {
        "test": "Full cycle: news event arrives \u2192 NLP scores \u2192 entity resolved \u2192 IOC OrderIntent submitted",
        "expected": "Order has builder.code (bytes32), no feeRateBps, tif=IOC, EIP-712 domain version '2'"
      },
      {
        "test": "Cooldown correctly prevents re-trading same entity-market pair within cooldown_s",
        "expected": "Second intent blocked for cooldown_s; emitted after cooldown elapsed"
      }
    ],
    "property": [
      {
        "property": "Bot never trades a market without a confirmed entity match in the resolution dictionary",
        "required": "Always true"
      },
      {
        "property": "feeRateBps never present on any signed OrderIntent",
        "required": "Always true \u2014 V2 fees are operator-set at match time"
      },
      {
        "property": "Cooldown always enforced per entity-market pair; no two intents within cooldown_s on same pair",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Trade material news events before the book fully digests them.",
  "legacy_pm_signals": [
    "Watchlisted markets matched by entity",
    "Book depth + recent volatility on the target market"
  ],
  "legacy_external_feeds": [
    "Reuters, AP, Bloomberg, league wires (via NewsIngest)",
    "NLP materiality classifier",
    "Entity-resolution dictionary"
  ],
  "reporting_groups": [
    "strategy_decision"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "clob_public",
    "clob_auth",
    "ws_market",
    "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)",
      "reason": "CLOB V2 cutover",
      "action_taken": "Switched to py-clob-client-v2. Removed feeRateBps from all signed order construction. Updated collateral to pUSD. Injected builder field (bytes32) on every OrderIntent. EIP-712 Exchange domain version updated from '1' to '2'. NewsIngest pipeline updated to V2 internal bus schema. Entity-resolution dictionary updated for 2026 market coverage."
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Executes taker IOC orders on standard binary markets when a material news event is scored above materiality_threshold by the NLP classifier. feeRateBps is not present on any signed order. Builder code injected on every intent."
  },
  "reference_implementation": {
    "summary": "Consumes news events from the NewsIngest internal bus, scores materiality via NLP classifier, resolves entities to open Polymarket markets, and emits IOC OrderIntents when materiality exceeds threshold and cooldown has elapsed.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION onNewsEvent(newsItem):\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active: RETURN\n\n  // --- 1. Materiality score gate ---\n  score = newsItem.materiality_score  // from NLP classifier\n  IF score < params.materiality_threshold_hard:  // 0.40\n    IF random() < 0.01:\n      EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_TOO_LOW', score=score)\n    RETURN\n\n  // --- 2. Entity resolution ---\n  markets = FETCH internal.entityDict.resolve(newsItem.entity_id)\n  IF NOT markets:\n    EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_NO_MARKET_MATCH')\n    RETURN\n\n  FOR market_id IN markets:\n    // --- 3. Market status check ---\n    mkt = FETCH clob_public.GET('/markets/' + market_id)\n    IF mkt.closed OR mkt.resolved OR mkt.minutes_to_close < 30:\n      CONTINUE\n\n    // --- 4. Cooldown check ---\n    lastTrade = FETCH state.lastTradeMs(newsItem.entity_id, market_id)\n    IF (now_ms() - lastTrade) < params.cooldown_s * 1000:\n      EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_COOLDOWN_ACTIVE')\n      CONTINUE\n\n    // --- 5. Already-digested check ---\n    recentMove = FETCH ws_market.priceChangeSinceMs(market_id, newsItem.received_at_ms)\n    IF recentMove > 0.5 * expectedNewsImpact(score):\n      EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_ALREADY_DIGESTED')\n      CONTINUE\n\n    // --- 6. Size determination ---\n    sizeMultiplier = 0.5 IF score < params.materiality_threshold ELSE 1.0\n    IF sizeMultiplier < 1.0: WARN('NEWS_MATERIALITY_SCORE_MARGINAL')\n    depth = FETCH clob_public.depth(market_id, side='YES')\n    orderSize = toPusdUnits(min(depth, params.max_position_usd) * sizeMultiplier)\n\n    // --- 7. Emit IOC OrderIntent (V2: no feeRateBps) ---\n    side = 'buy' IF newsItem.direction == 'positive' ELSE 'sell'\n    EMIT OrderIntent(\n      market_id = market_id, outcome = 'YES', side = side,\n      price = mkt.best_ask, size_pUSD = orderSize, tif = 'IOC', post_only = false,\n      builder = {code: internal.builder_code, fee_bps: 25}\n    )\n\n    // --- 8. Register cooldown ---\n    SET state.lastTradeMs(newsItem.entity_id, market_id, now_ms())\n\n    EMIT DecisionReport(intent_emitted=true, materiality_score=score,\n                        entity_id=newsItem.entity_id, market_id=market_id,\n                        reason='NEWS_MATERIALITY_TRADE_TRIGGERED')",
    "sdk_calls": [
      "fetchClobPublic('/markets/' + market_id)",
      "ws_market.subscribe('book', [market_id])",
      "internal.entityDict.resolve(entity_id)",
      "internal.newsingest.subscribe()",
      "internal.killswitch.status()",
      "toPusdUnits(rawFloat)",
      "buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })",
      "internal.builder_code"
    ],
    "complexity": "O(n_markets_matched) per news event; typically O(1) for single-entity events"
  },
  "wire_examples": {
    "input": [
      {
        "label": "NewsIngest event \u2014 high materiality, entity resolved",
        "source": "internal (NewsIngest bus)",
        "payload": {
          "event_id": "news_01HXNMT000EVENT",
          "entity_id": "entity_candidate_A_primary",
          "headline": "Candidate A wins state primary by large margin",
          "source": "Reuters",
          "materiality_score": 0.81,
          "direction": "positive",
          "matched_market_ids": [
            "0xnewsmat000000000000000000000000000000000000000000000000000000001"
          ],
          "received_at_ms": 1746790600000
        }
      }
    ],
    "output": [
      {
        "label": "OrderIntent \u2014 news IOC buy YES (builder-attributed)",
        "payload": {
          "intent_id": "oi_01HXNMT0000001A",
          "trace_id": "tr_01HXNMT000TR001",
          "market_id": "0xnewsmat000000000000000000000000000000000000000000000000000000001",
          "outcome": "YES",
          "side": "buy",
          "price": "0.438",
          "size_pUSD": "300.00",
          "tif": "IOC",
          "post_only": false,
          "builder": {
            "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
            "fee_bps": 25
          },
          "negrisk_aware": false,
          "decision": {
            "materiality_score": 0.81,
            "entity_id": "entity_candidate_A_primary",
            "news_source": "Reuters",
            "reasons": [
              "NEWS_MATERIALITY_TRADE_TRIGGERED"
            ]
          },
          "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
        }
      },
      {
        "label": "DecisionReport \u2014 skipped (score too low), sampled 1/100",
        "payload": {
          "report_id": "dr_01HXNMT999ZZZZ",
          "bot_id": "strat.news_materiality_trader",
          "entity_id": "entity_candidate_B",
          "intent_emitted": false,
          "materiality_score": 0.28,
          "reasons": [
            "NEWS_MATERIALITY_TOO_LOW"
          ],
          "sampled": true,
          "evaluated_at_ms": 1746790601000
        }
      }
    ]
  },
  "reason_codes": [
    {
      "code": "NEWS_MATERIALITY_TRADE_TRIGGERED",
      "severity": "INFO",
      "meaning": "materiality_score >= threshold, entity resolved, cooldown cleared, market open. IOC OrderIntent emitted.",
      "action": "Emit IOC OrderIntent; register cooldown.",
      "user_message": "A material news event was detected and a trade was placed to capture the expected price move."
    },
    {
      "code": "NEWS_MATERIALITY_TOO_LOW",
      "severity": "INFO",
      "meaning": "materiality_score is below the 0.40 hard floor. News is not material enough to trade.",
      "action": "Skip; emit sampled DecisionReport.",
      "user_message": "The news item was not rated as material enough to justify a trade."
    },
    {
      "code": "NEWS_MATERIALITY_SCORE_MARGINAL",
      "severity": "WARN",
      "meaning": "materiality_score between 0.40 and 0.72. Trade is marginal; size reduced to 50%.",
      "action": "Emit IOC at 50% size; log warning.",
      "user_message": "A moderately material news event was detected. A smaller trade was placed."
    },
    {
      "code": "NEWS_MATERIALITY_NO_MARKET_MATCH",
      "severity": "INFO",
      "meaning": "News entity did not resolve to any open watchlist market in the entity-resolution dictionary.",
      "action": "Skip; emit DecisionReport.",
      "user_message": ""
    },
    {
      "code": "NEWS_MATERIALITY_COOLDOWN_ACTIVE",
      "severity": "INFO",
      "meaning": "Last trade on this entity-market pair was within cooldown_s seconds.",
      "action": "Skip; emit DecisionReport.",
      "user_message": "A trade was placed recently for this news entity. The cooldown period must pass before another trade."
    },
    {
      "code": "NEWS_MATERIALITY_ALREADY_DIGESTED",
      "severity": "INFO",
      "meaning": "Market price has already moved significantly since the news event; likely already digested.",
      "action": "Skip; emit DecisionReport.",
      "user_message": "The market appears to have already reacted to this news. No trade was placed."
    },
    {
      "code": "NEWS_MATERIALITY_SHORT_COOLDOWN",
      "severity": "WARN",
      "meaning": "cooldown_s config is below the 45s warning threshold. Re-trading same event risk elevated.",
      "action": "Allow but log warning.",
      "user_message": ""
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "clob_public or ws_market data is stale (> 5s) for target market.",
      "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 news events; no OrderIntents emitted.",
      "user_message": "Trading is currently paused."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_strat_newsmateriality_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "verdict",
          "reason_code"
        ],
        "meaning": "Total news events evaluated by intent_emitted and reason code."
      },
      {
        "name": "polytraders_strat_newsmateriality_score",
        "type": "histogram",
        "unit": "score",
        "labels": [],
        "meaning": "Distribution of NLP materiality scores across all evaluated news events."
      },
      {
        "name": "polytraders_strat_newsmateriality_intents_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "news_source",
          "entity_category"
        ],
        "meaning": "Total IOC OrderIntents emitted by news source and entity category."
      },
      {
        "name": "polytraders_strat_newsmateriality_cooldown_blocks_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "entity_id"
        ],
        "meaning": "Number of trades blocked by active cooldown per entity."
      },
      {
        "name": "polytraders_strat_newsmateriality_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Wall-clock time from news event receipt to OrderIntent emit."
      },
      {
        "name": "polytraders_strat_newsmateriality_digested_skips_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "market_id"
        ],
        "meaning": "News events skipped because market had already priced the news."
      }
    ],
    "alerts": [
      {
        "name": "NewsMaterialityHighLatency",
        "condition": "histogram_quantile(0.99, rate(polytraders_strat_newsmateriality_eval_latency_ms_bucket[5m])) > 500",
        "severity": "warn",
        "runbook": "#runbook-newsmateriality-latency"
      },
      {
        "name": "NewsMaterialityHighDigestedRate",
        "condition": "rate(polytraders_strat_newsmateriality_digested_skips_total[10m]) / rate(polytraders_strat_newsmateriality_decisions_total[10m]) > 0.6",
        "severity": "warn",
        "runbook": "#runbook-newsmateriality-digested"
      },
      {
        "name": "NewsMaterialityStaleFeed",
        "condition": "rate(polytraders_strat_newsmateriality_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1",
        "severity": "warn",
        "runbook": "#runbook-newsmateriality-stale-feed"
      },
      {
        "name": "NewsMaterialityKillSwitchBlocking",
        "condition": "rate(polytraders_strat_newsmateriality_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0",
        "severity": "page",
        "runbook": "#runbook-killswitch"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Strategy / NewsMateriality score distribution and trade frequency",
      "Grafana \u2014 Strategy / NewsMateriality entity resolution hit rate and cooldown blocks"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Per entity-market pair last trade timestamp (last_trade_ms); per market recent price state for digestion check; keyed by entity_id + market_id",
    "ttl": "cooldown_s (default 120s) for cooldown state; 5s for price state",
    "recovery": "On cold start, cooldown state is cleared (conservative: may re-trade within cooldown on restart). Price state rebuilt from first ws_market tick.",
    "size_estimate": "~100 bytes per active entity-market pair; typically < 500 KB total"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 20,
    "idempotency_key": "intent_id",
    "timeout_ms": 300,
    "backpressure": "drop oldest pending news event per entity when queue depth > 3",
    "locking": "per entity_id+market_id mutex for cooldown state read/write"
  },
  "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": "IOC OrderIntents for risk guardrail evaluation before SmartRouter."
      },
      {
        "bot_id": "gov.builder_attribution",
        "what": "builder.code bytes32 on every OrderIntent."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Polymarket CLOB public API",
        "sla": "99.9% (Polymarket-published)",
        "fallback": "Skip trade; emit STALE_MARKET_DATA DecisionReport."
      },
      {
        "service": "Polymarket CLOB WebSocket (ws_market)",
        "sla": "best-effort",
        "fallback": "Use cached price state; if cache > 5s, treat as already-digested."
      },
      {
        "service": "NewsIngest internal feed (Reuters, AP, Bloomberg, league wires)",
        "sla": "internal SLA",
        "fallback": "Emit STALE_MARKET_DATA on any news event received while feed is lagged > 30s."
      },
      {
        "service": "NLP materiality classifier (internal)",
        "sla": "internal SLA",
        "fallback": "Reject news events if classifier is unavailable; fail-closed."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": true,
    "private_key_access": "signing-only",
    "abuse_vectors": [
      "Injecting synthetic news into the NewsIngest feed to trigger trades on specific markets",
      "Adversary front-runs the bot by monitoring entity-resolution patterns to predict which markets will be traded on material news",
      "Cooldown manipulation: flooding the system with low-score news to exhaust entity-market pair cooldown windows"
    ],
    "mitigations": [
      "NewsIngest feed is authenticated and sourced only from verified news providers; synthetic injection requires compromising the internal feed",
      "IOC orders do not rest on the book; adversary cannot exploit known bid/ask placement",
      "Hard materiality floor (0.40) prevents cooldown exhaustion via noise events",
      "V2 order timestamp(ms) invalidates replayed signed orders"
    ],
    "contract_calls": [
      {
        "contract": "CTFExchangeV2",
        "function": "matchOrders",
        "purpose": "Settlement of YES/NO token purchases triggered by material news events."
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "LOW_MATERIALITY_NEWS",
      "how_to_inject": "Send mock news event with materiality_score=0.25 (< 0.40 hard floor)",
      "expected_behaviour": "NEWS_MATERIALITY_TOO_LOW; no OrderIntent; sampled DecisionReport",
      "recovery": "Automatic; next qualifying news event can fire."
    },
    {
      "scenario": "COOLDOWN_ACTIVE",
      "how_to_inject": "Set state.lastTradeMs = now_ms() - 30s (< cooldown_s=120)",
      "expected_behaviour": "NEWS_MATERIALITY_COOLDOWN_ACTIVE; no OrderIntent",
      "recovery": "Automatic when cooldown_s elapses."
    },
    {
      "scenario": "NEWS_ALREADY_DIGESTED",
      "how_to_inject": "Set mock price change since news to > 50% of expected impact",
      "expected_behaviour": "NEWS_MATERIALITY_ALREADY_DIGESTED; no OrderIntent",
      "recovery": "Automatic; subsequent news events on same market can trade."
    },
    {
      "scenario": "NLP_CLASSIFIER_UNAVAILABLE",
      "how_to_inject": "Take NLP classifier offline",
      "expected_behaviour": "No trades while classifier is unavailable; all news events skipped (fail-closed)",
      "recovery": "Automatic when classifier restores."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behaviour": "No OrderIntents emitted for any news event",
      "recovery": "Automatic on manual KillSwitch reset."
    }
  ],
  "runbook": {
    "summary": "News Materiality Trader incidents are typically high already-digested rates (slow news pipeline latency), NLP classifier outages (blocking all trades), or false-materiality classifications causing unexpected positions. Classifier outages fail-closed and resolve automatically.",
    "oncall_actions": [
      {
        "alert": "NewsMaterialityHighLatency",
        "first_action": "Check end-to-end NewsIngest pipeline latency from wire receipt to OrderIntent emit.",
        "escalate_to": "Strategy pod lead if p99 latency > 1s (news edge consumed before order arrives)."
      },
      {
        "alert": "NewsMaterialityHighDigestedRate",
        "first_action": "Check NewsIngest pipeline lag; confirm news events are arriving within 10s of wire publication.",
        "escalate_to": "Infra on-call if feed lag > 30s consistently."
      },
      {
        "alert": "NewsMaterialityStaleFeed",
        "first_action": "Check ws_market WebSocket connectivity and clob_public API reachability.",
        "escalate_to": "Infra on-call if disconnected > 2 minutes."
      },
      {
        "alert": "NewsMaterialityKillSwitchBlocking",
        "first_action": "Confirm KillSwitch activation was intentional.",
        "escalate_to": "Risk pod lead immediately."
      }
    ],
    "manual_overrides": [
      {
        "name": "block_entity",
        "how": "Add entity_id to config.blocked_entities list",
        "when": "Entity is producing false-materiality matches (e.g., common name ambiguity in entity-resolution dictionary)."
      },
      {
        "name": "pause_bot",
        "how": "polytraders bot pause strat.news_materiality_trader",
        "when": "NLP classifier producing systematic false-positive scores during a high-volume news cycle."
      }
    ],
    "healthcheck": "GET /internal/health/news-materiality-trader -> 200 if NewsIngest feed live, NLP classifier reachable, clob_public reachable, KillSwitch inactive, at least one news event evaluated in last 10 min."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "All unit tests pass including cooldown invariant and entity-resolution gate",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "feeRateBps absence verified; tif=IOC verified in integration test",
        "how_measured": "Integration test asserting V2 order schema",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "p99 eval latency < 300ms over 24h (from news event receipt to OrderIntent emit)",
        "how_measured": "polytraders_strat_newsmateriality_eval_latency_ms histogram",
        "threshold": "p99 < 300ms"
      },
      {
        "gate": "Already-digested rate < 40% of qualifying events over 48h shadow run",
        "how_measured": "polytraders_strat_newsmateriality_digested_skips_total / decisions_total",
        "threshold": "< 40%"
      }
    ],
    "to_general_live": [
      {
        "gate": "E2E: news event arrives \u2192 NLP scores \u2192 entity resolved \u2192 IOC OrderIntent submitted on Polygon testnet",
        "how_measured": "E2E test",
        "threshold": "Pass"
      },
      {
        "gate": "Cooldown enforcement verified: same entity-market pair not re-traded within cooldown_s in integration test",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "DecisionReport"
    ],
    "topics": [
      "polytraders.reports.decision"
    ],
    "cadence": "every-event",
    "retention_class": "2y",
    "sampling_rule": "emit-every for emitted intents; sample-1/100 for low-score skips",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ObservationReport",
      "RiskVote"
    ],
    "reporting_groups": [
      "strategy_decision"
    ]
  },
  "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"
  }
}