{
  "schema_version": "1.0.0",
  "bot_id": "6.7",
  "bot_name": "Portfolio Sync",
  "slug": "portfolio-sync",
  "layer": "Governance",
  "layer_key": "gov",
  "bot_class": "Governance Service",
  "authority": [
    "Explain"
  ],
  "status": "live",
  "readiness": "General live",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Governance",
    "bot_class": "Governance Service",
    "authority": "Explain",
    "runs_before": "PnLReporter (provides position state), PortfolioGuard (provides current exposure for risk checks)",
    "runs_after": "Every fill event; on configured sync_interval_s; on-demand after significant position change",
    "applies_to": "All open positions and open orders across all active markets, including negative-risk multi-outcome positions",
    "default_mode": "general_live",
    "user_visible": "Summary only",
    "developer_owner": "Polytraders core \u2014 Governance pod"
  },
  "purpose": "PortfolioSync keeps the in-memory portfolio state \u2014 open positions and open orders \u2014 continuously synchronised with on-chain pUSD balances and CLOB open-order state. It reconciles the internal position store against clob_auth open orders and the Polygon on-chain balance every sync_interval_s. For negative-risk markets (NegRiskAdapter), it correctly aggregates multi-outcome positions using the negRisk flag. On discrepancy beyond discrepancy_alert_usd, it emits an alert and, if configured, pauses the affected strategy to prevent trading against a stale view of risk. Emits both a SettlementReport (position deltas) and an OperationsReport (sync health) on every cycle.",
  "why_it_matters": [
    {
      "failure": "In-memory position state drifts from on-chain reality",
      "consequence": "Strategies trade against a stale position view. Risk guards (PortfolioGuard, KillSwitch) see incorrect exposure, potentially allowing positions that exceed risk limits."
    },
    {
      "failure": "Negative-risk positions aggregated incorrectly",
      "consequence": "Multi-outcome positions in negRisk markets have correlated payoffs. Treating them as independent binary positions overstates available margin and may cause liquidation."
    },
    {
      "failure": "Open-order count diverges from CLOB",
      "consequence": "Strategies submit orders assuming capacity that no longer exists. This inflates fill expectations and distorts P&L projections."
    },
    {
      "failure": "Strategy not paused on drift beyond threshold",
      "consequence": "Trading continues against a stale portfolio state. Risk exposure is uncontrolled until the next successful sync cycle."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Open orders from CLOB (per wallet, per market)",
      "source": "clob_auth",
      "required": true,
      "use": "Compare against internal open-order store to detect divergence."
    },
    {
      "input": "On-chain pUSD balance (Polygon wallet)",
      "source": "onchain",
      "required": true,
      "use": "Cross-check internal pUSD accounting against actual on-chain balance."
    },
    {
      "input": "Market metadata (negRisk flag, condition_id, outcome tokens)",
      "source": "clob_auth",
      "required": true,
      "use": "Correctly aggregate multi-outcome positions in negRisk markets using NegRiskAdapter."
    }
  ],
  "internal_inputs": [
    {
      "input": "Internal position store snapshot",
      "source": "in-memory state",
      "required": true,
      "use": "The current internal view of open positions that must be reconciled against external state."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": false,
      "use": "When KillSwitch is active, continue syncing but suppress auto-pause-strategy logic."
    }
  ],
  "raw_params": [
    "sync_interval_s \u00b7 int",
    "discrepancy_alert_usd \u00b7 int",
    "auto_pause_strategy_on_drift \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "sync_interval_s",
      "default": 30,
      "warning": 120,
      "hard": 300,
      "controls": "How often PortfolioSync reconciles the in-memory state against CLOB and on-chain sources.",
      "why_default_matters": "30s gives a 1-minute worst-case drift window. Longer intervals risk strategies trading against significantly stale positions.",
      "threshold_logic": [
        {
          "condition": "sync_interval_s <= 30",
          "action": "Normal sync cadence"
        },
        {
          "condition": "30\u2013120s",
          "action": "WARN \u2014 drift window increased"
        },
        {
          "condition": "> 300s",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.sync_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "Your portfolio state is updated regularly to stay in sync with the exchange."
    },
    {
      "name": "discrepancy_alert_usd",
      "default": 10,
      "warning": 50,
      "hard": 500,
      "controls": "Threshold in pUSD above which a position discrepancy triggers an alert (and optionally pauses the affected strategy).",
      "why_default_matters": "10 pUSD catches even small drift from fill-rounding or missed cancellation events. 500 pUSD is a hard cap \u2014 drift above this is always critical.",
      "threshold_logic": [
        {
          "condition": "drift <= 10 pUSD",
          "action": "Log INFO; no alert"
        },
        {
          "condition": "10\u2013500 pUSD",
          "action": "Emit WARN PORTFOLIO_SYNC_DISCREPANCY"
        },
        {
          "condition": "> 500 pUSD",
          "action": "Emit page PORTFOLIO_SYNC_CRITICAL_DISCREPANCY; auto-pause strategy"
        }
      ],
      "dev_check": "if (drift > p.hard) alerting.emit('PORTFOLIO_SYNC_CRITICAL_DISCREPANCY', { drift_pusd: drift })",
      "user_facing": "The system alerts the team if there is any significant mismatch between the internal record and the actual position."
    },
    {
      "name": "auto_pause_strategy_on_drift",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true, automatically pauses the affected strategy when drift exceeds discrepancy_alert_usd hard threshold.",
      "why_default_matters": "True by default \u2014 trading against a stale position view is a safety risk. Auto-pause provides a safe floor without requiring manual intervention.",
      "threshold_logic": [
        {
          "condition": "auto_pause_strategy_on_drift=true AND drift > hard",
          "action": "Pause affected strategy; emit PORTFOLIO_SYNC_STRATEGY_PAUSED"
        },
        {
          "condition": "auto_pause_strategy_on_drift=false AND drift > hard",
          "action": "Emit page alert only; strategy continues (operator assumes responsibility)"
        }
      ],
      "dev_check": "if (p.auto_pause_strategy_on_drift && drift > p.discrepancy_alert_usd.hard) pauseStrategy(affected_strategy_id)",
      "user_facing": "If the portfolio state cannot be verified, affected trading strategies are paused automatically as a precaution."
    }
  ],
  "default_config": {
    "bot_id": "gov.portfolio_sync",
    "version": "2.0.0",
    "mode": "general_live",
    "defaults": {
      "sync_interval_s": 30,
      "discrepancy_alert_usd": 10,
      "auto_pause_strategy_on_drift": true
    },
    "locked": {
      "sync_interval_s": {
        "max": 300
      },
      "discrepancy_alert_usd": {
        "max": 500
      }
    }
  },
  "implementation_flow": [
    "Every sync_interval_s, snapshot the internal in-memory position store (all open positions and open orders).",
    "Fetch open orders from clob_auth for the wallet address; fetch on-chain pUSD balance from Polygon RPC.",
    "For each open position in the internal store: fetch the corresponding CLOB position and compare size, side, and average cost.",
    "For negative-risk markets (negRisk=true): use the NegRiskAdapter aggregation to compute the combined multi-outcome position before comparing.",
    "Compute drift_pusd = abs(internal_position_pusd - clob_position_pusd) for each market.",
    "If drift_pusd > discrepancy_alert_usd: emit WARN PORTFOLIO_SYNC_DISCREPANCY; if drift_pusd > discrepancy_alert_usd.hard, emit page and optionally pause strategy (auto_pause_strategy_on_drift).",
    "Apply deltas to the internal position store: add missing positions, remove closed positions, update sizes.",
    "Emit SettlementReport for each position delta (position added, removed, or updated).",
    "Emit OperationsReport for the full sync cycle: total_positions, synced_count, discrepancy_count, drift_pusd_max, sync_duration_ms.",
    "Retain SettlementReports for 7 years (position deltas are regulatory records). OperationsReports retained 1 year."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 PortfolioSync does not approve or reject trading decisions.",
    "reshape_required": "Not applicable.",
    "reject": "Not applicable as a trading decision. PortfolioSync may pause a strategy if drift exceeds the hard threshold and auto_pause_strategy_on_drift=true.",
    "warning_only": "Drift below the hard threshold emits a WARN but does not pause any strategy. Stale price or CLOB connectivity issues emit WARN STALE_DATA."
  },
  "decision_output_schema": "SettlementReport",
  "decision_output_example": {
    "report_id": "settlement_portfoliosync_1746792000000",
    "bot_id": "gov.portfolio_sync",
    "event_type": "POSITION_DELTA",
    "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
    "delta_type": "UPDATED",
    "internal_size_pusd": 1200.0,
    "clob_size_pusd": 1250.0,
    "drift_pusd": 50.0,
    "negRisk": false,
    "action_taken": "internal_store_updated",
    "synced_at_ms": 1746792000000,
    "report_kind": "SettlementReport"
  },
  "developer_log": {
    "bot_id": "gov.portfolio_sync",
    "event_type": "SYNC_CYCLE_COMPLETE",
    "total_positions": 12,
    "synced_count": 11,
    "discrepancy_count": 1,
    "drift_pusd_max": 50.0,
    "negRisk_positions": 2,
    "paused_strategies": [],
    "sync_duration_ms": 420,
    "synced_at_ms": 1746792000000
  },
  "user_explanations": [
    {
      "situation": "Sync cycle completed with no discrepancies",
      "message": "Your portfolio state matches the exchange records exactly."
    },
    {
      "situation": "Minor discrepancy detected and corrected",
      "message": "A small difference was found between the internal record and the exchange. It was corrected automatically."
    },
    {
      "situation": "Strategy paused due to portfolio drift",
      "message": "A significant difference was found between the internal position record and the exchange. The affected strategy has been paused while the team investigates."
    },
    {
      "situation": "Multi-outcome market position in portfolio",
      "message": "Some positions are in multi-outcome markets. These are tracked and valued using the correct multi-outcome formula."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "CLOB auth API or Polygon RPC is unavailable during a sync cycle. The internal position store cannot be reconciled; strategies may trade against stale state.",
    "false_positive_risk": "Transient CLOB API latency causes a fill-in-progress to appear as a position discrepancy, triggering a spurious WARN that resolves on the next sync cycle.",
    "false_negative_risk": "A negRisk position is not correctly identified (negRisk flag missing on market metadata), leading to under-aggregation of multi-outcome exposure and a silent undercount of position size.",
    "safe_fallback": "If CLOB API or RPC is unavailable, skip the sync cycle, emit WARN, and retry on the next scheduled interval. Do not apply partial deltas. Emit PORTFOLIO_SYNC_CLOB_UNAVAILABLE. If drift exceeds hard threshold from a stale state, emit the page alert regardless.",
    "required_dependencies": [
      "CLOB auth API (open orders per wallet)",
      "Polygon on-chain RPC (pUSD balance)",
      "Internal in-memory position store",
      "Market metadata API (negRisk flag)"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Discrepancy alert fires above discrepancy_alert_usd",
        "setup": "internal_size=1000 pUSD, clob_size=1060 pUSD (drift=60)",
        "expected": "PORTFOLIO_SYNC_DISCREPANCY emitted; internal store updated"
      },
      {
        "test": "Strategy paused at hard threshold",
        "setup": "drift_pusd=600 > hard=500; auto_pause_strategy_on_drift=true",
        "expected": "PORTFOLIO_SYNC_CRITICAL_DISCREPANCY alert; strategy paused; PORTFOLIO_SYNC_STRATEGY_PAUSED emitted"
      },
      {
        "test": "negRisk position aggregated correctly",
        "setup": "3 outcome tokens in same negRisk market; sizes [400, 300, 200] pUSD",
        "expected": "Combined position = NegRiskAdapter aggregate; not sum of three binary positions"
      },
      {
        "test": "sync_interval_s above hard maximum rejected",
        "setup": "sync_interval_s=400",
        "expected": "ConfigError PARAMETER_CHANGE_REQUIRES_APPROVAL"
      },
      {
        "test": "No delta applied on partial CLOB API response",
        "setup": "CLOB returns partial response (missing 3 positions)",
        "expected": "PORTFOLIO_SYNC_CLOB_UNAVAILABLE emitted; no partial delta applied; internal store unchanged"
      }
    ],
    "integration": [
      {
        "test": "End-to-end sync cycle: internal store matches CLOB after reconciliation",
        "expected": "SettlementReport per delta + OperationsReport per cycle emitted; Postgres updated"
      },
      {
        "test": "On-chain balance cross-check passes after fill",
        "expected": "pUSD balance within reconcile tolerance; SettlementReport onchain_reconciled=true"
      }
    ],
    "property": [
      {
        "property": "Internal position store is never more stale than sync_interval_s + sync_duration",
        "required": "Always true \u2014 partial sync is never applied"
      },
      {
        "property": "Every position delta emits exactly one SettlementReport",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Reconcile our internal position state with Polymarket's Data API every N seconds.",
  "legacy_pm_signals": [
    "Polymarket Data API positions per wallet",
    "Internal position-store snapshot"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "post_trade"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_auth",
    "onchain",
    "internal"
  ],
  "reference_implementation": {
    "summary": "Every sync_interval_s: snapshots internal state, fetches CLOB open orders and on-chain pUSD balance, reconciles per-market positions (with negRisk aggregation), applies deltas, emits SettlementReport per delta and OperationsReport per cycle.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "// ---- SYNC LOOP ----\nFUNCTION runSync():\n  sync_start = now()\n  internal_snapshot = position_store.snapshot()  // all open positions\n\n  // Fetch external state\n  clob_orders = FETCH clob_auth.GET('/orders?wallet=' + wallet_address)\n    ON_FAILURE: EMIT OperationsReport(event_type='PORTFOLIO_SYNC_CLOB_UNAVAILABLE'); RETURN\n  onchain_balance = FETCH onchain.getBalance(wallet_address, 'pUSD')\n    ON_FAILURE: EMIT OperationsReport(event_type='PORTFOLIO_SYNC_RPC_UNAVAILABLE'); RETURN\n\n  // Reconcile per-market\n  discrepancy_count = 0; drift_max = 0; deltas = []\n\n  FOR market_id IN union(keys(internal_snapshot), keys(clob_orders)):\n    internal_pos = internal_snapshot.get(market_id, ZERO)\n    clob_pos = clob_orders.get(market_id, ZERO)\n\n    market_meta = FETCH clob_auth.getMarketByConditionId(market_id)\n    IF market_meta.negRisk:\n      // Aggregate all outcome tokens via NegRiskAdapter\n      clob_pos = negRiskAggregate(clob_orders, market_id)\n      internal_pos = negRiskAggregate(internal_snapshot, market_id)\n\n    drift_pusd = abs(internal_pos.size_pusd - clob_pos.size_pusd)\n    drift_max = max(drift_max, drift_pusd)\n\n    IF drift_pusd > config.discrepancy_alert_usd:\n      discrepancy_count += 1\n      alerting.emit('PORTFOLIO_SYNC_DISCREPANCY', { market_id, drift_pusd })\n\n    IF drift_pusd > config.discrepancy_alert_usd.hard:  // 500 pUSD\n      alerting.emit('PORTFOLIO_SYNC_CRITICAL_DISCREPANCY', { market_id, drift_pusd })\n      IF config.auto_pause_strategy_on_drift:\n        pauseStrategy(getStrategyForMarket(market_id))\n        EMIT SettlementReport(event_type='PORTFOLIO_SYNC_STRATEGY_PAUSED', ...)\n\n    delta_type = computeDeltaType(internal_pos, clob_pos)\n    IF delta_type != 'NO_CHANGE':\n      position_store.apply(market_id, clob_pos)\n      deltas.append({ market_id, delta_type, drift_pusd, negRisk: market_meta.negRisk })\n      EMIT SettlementReport(event_type='POSITION_DELTA', market_id=market_id, ...)\n\n  // Ops report for this cycle\n  EMIT OperationsReport({\n    event_type:       'SYNC_CYCLE_COMPLETE',\n    total_positions:  len(union(internal_snapshot, clob_orders)),\n    synced_count:     len(deltas),\n    discrepancy_count: discrepancy_count,\n    drift_pusd_max:   drift_max,\n    onchain_balance:  onchain_balance,\n    sync_duration_ms: now() - sync_start\n  })\n",
    "sdk_calls": [
      "clob_auth.GET('/orders?wallet=<address>')",
      "clob_auth.getMarketByConditionId(market_id)",
      "onchain.getBalance(wallet_address, 'pUSD')",
      "negRiskAggregate(orders, market_id)",
      "alerting.emit('PORTFOLIO_SYNC_DISCREPANCY', { market_id, drift_pusd })",
      "pauseStrategy(strategy_id)"
    ],
    "complexity": "O(M) per sync cycle where M = number of open markets"
  },
  "wire_examples": {
    "input": {
      "label": "CLOB open orders response",
      "source": "clob_auth",
      "payload": {
        "wallet": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
        "open_orders": [
          {
            "order_id": "ord_00456",
            "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
            "side": "BUY",
            "size_pusd": 1250.0,
            "price": 0.6,
            "negRisk": false,
            "timestamp": 1746791900000
          }
        ],
        "onchain_balance_pusd": 52341.0
      }
    },
    "output": {
      "label": "SettlementReport \u2014 POSITION_DELTA",
      "payload": {
        "report_id": "settlement_portfoliosync_1746792000000",
        "bot_id": "gov.portfolio_sync",
        "event_type": "POSITION_DELTA",
        "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
        "delta_type": "UPDATED",
        "internal_size_pusd": 1200.0,
        "clob_size_pusd": 1250.0,
        "drift_pusd": 50.0,
        "negRisk": false,
        "action_taken": "internal_store_updated",
        "synced_at_ms": 1746792000000,
        "report_kind": "SettlementReport",
        "retained_until": "2033-05-09"
      }
    }
  },
  "reason_codes": [
    {
      "code": "PORTFOLIO_SYNC_CYCLE_COMPLETE",
      "severity": "INFO",
      "meaning": "Full sync cycle completed; OperationsReport emitted.",
      "action": "No action \u2014 routine.",
      "user_message": ""
    },
    {
      "code": "PORTFOLIO_SYNC_DISCREPANCY",
      "severity": "WARN",
      "meaning": "Position drift between internal store and CLOB exceeds discrepancy_alert_usd threshold.",
      "action": "Emit alert; apply delta to internal store; log drift_pusd.",
      "user_message": "A small difference was found between the internal record and the exchange. It was corrected."
    },
    {
      "code": "PORTFOLIO_SYNC_CRITICAL_DISCREPANCY",
      "severity": "WARN",
      "meaning": "Position drift exceeds the hard threshold (500 pUSD). This indicates a significant state divergence.",
      "action": "Emit page alert; optionally pause affected strategy (auto_pause_strategy_on_drift).",
      "user_message": "A significant difference was found. The affected strategy has been paused while the team investigates."
    },
    {
      "code": "PORTFOLIO_SYNC_STRATEGY_PAUSED",
      "severity": "WARN",
      "meaning": "A strategy was automatically paused because position drift exceeded the hard threshold.",
      "action": "Notify on-call; do not resume until drift is investigated and resolved.",
      "user_message": "A trading strategy was paused as a precaution while portfolio state is being verified."
    },
    {
      "code": "PORTFOLIO_SYNC_CLOB_UNAVAILABLE",
      "severity": "WARN",
      "meaning": "CLOB auth API was unavailable during a sync cycle; no delta applied.",
      "action": "Skip cycle; emit alert; retry on next interval.",
      "user_message": ""
    },
    {
      "code": "PORTFOLIO_SYNC_NEGRISK_AGGREGATE",
      "severity": "INFO",
      "meaning": "A negative-risk multi-outcome market was encountered; NegRiskAdapter aggregation applied.",
      "action": "No action \u2014 informational.",
      "user_message": ""
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "CLOB or RPC data is older than sync_interval_s; internal store may be stale.",
      "action": "Emit WARN; skip delta application; retry sync on next cycle.",
      "user_message": "Portfolio synchronisation is delayed. The team has been notified."
    },
    {
      "code": "PARAMETER_CHANGE_REQUIRES_APPROVAL",
      "severity": "HARD_REJECT",
      "meaning": "sync_interval_s exceeds the 300s hard maximum.",
      "action": "Reject the config change; emit alert.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_gov_portfoliosync_positions_synced_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "delta_type"
        ],
        "meaning": "Total position deltas applied (ADDED, REMOVED, UPDATED) across all sync cycles."
      },
      {
        "name": "polytraders_gov_portfoliosync_discrepancy_count",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of markets with drift above discrepancy_alert_usd at the last sync cycle."
      },
      {
        "name": "polytraders_gov_portfoliosync_drift_pusd_max",
        "type": "gauge",
        "unit": "usd",
        "labels": [],
        "meaning": "Maximum per-market drift (pUSD) observed in the last sync cycle."
      },
      {
        "name": "polytraders_gov_portfoliosync_sync_duration_ms",
        "type": "histogram",
        "unit": "ms",
        "labels": [],
        "meaning": "Wall-clock duration of a full sync cycle."
      },
      {
        "name": "polytraders_gov_portfoliosync_strategies_paused_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "strategy_id"
        ],
        "meaning": "Total strategy pause events triggered by critical position drift."
      },
      {
        "name": "polytraders_gov_portfoliosync_sync_cycles_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "status"
        ],
        "meaning": "Total sync cycles completed, labelled by status (ok, clob_unavailable, rpc_unavailable)."
      }
    ],
    "alerts": [
      {
        "name": "PortfolioSyncCriticalDiscrepancy",
        "condition": "polytraders_gov_portfoliosync_drift_pusd_max > 500",
        "severity": "page",
        "runbook": "#runbook-portfoliosync-critical-discrepancy"
      },
      {
        "name": "PortfolioSyncStrategyPaused",
        "condition": "rate(polytraders_gov_portfoliosync_strategies_paused_total[5m]) > 0",
        "severity": "page",
        "runbook": "#runbook-portfoliosync-strategy-paused"
      },
      {
        "name": "PortfolioSyncClobUnavailable",
        "condition": "rate(polytraders_gov_portfoliosync_sync_cycles_total{status='clob_unavailable'}[10m]) > 0",
        "severity": "warn",
        "runbook": "#runbook-portfoliosync-clob-unavailable"
      },
      {
        "name": "PortfolioSyncNoSyncIn5m",
        "condition": "rate(polytraders_gov_portfoliosync_sync_cycles_total[5m]) == 0",
        "severity": "page",
        "runbook": "#runbook-portfoliosync-no-sync"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Governance / PortfolioSync position drift timeline",
      "Grafana \u2014 Governance / PortfolioSync negRisk position aggregation"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "in-memory + postgres",
    "shape": "In-memory: position_store { market_id -> { size_pusd, avg_cost, side, negRisk, open_orders[] } }. Postgres: sync_log table for audit trail of all deltas and sync cycles.",
    "ttl": "In-memory state persists for process lifetime; Postgres sync_log retained 7 years",
    "recovery": "On restart, position_store is rebuilt from a full CLOB sync on startup. First sync cycle restores complete state before any trading is allowed.",
    "size_estimate": "~1 KB per open position; < 10 MB for all active positions; Postgres sync_log ~500 KB/day"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop (sync cycle) + async CLOB/RPC fetch",
    "max_in_flight": 50,
    "idempotency_key": "market_id + synced_at_ms",
    "timeout_ms": 5000,
    "backpressure": "if CLOB fetch exceeds timeout, skip cycle and emit PORTFOLIO_SYNC_CLOB_UNAVAILABLE",
    "locking": "per-market RwLock on position_store entries"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "internal.clob_auth",
        "why": "Open orders and market metadata are fetched from clob_auth on each sync cycle."
      },
      {
        "bot_id": "internal.onchain_rpc",
        "why": "pUSD on-chain balance cross-check requires Polygon RPC access."
      }
    ],
    "emits_to": [
      {
        "bot_id": "gov.pnl_reporter",
        "what": "Position store snapshot (consumed by PnLReporter for unrealised P&L)"
      },
      {
        "bot_id": "risk.portfolio_guard",
        "what": "Current position sizes for risk exposure checks"
      }
    ],
    "sibling": [
      {
        "bot_id": "gov.pnl_reporter",
        "why": "PortfolioSync provides the position state that PnLReporter uses for unrealised P&L computation."
      }
    ],
    "external": [
      {
        "service": "Polymarket CLOB v2 (open orders, market metadata)",
        "sla": "99.95% / 200ms p99 (Polymarket-published)",
        "fallback": "Skip sync cycle; emit PORTFOLIO_SYNC_CLOB_UNAVAILABLE; retry on next interval."
      },
      {
        "service": "Polygon on-chain RPC (pUSD balance)",
        "sla": "99.9% / 500ms p99",
        "fallback": "Skip on-chain balance check for this cycle; flag OperationsReport as rpc_unavailable."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Raising discrepancy_alert_usd hard threshold to suppress critical drift alerts",
      "Disabling auto_pause_strategy_on_drift to allow trading against stale state",
      "Manipulating the CLOB open-order response to show smaller positions than actual"
    ],
    "mitigations": [
      "discrepancy_alert_usd has a hard maximum of 500 pUSD enforced at config load",
      "sync_interval_s has a hard maximum of 300s enforced at config load",
      "On-chain balance provides an independent cross-check that cannot be manipulated via CLOB API",
      "PortfolioSync never signs orders or holds private keys \u2014 read-only access to CLOB and RPC"
    ],
    "contract_calls": []
  },
  "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 on Polygon",
    "notes": "PortfolioSync correctly handles negative-risk multi-outcome positions via NegRiskAdapter aggregation. All position sizes and pUSD balances are denominated in pUSD (not USDC.e). Open orders fetched via clob_auth V2 endpoint."
  },
  "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 denomination, V1 CLOB open-order endpoint)",
      "to": "v2 (pUSD denomination, clob_auth V2 endpoint)",
      "reason": "CLOB V2 cutover",
      "action_taken": "Updated all position sizes from USDC.e to pUSD denomination. Switched to clob_auth V2 open-order endpoint (py-clob-client-v2). Added NegRiskAdapter aggregation path for negative-risk multi-outcome markets. Updated SettlementReport schema to include negRisk flag per delta."
    }
  ],
  "failure_injection": [
    {
      "scenario": "CLOB_API_UNAVAILABLE",
      "how_to_inject": "Block TCP to clob.polymarket.com during a sync cycle",
      "expected_behavior": "PORTFOLIO_SYNC_CLOB_UNAVAILABLE emitted; no delta applied; internal state unchanged; retry on next interval",
      "recovery": "CLOB reconnects; next cycle performs full sync."
    },
    {
      "scenario": "CRITICAL_DRIFT",
      "how_to_inject": "Manually edit the in-memory position store to show 600 pUSD more than CLOB",
      "expected_behavior": "PORTFOLIO_SYNC_CRITICAL_DISCREPANCY page alert; affected strategy paused (auto_pause_strategy_on_drift=true)",
      "recovery": "Investigate root cause; resume strategy via manual override after verification."
    },
    {
      "scenario": "NEGRISK_MISCONFIGURED",
      "how_to_inject": "Set negRisk=false on a multi-outcome market; submit 3 outcome token positions",
      "expected_behavior": "Positions reconciled as independent binary positions; drift may be flagged if NegRisk aggregation would give different total",
      "recovery": "Update market metadata with correct negRisk flag; re-sync."
    },
    {
      "scenario": "ONCHAIN_RPC_FAILURE",
      "how_to_inject": "Block Polygon RPC calls during sync",
      "expected_behavior": "PORTFOLIO_SYNC_RPC_UNAVAILABLE in OperationsReport; on-chain balance check skipped; position deltas still applied from CLOB data",
      "recovery": "RPC reconnects; on-chain balance check resumes on next cycle."
    },
    {
      "scenario": "SYNC_LOOP_STALL",
      "how_to_inject": "Introduce 10s artificial delay in CLOB fetch (beyond timeout_ms=5000)",
      "expected_behavior": "CLOB fetch times out; PORTFOLIO_SYNC_CLOB_UNAVAILABLE emitted; cycle skipped",
      "recovery": "CLOB latency normalises; next cycle completes within timeout."
    }
  ],
  "runbook": {
    "summary": "PortfolioSync incidents fall into: critical position drift (strategy auto-paused), CLOB/RPC unavailability (sync skipped), or negRisk misconfiguration (silent exposure undercount). Drift above 500 pUSD is always P1.",
    "oncall_actions": [
      {
        "alert": "PortfolioSyncCriticalDiscrepancy",
        "first_step": "Identify which market(s) have critical drift. Check CLOB and on-chain balance directly. Do NOT resume strategy until root cause is confirmed.",
        "escalation": "Governance pod lead immediately"
      },
      {
        "alert": "PortfolioSyncStrategyPaused",
        "first_step": "Review PORTFOLIO_SYNC_CRITICAL_DISCREPANCY logs. Confirm which strategy was paused and why.",
        "escalation": "Governance pod lead + strategy owner"
      },
      {
        "alert": "PortfolioSyncClobUnavailable",
        "first_step": "Check Polymarket CLOB API status. If unavailable, monitor sync recovery. Strategies are not paused but are trading against stale state.",
        "escalation": "SRE on-call if CLOB unavailability exceeds 5 minutes"
      },
      {
        "alert": "PortfolioSyncNoSyncIn5m",
        "first_step": "Check PortfolioSync process status and internal bus connectivity.",
        "escalation": "Governance pod lead"
      }
    ],
    "manual_overrides": [
      {
        "name": "force_full_sync",
        "how": "polytraders gov portfolio force-sync",
        "when": "Trigger an immediate full sync cycle outside the normal interval.",
        "command": "polytraders gov portfolio force-sync",
        "effect": "Trigger an immediate full sync cycle outside the normal interval."
      },
      {
        "name": "resume_strategy",
        "how": "polytraders gov portfolio resume-strategy --strategy-id <id> --reviewed-by <operator>",
        "when": "Resume an auto-paused strategy after drift root cause has been investigated and resolved.",
        "command": "polytraders gov portfolio resume-strategy --strategy-id <id> --reviewed-by <operator>",
        "effect": "Resume an auto-paused strategy after drift root cause has been investigated and resolved."
      }
    ],
    "healthcheck": "Endpoint: /internal/health/portfolio-sync | Green: Last sync cycle completed within 2x sync_interval_s; CLOB reachable; discrepancy_count = 0; no strategies paused. | Red: No sync in 2x sync_interval_s; CLOB unavailable > 5 min; critical drift detected; strategies paused."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass: drift detection, negRisk aggregation, auto-pause threshold",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Sync cycle latency p99 < 5s for 50 open positions",
        "how_measured": "polytraders_gov_portfoliosync_sync_duration_ms histogram",
        "threshold": "< 5s"
      },
      {
        "gate": "Critical drift auto-pause fires correctly in failure injection",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "End-to-end: negRisk position correctly aggregated and reconciled with CLOB",
        "how_measured": "E2E test with staging CLOB",
        "threshold": "Pass"
      },
      {
        "gate": "7-year retention applied to SettlementReport position deltas",
        "how_measured": "Postgres retention policy audit",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "SettlementReport",
      "OperationsReport"
    ],
    "topics": [
      "polytraders.reports.settlement",
      "polytraders.reports.ops"
    ],
    "cadence": "every-event",
    "retention_class": "7y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "wal-then-retry",
    "user_visible": "yes",
    "consumes_kinds": []
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 7,
    "phase_name": "Governance & replay",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}