{
  "schema_version": "1.0.0",
  "bot_id": "3.20",
  "bot_name": "NarrativeCrowdingFade",
  "slug": "narrativecrowdingfade",
  "layer": "Strategy",
  "layer_key": "strat",
  "bot_class": "Alpha Strategy",
  "authority": [
    "Trade"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Strategy",
    "bot_class": "Alpha Strategy",
    "authority": "Trade",
    "runs_before": "Risk guardrail pipeline",
    "runs_after": "Observation bus / internal analytics",
    "applies_to": "Polymarket binary markets where attention Z-score >= min_attention_zscore and price drift from base rate >= min_drift_bps, indicating narrative crowding above statistical base rate",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Strategy pod"
  },
  "purpose": "NarrativeCrowdingFade detects Polymarket binary markets where social attention and narrative momentum have pushed prices above the statistical base rate. When attention Z-score and price drift signal crowding, the bot fades the move \u2014 taking the opposite position to capture reversion toward the base rate.",
  "why_it_matters": [
    {
      "failure": "News is genuinely paradigm-shifting",
      "consequence": "If the crowded narrative reflects real new information rather than sentiment overshoot, the fade position loses as the market price correctly adjusts to the new fundamental."
    },
    {
      "failure": "Stale input data",
      "consequence": "Acting on stale signals for NarrativeCrowdingFade produces trades based on outdated market conditions, generating adverse fills."
    },
    {
      "failure": "KillSwitch not respected",
      "consequence": "Emitting OrderIntents while KillSwitch is active bypasses risk controls."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "CLOB book (mid, depth, spread)",
      "source": "ws_market",
      "required": true,
      "use": "Read current market price and available depth for order sizing."
    },
    {
      "input": "Market status (open/closed/resolved)",
      "source": "clob_public",
      "required": true,
      "use": "Skip closed or resolved markets."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Abort all intent emission if KillSwitch active."
    },
    {
      "input": "NarrativeCrowdingFade analytics signal",
      "source": "internal (analytics engine)",
      "required": true,
      "use": "Provides the core NarrativeCrowdingFade signal that drives trade decisions."
    },
    {
      "input": "Builder code bytes32",
      "source": "internal config",
      "required": true,
      "use": "Injected into builder field on every signed V2 OrderIntent."
    }
  ],
  "raw_params": [
    "min_attention_zscore \u00b7 float",
    "min_drift_bps \u00b7 int",
    "max_position_per_event \u00b7 int",
    "cool_off_after_news \u00b7 int"
  ],
  "parameters": [
    {
      "name": "min_attention_zscore",
      "default": 2.0,
      "warning": 1.5,
      "hard": 1.0,
      "controls": "Minimum Z-score of attention (volume, social mentions) relative to market history before the bot considers a fade trade.",
      "why_default_matters": "Z-score >= 2.0 indicates statistically significant crowding, not normal attention.",
      "threshold_logic": [
        {
          "condition": ">= 2.0",
          "action": "Allow fade evaluation"
        },
        {
          "condition": "1.5\u20132.0",
          "action": "WARN NCF_WEAK_CROWDING; halve size"
        },
        {
          "condition": "< 1.0",
          "action": "SKIP NCF_ATTENTION_BELOW_FLOOR"
        }
      ],
      "dev_check": "if attention_zscore < params.hard: return skip('NCF_ATTENTION_BELOW_FLOOR')",
      "user_facing": "The attention signal was not strong enough to indicate crowding."
    },
    {
      "name": "min_drift_bps",
      "default": 200,
      "warning": 100,
      "hard": 50,
      "controls": "Minimum price drift in bps from the base rate required before the bot fades the move.",
      "why_default_matters": "200 bps ensures the market has meaningfully overshot the base rate before fading.",
      "threshold_logic": [
        {
          "condition": ">= 200 bps",
          "action": "EMIT fade IOC"
        },
        {
          "condition": "100\u2013200 bps",
          "action": "WARN NCF_DRIFT_MARGINAL; halve size"
        },
        {
          "condition": "< 50 bps",
          "action": "SKIP NCF_NO_DRIFT"
        }
      ],
      "dev_check": "if drift_bps < params.hard: return skip('NCF_NO_DRIFT')",
      "user_facing": "The price drift was too small to justify a fade trade."
    },
    {
      "name": "max_position_per_event",
      "default": 400,
      "warning": 600,
      "hard": 800,
      "controls": "Maximum pUSD per event for narrative fade positions.",
      "why_default_matters": "400 pUSD limits single-event narrative exposure.",
      "threshold_logic": [
        {
          "condition": "<= 400 pUSD",
          "action": "Normal sizing"
        },
        {
          "condition": "> 800 pUSD",
          "action": "Reject config"
        }
      ],
      "dev_check": "if params.max_position_per_event > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "Position capped at per-event maximum."
    },
    {
      "name": "cool_off_after_news",
      "default": 300,
      "warning": 120,
      "hard": 0,
      "controls": "Seconds to pause after a major news hit before re-evaluating the fade thesis.",
      "why_default_matters": "300s cool-off allows initial price discovery before the fade re-enters.",
      "threshold_logic": [
        {
          "condition": ">= 300s",
          "action": "Standard cool-off"
        },
        {
          "condition": "< 120s",
          "action": "WARN NCF_SHORT_COOLOFF"
        }
      ],
      "dev_check": "if in_cooloff(market_id): return skip('NCF_COOLOFF_ACTIVE')",
      "user_facing": "A brief pause is in effect after a major news event."
    }
  ],
  "default_config": {
    "bot_id": "strat.narrativecrowdingfade",
    "version": "0.1.0",
    "mode": "shadow_only",
    "defaults": {
      "min_attention_zscore": 2.0,
      "min_drift_bps": 200,
      "max_position_per_event": 400,
      "cool_off_after_news": 300
    },
    "locked": {
      "min_attention_zscore": {
        "min": 1.0
      },
      "min_drift_bps": {
        "min": 50
      },
      "max_position_per_event": {
        "min": 800
      },
      "cool_off_after_news": {
        "min": 0
      }
    }
  },
  "implementation_flow": [
    "Check KillSwitch; if active, emit no OrderIntents.",
    "FETCH NarrativeCrowdingFade analytics signal from internal engine.",
    "IF signal below hard floor: SKIP, emit sampled DecisionReport NCF_NO_EDGE.",
    "FETCH clob_public market status; skip if closed or resolved.",
    "FETCH ws_market book; compute current mid and available depth.",
    "IF signal < warning threshold: WARN NCF_MARGINAL; reduce size 50%.",
    "Compute order size = min(max_size_param, available_depth).",
    "EMIT IOC OrderIntent with builder code.",
    "EMIT DecisionReport with intent_emitted=true, reason=NCF_TRADE."
  ],
  "decision_logic": {
    "approve": "All gates passed, KillSwitch inactive, market open. Emit IOC OrderIntent.",
    "reshape_required": "Not applicable \u2014 reshaping handled by downstream Risk guardrail.",
    "reject": "Signal below hard floor; stale data; market closed; KillSwitch active.",
    "warning_only": "Signal in warning zone triggers 50% size reduction."
  },
  "decision_output_schema": "OrderIntent",
  "decision_output_example": {
    "intent_id": "oi_01HNCF0000001A",
    "trace_id": "tr_01HNCF000TR001",
    "market_id": "0xnarrativ000000000000000000000000000000000000000000000000000000000001",
    "outcome": "YES",
    "side": "buy",
    "price": "0.540",
    "size_pUSD": "200.00",
    "tif": "IOC",
    "post_only": false,
    "builder": {
      "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
      "fee_bps": 25
    },
    "negrisk_aware": false,
    "decision": {
      "signal_score": 0.75,
      "reasons": [
        "NCF_TRADE"
      ]
    },
    "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
  },
  "developer_log": {
    "bot_id": "strat.narrativecrowdingfade",
    "market_id": "0xnarrativ000000000000000000000000000000000000000000000000000000000001",
    "signal_score": 0.75,
    "intent_emitted": true,
    "reason": "NCF_TRADE",
    "emitted_at_ms": 1746790800000
  },
  "user_explanations": [
    {
      "situation": "NarrativeCrowdingFade trade placed",
      "message": "The NarrativeCrowdingFade strategy detected a suitable opportunity and placed a trade."
    },
    {
      "situation": "Edge too small \u2014 no trade",
      "message": "The signal was below the minimum threshold. No trade was placed."
    },
    {
      "situation": "Safety gate active \u2014 no trade",
      "message": "A safety condition (stale data, kill switch, or parameter limit) blocked the trade."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "If the crowded narrative reflects real new information rather than sentiment overshoot, the fade position loses as the market price correctly adjusts to the new fundamental.",
    "false_positive_risk": "Signal mis-fires when market conditions change rapidly, producing trades that quickly move against the NarrativeCrowdingFade thesis.",
    "false_negative_risk": "Hard floor set too conservatively misses genuine opportunities.",
    "safe_fallback": "If ws_market feed stale or analytics signal unavailable, skip without emitting any OrderIntent.",
    "required_dependencies": [
      "ws_market",
      "clob_public",
      "internal NarrativeCrowdingFade analytics engine",
      "KillSwitch",
      "internal builder code"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Emit IOC when signal=0.75 and all gates pass",
        "setup": "standard config",
        "expected": "IOC OrderIntent; reason=NCF_TRADE"
      },
      {
        "test": "Skip when signal below hard floor",
        "setup": "signal=below_hard",
        "expected": "No OrderIntent; sampled reason=NCF_NO_EDGE"
      },
      {
        "test": "Skip when KillSwitch active",
        "setup": "killswitch.active=true",
        "expected": "No OrderIntents emitted"
      }
    ],
    "integration": [
      {
        "test": "Full cycle: signal \u2192 computation \u2192 IOC OrderIntent on Polygon testnet",
        "expected": "Order has builder.code, no feeRateBps, EIP-712 domain v2"
      }
    ],
    "property": [
      {
        "property": "Bot never emits OrderIntent when KillSwitch is active",
        "required": "Always true"
      },
      {
        "property": "feeRateBps never present on any signed OrderIntent",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Fade markets where social attention has overshot the underlying base rate.",
  "legacy_pm_signals": [
    "SocialSentiment (4.6) volume and acceleration",
    "Mid drift in the past 24h vs. base-rate prior",
    "Concentration of new buyers in last N hours"
  ],
  "legacy_external_feeds": [
    "Social-sentiment service (Layer 4)"
  ],
  "reporting_groups": [
    "strategy_decision"
  ],
  "reason_codes": [
    {
      "code": "NCF_TRADE",
      "severity": "INFO",
      "meaning": "All gates passed. IOC OrderIntent emitted for NarrativeCrowdingFade.",
      "action": "Emit IOC OrderIntent.",
      "user_message": "A NarrativeCrowdingFade trade was placed."
    },
    {
      "code": "NCF_MARGINAL",
      "severity": "WARN",
      "meaning": "Edge is within the warning threshold; size reduced 50%.",
      "action": "Emit at 50% size; log warning.",
      "user_message": "A small edge was found; a reduced-size NarrativeCrowdingFade trade was placed."
    },
    {
      "code": "NCF_NO_EDGE",
      "severity": "INFO",
      "meaning": "Edge below hard floor. Skipping.",
      "action": "Skip; emit sampled DecisionReport.",
      "user_message": "The edge was too small to justify a trade."
    },
    {
      "code": "NCF_HARD_REJECT",
      "severity": "HARD_REJECT",
      "meaning": "A critical gate condition blocked the trade (stale data, kill switch, or hard parameter breach).",
      "action": "Skip; no OrderIntent.",
      "user_message": "A safety condition blocked the trade."
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Skip all markets; no OrderIntents emitted.",
      "user_message": "Trading is currently paused."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_strat_narrativecrowdingfade_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "verdict",
          "reason_code"
        ],
        "meaning": "Total evaluation cycles by verdict and reason code."
      },
      {
        "name": "polytraders_strat_narrativecrowdingfade_signal_score",
        "type": "histogram",
        "unit": "score",
        "labels": [],
        "meaning": "Distribution of analytics signal scores at evaluation."
      },
      {
        "name": "polytraders_strat_narrativecrowdingfade_intents_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "outcome"
        ],
        "meaning": "Total IOC OrderIntents emitted."
      },
      {
        "name": "polytraders_strat_narrativecrowdingfade_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Latency from signal receipt to OrderIntent emit."
      }
    ],
    "alerts": [
      {
        "name": "NarrativeCrowdingFadeStaleFeed",
        "condition": "rate(polytraders_strat_narrativecrowdingfade_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1",
        "severity": "warn",
        "runbook": "#runbook-narrativecrowdingfade-stale"
      },
      {
        "name": "NarrativeCrowdingFadeKillSwitch",
        "condition": "rate(polytraders_strat_narrativecrowdingfade_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0",
        "severity": "page",
        "runbook": "#runbook-killswitch"
      },
      {
        "name": "NarrativeCrowdingFadeNoEdge",
        "condition": "rate(polytraders_strat_narrativecrowdingfade_decisions_total{verdict='skip',reason_code='NCF_NO_EDGE'}[10m]) / rate(polytraders_strat_narrativecrowdingfade_decisions_total[10m]) > 0.95",
        "severity": "warn",
        "runbook": "#runbook-narrativecrowdingfade-edge"
      }
    ]
  },
  "state": {
    "store": "redis",
    "shape": "Per market: last signal score, last evaluated timestamp; keyed by market_id",
    "ttl": "60s per signal snapshot",
    "recovery": "On cold start, signals rebuilt from next analytics engine poll.",
    "size_estimate": "~150 bytes per tracked market; < 500 KB total"
  },
  "concurrency": {
    "execution_model": "actor-per-market",
    "max_in_flight": 25,
    "idempotency_key": "intent_id",
    "timeout_ms": 300,
    "backpressure": "drop oldest signal per market_id when queue > 2",
    "locking": "per-market_id mutex for signal state"
  },
  "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."
      },
      {
        "bot_id": "gov.builder_attribution",
        "what": "builder.code bytes32 on every OrderIntent."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Polymarket CLOB WebSocket (ws_market)",
        "sla": "best-effort",
        "fallback": "Skip all OrderIntent emission on disconnect."
      },
      {
        "service": "Internal NarrativeCrowdingFade analytics engine",
        "sla": "internal SLA",
        "fallback": "If signal unavailable, treat as below hard floor and skip."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": true,
    "private_key_access": "signing-only",
    "abuse_vectors": [
      "Signal injection to produce false NarrativeCrowdingFade trades",
      "Order sizing parameter manipulation to exceed position limits"
    ],
    "mitigations": [
      "NarrativeCrowdingFade analytics signals sourced from authenticated internal engine only",
      "Hard limits on position size enforced before OrderIntent emission",
      "Builder code injected from secure internal config"
    ]
  },
  "failure_injection": [
    {
      "scenario": "SIGNAL_UNAVAILABLE",
      "how_to_inject": "Cut internal analytics engine connection",
      "expected_behaviour": "No entries; DecisionReport with reason=NCF_HARD_REJECT emitted",
      "recovery": "Automatic when engine reconnects."
    },
    {
      "scenario": "HARD_FLOOR_BREACH",
      "how_to_inject": "Inject signal below hard floor",
      "expected_behaviour": "NCF_NO_EDGE; no OrderIntent",
      "recovery": "Automatic on next valid signal."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behaviour": "No OrderIntents emitted for any market",
      "recovery": "Automatic on manual KillSwitch reset."
    }
  ],
  "runbook": {
    "summary": "NarrativeCrowdingFade incidents are typically stale analytics feeds or kill-switch activations. Hard-floor skips are normal.",
    "oncall_actions": [
      {
        "alert": "NarrativeCrowdingFadeStaleFeed",
        "first_action": "Check internal NarrativeCrowdingFade analytics engine connectivity and feed health.",
        "escalate_to": "Infra on-call if feed lag > 5 min."
      },
      {
        "alert": "NarrativeCrowdingFadeKillSwitch",
        "first_action": "Confirm KillSwitch activation was intentional.",
        "escalate_to": "Risk pod lead immediately."
      },
      {
        "alert": "NarrativeCrowdingFadeNoEdge",
        "first_action": "Review signal calibration; check if market conditions have changed.",
        "escalate_to": "Strategy pod lead if prolonged."
      }
    ],
    "manual_overrides": [
      {
        "name": "exclude_market",
        "how": "Add market_id to config.excluded_markets",
        "when": "Market is showing anomalous conditions that invalidate the NarrativeCrowdingFade signal."
      }
    ],
    "healthcheck": "GET /internal/health/narrativecrowdingfade -> 200 if Analytics engine active; signal age < 60s; KillSwitch inactive.. Red: Analytics engine down or KillSwitch active.."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass including hard-floor skip and kill-switch block",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "p99 eval latency < 300ms over 24h shadow run",
        "how_measured": "polytraders_strat_narrativecrowdingfade_eval_latency_ms histogram",
        "threshold": "p99 < 300ms"
      }
    ],
    "to_general_live": [
      {
        "gate": "E2E: signal \u2192 computation \u2192 IOC OrderIntent on Polygon testnet with builder.code and no feeRateBps",
        "how_measured": "E2E test",
        "threshold": "Pass"
      }
    ]
  },
  "wire_examples": {
    "input": [
      {
        "label": "NarrativeCrowdingFade analytics signal",
        "source": "internal (analytics engine)",
        "payload": {
          "market_id": "0xnarrativ000000000000000000000000000000000000000000000000000000000001",
          "signal_score": "0.75",
          "received_at_ms": 1746790800000
        }
      }
    ],
    "output": [
      {
        "label": "OrderIntent \u2014 NarrativeCrowdingFade IOC buy YES",
        "payload": {
          "intent_id": "oi_01HNCF0000001A",
          "market_id": "0xnarrativ000000000000000000000000000000000000000000000000000000000001",
          "outcome": "YES",
          "side": "buy",
          "price": "0.540",
          "size_pUSD": "200.00",
          "tif": "IOC",
          "builder": {
            "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
            "fee_bps": 25
          },
          "decision": {
            "signal_score": 0.75,
            "reasons": [
              "NCF_TRADE"
            ]
          }
        }
      }
    ]
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION onSignalUpdate(market_id, signal):\n  ks = FETCH internal.killswitch.status\n  IF ks.active: RETURN\n\n  // Hard floor gate\n  IF signal.score < params.min_attention_zscore_hard:\n    IF random() < 0.01:\n      EMIT DecisionReport(intent_emitted=false, reason='NCF_NO_EDGE')\n    RETURN\n\n  mkt = FETCH clob_public.GET('/markets/' + market_id)\n  IF mkt.closed OR mkt.resolved: RETURN\n\n  // Warning threshold check\n  sizeMultiplier = 0.5 IF signal.score < params.min_attention_zscore_warn ELSE 1.0\n  IF sizeMultiplier < 1.0: WARN('NCF_MARGINAL')\n\n  // Book snapshot\n  book = FETCH ws_market.book(market_id)\n  mid = (book.best_bid + book.best_ask) / 2\n  depth = FETCH clob_public.depth(market_id)\n\n  // Size computation\n  orderSize = toPusdUnits(min(params.min_drift_bps * sizeMultiplier, depth.available))\n\n  EMIT OrderIntent(market=market_id, outcome='YES', side='buy', price=mid,\n                   size_pUSD=orderSize, tif='IOC', builder=internalBuilderCode)\n  EMIT DecisionReport(intent_emitted=true, signal_score=signal.score,\n                      reason='NCF_TRADE')",
    "sdk_calls": [
      "ws_market.subscribe('book', [market_id])",
      "fetchClobPublic('/markets/' + market_id)",
      "internal.analyticsEngine.signal(market_id)",
      "buildOrderTypedData(orderParams, {name:'CTFExchange', version:'2', chainId:137})",
      "internal.builder_code"
    ],
    "complexity": "O(1) per signal update per market"
  },
  "api_surface": [
    "clob_public",
    "clob_auth",
    "ws_market",
    "internal"
  ],
  "network": [
    "polygon"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "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": "Bot not yet implemented; designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain). feeRateBps not present on any signed OrderIntent."
  },
  "reporting": {
    "emits_kinds": [
      "DecisionReport"
    ],
    "topics": [
      "polytraders.reports.decision"
    ],
    "cadence": "every-event",
    "retention_class": "2y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "yes",
    "consumes_kinds": [
      "ObservationReport",
      "RiskVote"
    ]
  },
  "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"
  }
}