{
  "schema_version": "1.0.0",
  "bot_id": "4.15",
  "bot_name": "AnomalyDetector",
  "slug": "anomalydetector",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "",
    "runs_after": "",
    "applies_to": "",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Flag statistical anomalies in book, trades, or feed data for human review.",
  "why_it_matters": [
    {
      "failure": "Book or trade anomaly goes unnoticed",
      "consequence": "Without a statistical watchdog, sudden mid-price jumps, volume spikes, or microstructure changes that signal a feed glitch, market manipulation, or upstream halt are detected only when a strategy bot already trades on the bad data. By then unintended fills have already occurred.",
      "worked_example": {
        "setup": "Market 0xab12 (NFL game in progress). Mid-price has sat at 0.62 \u00b1 0.01 for 40 minutes. The CLOB V2 feed delivers a single tick at 0.91 followed by silence for 6 seconds, then resumes at 0.62.",
        "without_bot": "A maker strategy reads 0.91 as the new mid, re-quotes its ladder around 0.91, and gets crossed by the next 0.62 print \u2014 leaving open inventory it never intended to take.",
        "with_bot": "AnomalyDetector emits a z-score=8.4 signal on that tick. Risk widens its staleness window for the market, smartrouter blocks new entries for 10s, and the maker bot keeps its prior ladder."
      }
    },
    {
      "failure": "No baseline for human review",
      "consequence": "Operators reviewing a market in incident mode have no reference for what 'normal' looked like for that market in the prior hour. Anomaly emissions provide the timestamped baseline that every post-incident review needs."
    },
    {
      "failure": "Z-score signal duplicated across strategies",
      "consequence": "If anomaly detection lives inside individual strategy bots, every team reinvents it inconsistently. A single shared service emits one canonical anomaly stream that all consumers \u2014 Risk, Strategy, Governance \u2014 read from."
    },
    {
      "failure": "Strategy bots blind to feed degradation",
      "consequence": "A z-score spike often precedes a stale-book event by seconds. Without an explicit anomaly signal, downstream Risk bots have no early warning to widen their staleness windows or pause new entries."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "CLOB V2 order book mid-price per condition_id",
      "source": "clob_public",
      "required": true,
      "use": "Compute price z-score vs rolling baseline."
    },
    {
      "input": "Recent trade volume per condition_id",
      "source": "data",
      "required": true,
      "use": "Compute volume z-score vs rolling baseline."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all anomaly emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "z_threshold \u00b7 float",
    "min_baseline_window_h \u00b7 int",
    "publish_to \u00b7 list",
    "auto_pause_strategies \u00b7 list"
  ],
  "parameters": [
    {
      "name": "z_score_threshold",
      "default": 3.0,
      "warning": 2.0,
      "hard": 1.0,
      "controls": "Minimum z-score (standard deviations from rolling mean) to trigger an anomaly signal.",
      "why_default_matters": "3.0 sigma suppresses noise from normal market volatility while catching genuine outlier events.",
      "threshold_logic": [
        {
          "condition": "z >= 3.0",
          "action": "ANOMALYDETECTOR_PRICE_SPIKE or VOLUME_SPIKE; emit ObservationReport"
        },
        {
          "condition": "2.0\u20133.0",
          "action": "WARN \u2014 borderline anomaly; emit with low_confidence flag"
        },
        {
          "condition": "< 1.0 (hard floor)",
          "action": "Reject \u2014 below minimum anomaly sensitivity; PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.z_score_threshold < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Statistical anomalies are only reported when market behaviour deviates significantly from the historical baseline."
    },
    {
      "name": "baseline_window_s",
      "default": 3600,
      "warning": 7200,
      "hard": 300,
      "controls": "Rolling window in seconds used to compute mean and standard deviation for z-score baseline.",
      "why_default_matters": "3600 s (1 h) captures intraday price patterns without including multi-day regime shifts in the baseline.",
      "threshold_logic": [
        {
          "condition": "window >= 3600 s",
          "action": "Normal"
        },
        {
          "condition": "300\u20133600 s",
          "action": "WARN \u2014 baseline may be noisy with short window"
        },
        {
          "condition": "< 300 s",
          "action": "Reject \u2014 insufficient baseline; emit ANOMALYDETECTOR_INSUFFICIENT_BASELINE"
        }
      ],
      "dev_check": "if (p.baseline_window_s < p.hard) emit('ANOMALYDETECTOR_INSUFFICIENT_BASELINE');",
      "user_facing": "The anomaly baseline uses a rolling historical window to adapt to normal market conditions."
    }
  ],
  "default_config": {
    "bot_id": "intel.anomalydetector",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "z_score_threshold": 3.0,
      "baseline_window_s": 3600,
      "sample_rate": 10
    },
    "locked": {
      "z_score_threshold": {
        "min": 1.0
      },
      "baseline_window_s": {
        "min": 300
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_ad_0xf1a2_1746703000000",
    "trace_id": "trc_0xbeef0102030405060716",
    "bot_id": "intel.anomalydetector",
    "kind": "ObservationReport",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "anomaly_detected": true,
    "z_price": 3.8,
    "z_vol": 1.2,
    "warnings": [
      "ANOMALYDETECTOR_PRICE_SPIKE"
    ],
    "emitted_at_ms": 1746703005000
  },
  "developer_log": {
    "bot_id": "intel.anomalydetector",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "z_price": 3.8,
    "z_vol": 1.2,
    "anomaly_detected": true,
    "baseline_sample_count": 120,
    "killswitch_active": false,
    "emitted_at_ms": 1746703005000
  },
  "user_explanations": [
    {
      "situation": "Strategy paused on a market after price anomaly detected",
      "message": "An unusual price movement was detected on this market that is statistically significant compared to recent history. The system paused new entries pending market stabilisation."
    },
    {
      "situation": "No anomaly detected despite market movement",
      "message": "The recent price movement was within the normal statistical range based on the last hour of market data. No anomaly signal was generated."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "CLOB API outage prevents fresh price data from being fetched, causing AnomalyDetector to serve stale z-scores and miss genuine anomalies during the outage period.",
    "false_positive_risk": "Thin book with high bid-ask spread causes mid-price jumps that generate price z-scores above threshold despite no meaningful price discovery event.",
    "false_negative_risk": "Slow-moving price manipulation that stays within 3 sigma of the rolling baseline evades the anomaly detector entirely until the baseline itself shifts.",
    "safe_fallback": "If CLOB API or Data API is unavailable, emit STALE_DATA WARN and halt anomaly detection. Do not emit from stale z-scores. Resume on API recovery with fresh baseline computation.",
    "required_dependencies": [
      "Polymarket CLOB V2 public API",
      "Polymarket Data API for trade volume",
      "KillSwitch active flag",
      "Redis for rolling baseline state"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Price z-score above threshold emits ObservationReport with anomaly_detected=true",
        "setup": "mid_price 5 sigma above rolling mean, baseline_window=3600 s",
        "expected": "ObservationReport emitted with z_price=5.0, anomaly_detected=true, ANOMALYDETECTOR_PRICE_SPIKE"
      },
      {
        "test": "Price within threshold emits sampled ObservationReport with anomaly_detected=false",
        "setup": "mid_price 0.5 sigma above rolling mean; sample_rate=10",
        "expected": "ObservationReport emitted every 10th cycle with anomaly_detected=false"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; anomaly present",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Anomaly detected, emitted, consumed by LiquidityGuard for pre-trade gate",
        "expected": "LiquidityGuard receives ObservationReport with anomaly_detected=true before strategy entry"
      },
      {
        "test": "CLOB API down: STALE_DATA emitted; no anomaly checks run",
        "expected": "STALE_DATA WARN; AnomalyDetectorStale alert fires; no ObservationReports"
      }
    ],
    "property": [
      {
        "property": "AnomalyDetector never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Flag statistical anomalies in book, trades, or feed data for human review.",
  "legacy_pm_signals": [
    "Trade-imbalance spikes vs. rolling baseline",
    "Wash-trade signatures and self-cross rates",
    "Feed-vs-source divergence beyond expected noise"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "data",
    "onchain",
    "internal"
  ],
  "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": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Detects statistical anomalies in CLOB V2 price, volume, and on-chain flow data. Read-only. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION detectAnomalies(condition_id):\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch current price and volume\n  book = FETCH clob_public.GET('/book?token_id=' + condition_id)\n  trades = FETCH data.GET('/trades?condition_id=' + condition_id + '&window=300')\n\n  // 2. Compute z-score vs rolling baseline\n  mid = (book.best_bid + book.best_ask) / 2\n  baseline = state.get_rolling_stats(condition_id)\n  z_price = (mid - baseline.mean_price) / baseline.std_price\n  z_vol = (volume(trades) - baseline.mean_vol) / baseline.std_vol\n\n  // 3. Apply anomaly thresholds\n  anomaly = False\n  IF abs(z_price) > z_score_threshold:\n    EMIT WARN 'ANOMALYDETECTOR_PRICE_SPIKE'\n    anomaly = True\n  IF abs(z_vol) > z_score_threshold:\n    EMIT WARN 'ANOMALYDETECTOR_VOLUME_SPIKE'\n    anomaly = True\n\n  // 4. Sample gate\n  IF sample_counter % sample_rate != 0 AND NOT anomaly:\n    sample_counter++\n    RETURN\n\n  // 5. Emit ObservationReport\n  EMIT ObservationReport {\n    report_id: gen_id(),\n    kind: 'ObservationReport',\n    condition_id: condition_id,\n    anomaly_detected: anomaly,\n    z_price: z_price,\n    z_vol: z_vol,\n    emitted_at_ms: now_ms()\n  }\n  sample_counter = 0",
    "sdk_calls": [
      "clob_public.GET('/book?token_id=<condition_id>')",
      "data.GET('/trades?condition_id=<condition_id>&window=300')",
      "internal.killswitch.status"
    ],
    "complexity": "O(1) per market per poll cycle with O(W) rolling window state update"
  },
  "wire_examples": {
    "input": {
      "label": "CLOB V2 order book and recent trades for anomaly detection",
      "source": "clob_public",
      "payload": {
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "best_bid": "0.82",
        "best_ask": "0.84",
        "recent_trade_volume_pusd": 45000,
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 price spike anomaly detected",
      "payload": {
        "report_id": "rep_ad_0xf1a2_1746703000000",
        "trace_id": "trc_0xbeef0102030405060716",
        "bot_id": "intel.anomalydetector",
        "kind": "ObservationReport",
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "anomaly_detected": true,
        "z_price": 3.8,
        "z_vol": 1.2,
        "emitted_at_ms": 1746703005000
      }
    }
  },
  "reason_codes": [
    {
      "code": "ANOMALYDETECTOR_PRICE_SPIKE",
      "severity": "WARN",
      "meaning": "Price z-score exceeds z_score_threshold relative to rolling baseline.",
      "action": "Emit ObservationReport with anomaly_detected=true; strategies may reduce size or pause.",
      "user_message": "Unusual price movement detected on this market."
    },
    {
      "code": "ANOMALYDETECTOR_VOLUME_SPIKE",
      "severity": "WARN",
      "meaning": "Volume z-score exceeds z_score_threshold relative to rolling baseline.",
      "action": "Emit ObservationReport with anomaly_detected=true; strategies monitor for continuation.",
      "user_message": "Unusual trading volume detected on this market."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "CLOB API or Data API is unavailable; anomaly detection paused.",
      "action": "Skip detection cycle; retry on next poll.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all AnomalyDetector emissions suppressed.",
      "action": "Continue anomaly detection internally; suppress all ObservationReport emissions.",
      "user_message": "Anomaly detection signals paused while trading is suspended system-wide."
    },
    {
      "code": "ANOMALYDETECTOR_INSUFFICIENT_BASELINE",
      "severity": "INFO",
      "meaning": "Rolling baseline has insufficient data points to compute reliable z-scores.",
      "action": "Skip anomaly check for this market until baseline window is populated.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_anomalydetector_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "anomaly_detected"
        ],
        "meaning": "ObservationReports emitted, broken down by anomaly_detected (true/false)."
      },
      {
        "name": "polytraders_intel_anomalydetector_anomalies_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "anomaly_type"
        ],
        "meaning": "Total anomaly events by type (price_spike, volume_spike)."
      },
      {
        "name": "polytraders_intel_anomalydetector_z_score",
        "type": "gauge",
        "unit": "sigma",
        "labels": [
          "condition_id",
          "metric"
        ],
        "meaning": "Latest z-score for price and volume per tracked market."
      },
      {
        "name": "polytraders_intel_anomalydetector_baseline_age_s",
        "type": "gauge",
        "unit": "seconds",
        "labels": [
          "condition_id"
        ],
        "meaning": "Age of the rolling baseline in seconds; high values indicate stale baseline."
      }
    ],
    "alerts": [
      {
        "name": "AnomalyDetectorPriceSpike",
        "condition": "rate(polytraders_intel_anomalydetector_anomalies_total{anomaly_type='price_spike'}[5m]) > 0",
        "severity": "warn",
        "runbook": "#runbook-anomalydetector-price-spike"
      },
      {
        "name": "AnomalyDetectorStale",
        "condition": "rate(polytraders_intel_anomalydetector_observations_emitted_total[10m]) == 0",
        "severity": "warn",
        "runbook": "#runbook-anomalydetector-stale"
      },
      {
        "name": "AnomalyDetectorHighZScore",
        "condition": "polytraders_intel_anomalydetector_z_score > 5",
        "severity": "page",
        "runbook": "#runbook-anomalydetector-high-zscore"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / AnomalyDetector z-score heatmap by market",
      "Grafana \u2014 Intelligence / AnomalyDetector anomaly rate over time"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Per condition_id: rolling_mean_price, rolling_std_price, rolling_mean_vol, rolling_std_vol, baseline_sample_count, last_updated_at_ms.",
    "ttl": "Per-market baseline expires after 24 h of inactivity; rebuilt on next cycle",
    "recovery": "On cold start, baseline rebuilt from next N poll cycles; anomaly detection suppressed until baseline is populated.",
    "size_estimate": "~3 KB per tracked market"
  },
  "concurrency": {
    "execution_model": "async per-market poll loop",
    "max_in_flight": 30,
    "idempotency_key": "condition_id + poll_cycle_ms",
    "timeout_ms": 8000,
    "backpressure": "drop-after-buffer \u2014 skip markets that have not drained within 2x poll interval",
    "locking": "Redis SETNX on condition_id + cycle to prevent duplicate detection runs"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.all_strategies",
        "what": "ObservationReport with anomaly_detected, z_price, z_vol for market-level risk awareness"
      },
      {
        "bot_id": "risk.liquidity_guard",
        "what": "ObservationReport with anomaly_detected for pre-trade risk gating on anomalous markets"
      }
    ],
    "sibling": [
      "intel.liquidityforecastmodel"
    ],
    "external": [
      {
        "service": "Polymarket CLOB V2 (public book)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.9% / 200 ms p99",
        "fallback": "Emit STALE_DATA if book unavailable; skip detection for this cycle"
      },
      {
        "service": "Polymarket Data API",
        "endpoint": "https://data-api.polymarket.com",
        "sla": "99.9% / 300 ms p99",
        "fallback": "Skip volume z-score if data API unavailable; price anomaly only"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Adversary places and cancels orders to inflate z-scores and trigger false anomaly signals causing strategy pauses",
      "Coordinated thin-book manipulation causes persistent high z-scores on a target market"
    ],
    "mitigations": [
      "z_score_threshold tuned to suppress noise from normal spread fluctuations",
      "Anomalies are informational only \u2014 strategies apply independent validation before pausing"
    ]
  },
  "failure_injection": [
    {
      "scenario": "CLOB_API_DOWN",
      "how_to_inject": "Block CLOB public API for 10 min",
      "expected_behaviour": "STALE_DATA WARN; no anomaly checks; AnomalyDetectorStale alert fires",
      "recovery": "Automatic on API recovery; baseline resumes updating on next poll"
    },
    {
      "scenario": "SYNTHETIC_PRICE_SPIKE",
      "how_to_inject": "Inject mock mid-price 5 standard deviations above baseline for a test market",
      "expected_behaviour": "ANOMALYDETECTOR_PRICE_SPIKE WARN; ObservationReport emitted with anomaly_detected=true; AnomalyDetectorPriceSpike alert fires",
      "recovery": "Automatic when price returns to normal range"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during active detection",
      "expected_behaviour": "Detection continues internally; emissions suppressed; KILL_SWITCH_ACTIVE logged",
      "recovery": "Automatic on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "AnomalyDetector incidents include genuine market anomalies (informational), API outages, and high z-scores requiring strategy team attention. Page on z-score > 5.",
    "oncall_actions": [
      {
        "alert": "AnomalyDetectorHighZScore",
        "first_step": "Identify condition_id from alert. Check live price and order book on Polymarket.com. Notify strategy team if z_score > 5.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead immediately; strategy team on-call for active positions"
      },
      {
        "alert": "AnomalyDetectorStale",
        "first_step": "Check CLOB API and Data API health. Verify last_updated_at_ms for affected markets.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if stale > 10 min"
      },
      {
        "alert": "AnomalyDetectorPriceSpike",
        "first_step": "Verify on Polymarket.com. If genuine price movement, no action needed \u2014 downstream strategies consume signal. If book manipulation suspected, alert strategy team.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Strategy team on-call if anomaly persists > 5 min"
      }
    ],
    "manual_overrides": [
      {
        "command": "reset_baseline",
        "effect": "DEL redis key baseline:<condition_id> to reset rolling baseline for a specific market; next N polls rebuild it \u2014 After known structural break in market (e.g. major news event resetting price regime)"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/anomalydetector | Green: Last detection < 2x poll_interval_s AND Redis reachable AND CLOB + Data APIs returning 200 | Red: No detection for > 10 min OR Redis unreachable OR CLOB API down > 5 min"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for price spike, volume spike, insufficient baseline, and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "False-positive rate < 1% over 48 h on staging with live market data",
        "how_measured": "anomaly_detected=true rate vs manual audit of price charts",
        "threshold": "< 1% false positives"
      }
    ],
    "to_general_live": [
      {
        "gate": "100% detection of synthetic price and volume spikes (5-sigma) over 7-day soak",
        "how_measured": "Integration test log audit",
        "threshold": "100% detection"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "cadence": "every-N",
    "retention_class": "30d",
    "retention_notes": "Full fidelity for 30 d; rolled-up summary retained for 1 y",
    "sampling_rule": "sample-1/10",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "no",
    "consumes_kinds": []
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 2,
    "phase_name": "Data normalisation",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}