{
  "schema_version": "1.0.0",
  "bot_id": "1.11",
  "bot_name": "CorrelationShockGuard",
  "slug": "correlationshockguard",
  "layer": "Risk",
  "layer_key": "risk",
  "bot_class": "Guardrail",
  "authority": [
    "Veto",
    "Reshape"
  ],
  "status": "planned",
  "readiness": "Planned",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Risk",
    "bot_class": "Guardrail",
    "authority": "Veto, Reshape",
    "runs_before": "ExecutionPlan emit",
    "runs_after": "Strategy OrderIntent",
    "applies_to": "Every OrderIntent \u2014 detects when the portfolio's supposedly independent positions are moving in lockstep, indicating a hidden correlation shock",
    "default_mode": "planned",
    "user_visible": "summary-only",
    "developer_owner": "Polytraders core \u2014 Risk pod"
  },
  "purpose": "CorrelationShockGuard monitors the rolling pairwise return correlation of open positions and blocks new orders when the portfolio correlation exceeds the configured ceiling. It detects sudden hidden correlation shocks where previously independent markets begin resolving together, increasing tail risk beyond what the capital model assumes.",
  "why_it_matters": [
    {
      "failure": "Hidden correlation shock",
      "consequence": "Markets that appeared independent begin resolving together (e.g. macro shock, shared underlying fact), transforming the portfolio's diversification into concentrated tail risk with no warning."
    },
    {
      "failure": "New order added during correlation spike",
      "consequence": "Adding a new position while portfolio correlation is elevated amplifies the shock exposure instead of providing diversification."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market price series for all open positions",
      "source": "data_api",
      "required": true,
      "use": "Compute rolling pairwise correlations across positions over a configurable lookback window."
    },
    {
      "input": "Market category and topic tags",
      "source": "gamma",
      "required": false,
      "use": "Group markets by topic to detect within-group correlation that may indicate shared resolution events."
    }
  ],
  "internal_inputs": [
    {
      "input": "Current open position list and notional values",
      "source": "internal",
      "required": true,
      "use": "Identify which markets to include in the correlation calculation."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "If active, reject immediately."
    }
  ],
  "raw_params": [
    "max_portfolio_correlation \u00b7 float",
    "lookback_periods \u00b7 int",
    "min_positions_to_check \u00b7 int"
  ],
  "parameters": [
    {
      "name": "max_portfolio_correlation",
      "default": 0.6,
      "warning": 0.45,
      "hard": 0.6,
      "controls": "Maximum allowed average pairwise return correlation across open positions before new orders are blocked.",
      "why_default_matters": "A portfolio-average correlation above 0.6 indicates positions are behaving as a single concentrated bet rather than a diversified set.",
      "threshold_logic": [
        {
          "condition": "avg_corr <= 0.45",
          "action": "APPROVE"
        },
        {
          "condition": "0.45 < avg_corr <= 0.6",
          "action": "WARN \u2014 CORRELATION_SHOCK_APPROACHING"
        },
        {
          "condition": "avg_corr > 0.6",
          "action": "HARD_REJECT \u2014 CORRELATION_SHOCK_DETECTED"
        }
      ],
      "dev_check": "if (avgCorr > params.max_portfolio_correlation) return reject('CORRELATION_SHOCK_DETECTED');",
      "user_facing": "Your portfolio positions are currently highly correlated. New orders are blocked until correlation normalises."
    },
    {
      "name": "lookback_periods",
      "default": 20,
      "warning": null,
      "hard": null,
      "controls": "Number of price-update periods over which pairwise correlations are computed. Shorter windows detect shocks faster; longer windows reduce false positives.",
      "why_default_matters": "20 periods balances responsiveness (detecting a new shock within minutes) with stability (not rejecting on single-period noise).",
      "threshold_logic": [
        {
          "condition": "always",
          "action": "Use configured lookback; no reject threshold"
        }
      ],
      "dev_check": "corrMatrix = computeRollingCorr(priceSeries, params.lookback_periods);",
      "user_facing": ""
    },
    {
      "name": "min_positions_to_check",
      "default": 3,
      "warning": null,
      "hard": null,
      "controls": "Minimum number of open positions required before the correlation check is applied. Below this threshold, the check is skipped (not enough data for meaningful correlation).",
      "why_default_matters": "With fewer than 3 positions, pairwise correlation is trivial and may produce misleading values.",
      "threshold_logic": [
        {
          "condition": "open_positions < min_positions_to_check",
          "action": "SKIP check \u2014 APPROVE"
        },
        {
          "condition": "open_positions >= min_positions_to_check",
          "action": "Run correlation check"
        }
      ],
      "dev_check": "if (openPositions.length < params.min_positions_to_check) return approve('CORRELATION_SHOCK_SKIPPED');",
      "user_facing": ""
    }
  ],
  "default_config": {
    "bot_id": "risk.correlation_shock_guard",
    "version": "0.1.0",
    "mode": "hard_guard",
    "defaults": {
      "max_portfolio_correlation": 0.6,
      "lookback_periods": 20,
      "min_positions_to_check": 3
    },
    "locked": {
      "max_portfolio_correlation": {
        "max": 0.8
      }
    }
  },
  "implementation_flow": [
    "Receive OrderIntent with market_id and strategy context.",
    "Check KillSwitch; if active, HARD_REJECT(KILL_SWITCH_ACTIVE).",
    "Load open position list and price series for each from data_api.",
    "If open positions < min_positions_to_check, APPROVE immediately (skip).",
    "Compute rolling pairwise return correlations over lookback_periods.",
    "Compute average pairwise correlation across the matrix.",
    "If avg_corr > max_portfolio_correlation.hard, HARD_REJECT(CORRELATION_SHOCK_DETECTED).",
    "If avg_corr > max_portfolio_correlation.warning, attach WARN annotation; APPROVE.",
    "All checks passed \u2014 APPROVE with correlation_score attached."
  ],
  "decision_logic": {
    "approve": "Portfolio average correlation is at or below the warning threshold, or fewer than min_positions_to_check are open.",
    "reshape_required": "Not used; correlation is a portfolio-level metric that cannot be partially accommodated by resizing a single order.",
    "reject": "Average pairwise correlation exceeds the hard ceiling, indicating a correlation shock in the open portfolio."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "guard_id": "risk.correlation_shock_guard",
    "decision": "HARD_REJECT",
    "severity": "HARD",
    "reason_code": "CORRELATION_SHOCK_DETECTED",
    "message": "Portfolio avg pairwise correlation 0.72 exceeds hard ceiling 0.60. New orders blocked.",
    "constraints": {},
    "inputs_used": [
      "data_api.price_series",
      "internal.positions",
      "internal.killswitch.status"
    ],
    "checked_at": "2026-05-10T10:00:00Z"
  },
  "developer_log": {
    "bot_id": "risk.correlation_shock_guard",
    "decision": "HARD_REJECT",
    "reason_code": "CORRELATION_SHOCK_DETECTED",
    "inputs_used": [
      "data_api.price_series",
      "internal.positions"
    ],
    "metrics": {
      "avg_pairwise_corr": 0.72,
      "num_positions": 5,
      "lookback_periods": 20,
      "hard_ceiling": 0.6
    },
    "checked_at": "2026-05-10T10:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Order blocked \u2014 correlation shock",
      "message": "Your open positions are currently moving together more than expected, indicating a correlated market event. New orders are blocked until correlation normalises."
    },
    {
      "situation": "Warning \u2014 correlation approaching ceiling",
      "message": "Your portfolio correlation is elevated. Consider reviewing your open positions before adding new ones."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Missing price data for one or more positions causes the correlation matrix to be computed on a subset, potentially underestimating the true correlation.",
    "false_positive_risk": "A transient data feed spike for one market artificially inflates its correlation with all others, triggering a spurious block.",
    "false_negative_risk": "Correlation develops slowly over many periods and avg_corr approaches but never crosses the hard ceiling while the portfolio remains concentrated.",
    "safe_fallback": "If price series data is unavailable for any open position, HARD_REJECT with CORRELATION_SHOCK_DATA_UNAVAILABLE. Never approve on incomplete correlation data.",
    "required_dependencies": [
      "Data API price series",
      "Open position ledger",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when correlation below warning threshold",
        "setup": "avg_corr=0.30, positions=4",
        "expected": "APPROVE"
      },
      {
        "test": "Warn when correlation between warning and hard",
        "setup": "avg_corr=0.50, positions=4",
        "expected": "APPROVE with WARN annotation"
      },
      {
        "test": "Reject when correlation above hard ceiling",
        "setup": "avg_corr=0.72, positions=4",
        "expected": "HARD_REJECT(CORRELATION_SHOCK_DETECTED)"
      },
      {
        "test": "Skip when fewer than min_positions_to_check",
        "setup": "open_positions=2, min=3",
        "expected": "APPROVE (check skipped)"
      }
    ],
    "integration": [
      {
        "test": "Correlation spike detected from live price data",
        "expected": "HARD_REJECT(CORRELATION_SHOCK_DETECTED) within one evaluation cycle of avg_corr exceeding ceiling"
      },
      {
        "test": "KillSwitch bypasses all correlation checks",
        "expected": "HARD_REJECT(KILL_SWITCH_ACTIVE) without reading price series"
      }
    ],
    "property": [
      {
        "property": "Avg correlation above hard ceiling never results in APPROVE",
        "required": "Always true"
      },
      {
        "property": "Fewer than min_positions always results in APPROVE (skip)",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Detect when supposedly independent positions start moving as one.",
  "legacy_pm_signals": [
    "Rolling correlation matrix across open positions (returns, mark moves)",
    "Cluster topology from CrossMarketGraph (4.4)",
    "Synchronised mid-price moves across >N markets"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "risk_compliance"
  ],
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch active.",
      "action": "Immediate HARD_REJECT.",
      "user_message": "Trading is paused. Please try again later."
    },
    {
      "code": "CORRELATION_SHOCK_DETECTED",
      "severity": "HARD_REJECT",
      "meaning": "Portfolio average pairwise correlation exceeds the hard ceiling.",
      "action": "HARD_REJECT; log avg_corr, num_positions, and lookback_periods.",
      "user_message": "Your portfolio positions are highly correlated. New orders are blocked."
    },
    {
      "code": "CORRELATION_SHOCK_APPROACHING",
      "severity": "WARN",
      "meaning": "Portfolio correlation is between the warning and hard thresholds.",
      "action": "Attach WARN annotation to APPROVE; do not block.",
      "user_message": ""
    },
    {
      "code": "CORRELATION_SHOCK_DATA_UNAVAILABLE",
      "severity": "HARD_REJECT",
      "meaning": "Price series data unavailable for one or more positions.",
      "action": "HARD_REJECT (fail-closed).",
      "user_message": "We could not retrieve market data needed to check portfolio correlation. Please try again."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_risk_correlationshockguard_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code"
        ],
        "meaning": "Total RiskVote decisions by type."
      },
      {
        "name": "polytraders_risk_correlationshockguard_avg_correlation",
        "type": "gauge",
        "unit": "ratio",
        "labels": [],
        "meaning": "Current portfolio average pairwise correlation (0\u20131) at last evaluation."
      },
      {
        "name": "polytraders_risk_correlationshockguard_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Latency from intent receipt to RiskVote emit."
      }
    ],
    "alerts": [
      {
        "name": "CorrelationShockGuardTriggered",
        "condition": "rate(polytraders_risk_correlationshockguard_decisions_total{reason_code='CORRELATION_SHOCK_DETECTED'}[5m]) > 0",
        "severity": "P2",
        "runbook": "#runbook-correlationshock-triggered"
      },
      {
        "name": "CorrelationShockGuardDataUnavailable",
        "condition": "rate(polytraders_risk_correlationshockguard_decisions_total{reason_code='CORRELATION_SHOCK_DATA_UNAVAILABLE'}[5m]) > 0",
        "severity": "P1",
        "runbook": "#runbook-correlationshock-data"
      }
    ]
  },
  "state": {
    "store": "in-memory",
    "shape": "Rolling price series buffer per market_id keyed by user_id, sized at lookback_periods entries.",
    "ttl": "Rolling window evicts entries older than lookback_periods.",
    "recovery": "On cold start, price series is populated from data_api before first evaluation. Insufficient data causes check to be skipped.",
    "size_estimate": "~2 KB per market per 20-period lookback"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 100,
    "idempotency_key": "intent_id",
    "timeout_ms": 200,
    "backpressure": "drop newest",
    "locking": "per-user_id mutex during correlation matrix computation"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Global brake checked first.",
        "contract": "HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all evaluation."
      }
    ],
    "emits_to": [
      {
        "bot_id": "exec.smart_router",
        "why": "Approved RiskVote passes to SmartRouter.",
        "contract": "APPROVE passes; HARD_REJECT discards intent."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Data API (price series)",
        "endpoint": "https://data-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "HARD_REJECT(CORRELATION_SHOCK_DATA_UNAVAILABLE) if data unavailable."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Injecting manipulated price data to artificially lower apparent correlation",
      "Submitting orders rapidly before correlation spike is detected"
    ],
    "mitigations": [
      "Price series sourced exclusively from data_api with provenance timestamp validation",
      "Idempotency deduplication prevents rapid concurrent submissions from the same user"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Position prices are denominated in pUSD. Uses data_api V2 price series; no order signing."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q4-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)"
    }
  ],
  "reference_implementation": {
    "pseudocode": "FUNCTION evaluateCorrelation(intent):\n  ks = FETCH internal.killswitch.status\n  IF ks.active: EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN\n\n  positions = FETCH internal.open_positions(intent.user_id)\n  IF len(positions) < params.min_positions_to_check:\n    EMIT RiskVote(APPROVE); RETURN\n\n  priceSeries = {}\n  FOR pos IN positions:\n    series = FETCH data_api.price_series(pos.market_id, params.lookback_periods)\n    IF series IS NULL:\n      EMIT RiskVote(HARD_REJECT, CORRELATION_SHOCK_DATA_UNAVAILABLE); RETURN\n    priceSeries[pos.market_id] = series\n\n  returns = {m: diff(priceSeries[m]) FOR m IN priceSeries}\n  corrMatrix = pairwise_corr(returns)\n  avgCorr = mean(upper_triangle(corrMatrix))\n\n  IF avgCorr > params.max_portfolio_correlation:\n    EMIT RiskVote(HARD_REJECT, CORRELATION_SHOCK_DETECTED,\n                  avg_corr=avgCorr); RETURN\n  IF avgCorr > params.max_portfolio_correlation * 0.75:\n    annotations.append(WARN(CORRELATION_SHOCK_APPROACHING))\n\n  EMIT RiskVote(APPROVE, correlation_score=avgCorr)",
    "sdk_calls": [
      "data_api.price_series(market_id, lookback)",
      "internal.open_positions(user_id)",
      "internal.killswitch.status()"
    ],
    "complexity": "O(N^2) where N = number of open positions (max ~20)"
  },
  "wire_examples": {
    "input": [
      {
        "label": "OrderIntent \u2014 correlation shock active",
        "source": "internal",
        "payload": {
          "intent_id": "int_c3d4e5f6a7b80003",
          "market_id": "0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c",
          "size_usd": 300,
          "generated_at_ms": 1746800000000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 HARD_REJECT",
        "payload": {
          "guard_id": "risk.correlation_shock_guard",
          "decision": "HARD_REJECT",
          "severity": "HARD",
          "reason_code": "CORRELATION_SHOCK_DETECTED",
          "message": "Avg pairwise correlation 0.72 exceeds ceiling 0.60.",
          "constraints": {},
          "checked_at": "2026-05-10T10:00:00Z"
        }
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "PRICE_DATA_UNAVAILABLE",
      "how_to_inject": "Block data_api endpoint for one market in the portfolio",
      "expected_behaviour": "HARD_REJECT(CORRELATION_SHOCK_DATA_UNAVAILABLE)",
      "recovery": "Returns to normal within one evaluation cycle after data_api is reachable."
    },
    {
      "scenario": "CORRELATION_SPIKE",
      "how_to_inject": "Inject price series with near-identical returns for all positions (corr=0.9)",
      "expected_behaviour": "HARD_REJECT(CORRELATION_SHOCK_DETECTED) with avg_corr > 0.6",
      "recovery": "Returns to APPROVE once price series diverge below the ceiling."
    },
    {
      "scenario": "INSUFFICIENT_POSITIONS",
      "how_to_inject": "Set open positions to 2 when min_positions_to_check=3",
      "expected_behaviour": "APPROVE (check skipped)",
      "recovery": "Check activates once a 3rd position is opened."
    }
  ],
  "runbook": {
    "summary": "CorrelationShockGuard incidents are typically caused by a macro market event driving correlated resolution across multiple markets. Verify the correlation is genuine before pausing the guard.",
    "oncall_actions": [
      {
        "alert": "CorrelationShockGuardTriggered",
        "first_step": "Inspect the avg_corr metric and identify which markets are correlated. Check if a macro event is driving the correlation.",
        "escalation": "Risk pod lead; consider pausing affected strategies if shock is genuine.",
        "diagnosis": "",
        "mitigation": ""
      },
      {
        "alert": "CorrelationShockGuardDataUnavailable",
        "first_step": "Check data_api connectivity; confirm price series endpoint is responding.",
        "escalation": "Infra on-call if data_api is down > 5 minutes.",
        "diagnosis": "",
        "mitigation": ""
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders risk disable-check risk.correlation_shock_guard --duration 300s",
        "effect": "During a known data feed issue causing false data unavailability rejections; requires risk pod lead approval."
      }
    ],
    "healthcheck": "GET /internal/health/correlationshockguard \u2192 green: data_api reachable, price series populated for all tracked positions, avg_corr below warning threshold; red: data_api unreachable, price series stale > lookback_periods, or avg_corr above hard ceiling"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for correlation spike and skip scenarios",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow reject rate from genuine correlation events within 5% of expected over 48h",
        "how_measured": "Grafana shadow vs live",
        "threshold": "< 5% divergence"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero DATA_UNAVAILABLE rejections during normal hours over 7 days",
        "how_measured": "CorrelationShockGuardDataUnavailable alert history",
        "threshold": "0 firings"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "RiskVote"
    ],
    "topics": [
      "polytraders.reports.risk"
    ],
    "partition_by": "trace_id",
    "cadence": "every-event",
    "retention_class": "2y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ObservationReport"
    ]
  },
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 4,
    "phase_name": "Core risk",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}