{
  "schema_version": "1.0.0",
  "bot_id": "6.3",
  "bot_name": "PnL Reporter",
  "slug": "pnl-reporter",
  "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": "Nothing \u2014 runs post-trade on every fill event and on the configured report_window cadence",
    "runs_after": "Order fill confirmation from CTFExchangeV2 match event",
    "applies_to": "Every filled or partially filled order; daily and weekly P&L windows across all bots, markets, and builders",
    "default_mode": "general_live",
    "user_visible": "Summary and detail view",
    "developer_owner": "Polytraders core \u2014 Governance pod"
  },
  "purpose": "PnLReporter reconciles all fill events into realised and unrealised P&L, denominated in pUSD. It reads fee from the fill's match event (not from the signed order \u2014 fees are operator-set at match time in V2). It accrues maker rebates (20\u201325% of fees, paid in pUSD, per market) and credits them against gross fees. It groups P&L by bot, market, and builder as configured. It emits a SettlementReport after every fill and on the configured window cadence for regulatory retention. PnLReporter is the authoritative post-trade ledger for all pUSD flows.",
  "why_it_matters": [
    {
      "failure": "Fee read from signed order instead of match event",
      "consequence": "In V2, operator fees are set at match time \u2014 not on the signed order. Reading fees from the order produces systematically wrong P&L. Maker rebates accrued against wrong fee basis will cause discrepancies in regulatory reports."
    },
    {
      "failure": "Maker rebate not accrued",
      "consequence": "Gross fees overstate true cost. P&L appears worse than actual. Fee dispute evidence is incomplete."
    },
    {
      "failure": "P&L not denominated in pUSD",
      "consequence": "Mixed USDC.e and pUSD figures corrupt the ledger. Post-trade reports cannot be reconciled against on-chain settlement."
    },
    {
      "failure": "Realised P&L not separated from unrealised",
      "consequence": "Open positions inflate reported returns. Regulatory settlement reports misrepresent actual cash flows."
    },
    {
      "failure": "Negative-risk positions mis-valued",
      "consequence": "Multi-outcome market positions (NegRiskAdapter) have non-linear payoffs. Treating them as independent binary positions overstates or understates exposure."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Fill confirmation from CTFExchangeV2 match event (includes operator-set fee_bps at match time)",
      "source": "clob_auth",
      "required": true,
      "use": "Primary fill record. Fee is extracted from match event field \u2014 not from the signed order."
    },
    {
      "input": "Mark-to-market prices for open positions",
      "source": "clob_public",
      "required": true,
      "use": "Compute unrealised P&L by marking open positions at current mid-price."
    },
    {
      "input": "On-chain balance query (pUSD wallet balance)",
      "source": "onchain",
      "required": true,
      "use": "Cross-check realised P&L against actual on-chain pUSD balance for regulatory reconciliation."
    },
    {
      "input": "Market metadata (negRisk flag, condition_id, market_type)",
      "source": "clob_public",
      "required": true,
      "use": "Identify negative-risk markets for correct payoff valuation."
    }
  ],
  "internal_inputs": [
    {
      "input": "Position store snapshot",
      "source": "gov.portfolio_sync",
      "required": true,
      "use": "Current open position sizes per token per market, used for unrealised P&L computation."
    },
    {
      "input": "BuilderAttribution fill log",
      "source": "gov.builder_attribution",
      "required": true,
      "use": "Join fills with builder code and builder_fee_pusd for per-builder P&L breakdown."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": false,
      "use": "When KillSwitch is active, continue recording fills but flag the reporting window as impacted."
    }
  ],
  "raw_params": [
    "report_window \u00b7 enum (daily, weekly)",
    "group_by \u00b7 enum (bot, market, author)",
    "include_paper \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "report_window",
      "default": "daily",
      "warning": null,
      "hard": null,
      "controls": "Cadence of the aggregate SettlementReport: daily or weekly.",
      "why_default_matters": "Daily aligns with Polymarket's rolling attribution report and typical regulatory reconciliation cadence.",
      "threshold_logic": [
        {
          "condition": "report_window=daily",
          "action": "Emit aggregate SettlementReport every 24h"
        },
        {
          "condition": "report_window=weekly",
          "action": "Emit aggregate SettlementReport every 7 days"
        }
      ],
      "dev_check": "assert p.report_window in ('daily', 'weekly')",
      "user_facing": "Your P&L summary is updated and reported on a regular schedule."
    },
    {
      "name": "group_by",
      "default": "bot",
      "warning": null,
      "hard": null,
      "controls": "Dimension along which P&L is aggregated: bot, market, or builder.",
      "why_default_matters": "Grouping by bot aligns with strategy-level performance attribution used for risk management.",
      "threshold_logic": [
        {
          "condition": "group_by in (bot, market, builder)",
          "action": "Apply grouping"
        },
        {
          "condition": "invalid value",
          "action": "Reject config; emit ConfigError"
        }
      ],
      "dev_check": "assert p.group_by in ('bot', 'market', 'builder')",
      "user_facing": "P&L is broken down by trading strategy."
    },
    {
      "name": "include_paper",
      "default": false,
      "warning": null,
      "hard": null,
      "controls": "When true, include paper-trading fills in the P&L report alongside live fills.",
      "why_default_matters": "False by default \u2014 paper fills must not appear in regulatory SettlementReports. Enable only for development dashboards.",
      "threshold_logic": [
        {
          "condition": "include_paper=true AND report is regulatory",
          "action": "WARN \u2014 paper fills must not be in regulatory reports; flag report accordingly"
        },
        {
          "condition": "include_paper=false",
          "action": "Only live fills included"
        }
      ],
      "dev_check": "if (p.include_paper && report.is_regulatory) log.warn('PNL_REPORTER_PAPER_IN_REGULATORY')",
      "user_facing": "Live P&L reporting excludes test trades."
    },
    {
      "name": "reconcile_tolerance_pusd",
      "default": 0.01,
      "warning": 5.0,
      "hard": 50.0,
      "controls": "Maximum pUSD difference between aggregate P&L total and on-chain balance before the reconciliation is flagged as failed.",
      "why_default_matters": "A 0.01 pUSD tolerance accounts for floating-point rounding only. Larger values risk masking genuine discrepancies.",
      "threshold_logic": [
        {
          "condition": "abs(pnl_total - onchain_balance) <= 0.01",
          "action": "onchain_reconciled=true"
        },
        {
          "condition": "0.01\u201350 pUSD",
          "action": "WARN PNL_REPORTER_ONCHAIN_RECONCILE_FAIL"
        },
        {
          "condition": "> 50 pUSD",
          "action": "Page alert; flag report as unreconciled; hold 7y retention record"
        }
      ],
      "dev_check": "if (drift > p.hard) alerting.emit(\"PNL_REPORTER_ONCHAIN_RECONCILE_FAIL\", { drift })",
      "user_facing": ""
    }
  ],
  "default_config": {
    "bot_id": "gov.pnl_reporter",
    "version": "2.0.0",
    "mode": "general_live",
    "defaults": {
      "report_window": "daily",
      "group_by": "bot",
      "include_paper": false,
      "reconcile_tolerance_pusd": 0.01
    }
  },
  "implementation_flow": [
    "On every fill event from CTFExchangeV2: extract fill_id, order_id, market_id, side, size_pusd, price, and fee_bps from the MATCH EVENT (not from the signed order).",
    "Compute gross_fee_pusd = size_pusd * fee_bps / 10_000. Distinguish maker vs taker side; apply maker rebate (20\u201325% of fee, per market, paid in pUSD).",
    "Compute net_fee_pusd = gross_fee_pusd - maker_rebate_pusd. Validate taker fee_bps <= 100, maker fee_bps <= 50.",
    "Determine position delta: BUY increases long position; SELL decreases or creates short position.",
    "Compute realised_pnl_pusd on position close using FIFO cost basis. Mark open positions at current mid-price for unrealised_pnl_pusd.",
    "For negative-risk markets (negRisk=true), use NegRiskAdapter payoff function instead of binary payoff.",
    "Append fill record to the P&L ledger (Postgres) with all fields including net_fee_pusd, maker_rebate_pusd, realised_pnl_pusd, unrealised_pnl_pusd.",
    "Emit SettlementReport per fill event (emit-every) to satisfy regulatory retention requirement.",
    "At report_window boundary, compute aggregate SettlementReport grouped by group_by dimension. Cross-check totals against on-chain pUSD balance.",
    "Retain all SettlementReport records for 7 years (regulatory requirement). Use WAL-backed store with retry on bus failure."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 PnLReporter does not approve or reject orders.",
    "reshape_required": "Not applicable.",
    "reject": "Not applicable as a trading decision. PnLReporter will flag and quarantine fills where fee_bps exceeds V2 caps (taker > 100 bps or maker > 50 bps).",
    "warning_only": "include_paper=true on a regulatory report emits a warning. Unrealised P&L calculations that fail to fetch mark-to-market prices emit WARN STALE_DATA and use last known price."
  },
  "decision_output_schema": "SettlementReport",
  "decision_output_example": {
    "report_id": "settlement_pnl_20260509T000000Z",
    "bot_id": "gov.pnl_reporter",
    "event_type": "SETTLEMENT_REPORT",
    "window_start": "2026-05-08T00:00:00Z",
    "window_end": "2026-05-09T00:00:00Z",
    "group_by": "bot",
    "total_fills": 143,
    "gross_volume_pusd": 87450.0,
    "realised_pnl_pusd": 1240.5,
    "unrealised_pnl_pusd": 312.0,
    "gross_fees_pusd": 218.6,
    "maker_rebates_pusd": 49.7,
    "net_fees_pusd": 168.9,
    "net_pnl_pusd": 1071.6,
    "onchain_balance_pusd": 52341.0,
    "onchain_reconciled": true,
    "negRisk_positions": 2,
    "report_kind": "SettlementReport",
    "retained_until": "2033-05-09"
  },
  "developer_log": {
    "bot_id": "gov.pnl_reporter",
    "event_type": "FILL_PNL_COMPUTED",
    "fill_id": "fill_00a1b2c3d4e5f6a7",
    "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
    "side": "BUY",
    "size_pusd": 500.0,
    "price": 0.58,
    "fee_bps_from_match_event": 25,
    "gross_fee_pusd": 12.5,
    "maker_rebate_pusd": 2.5,
    "net_fee_pusd": 10.0,
    "realised_pnl_pusd": 0,
    "unrealised_pnl_pusd": 10.0,
    "negRisk": false,
    "fill_confirmed_at_ms": 1746792060000
  },
  "user_explanations": [
    {
      "situation": "Daily P&L report generated",
      "message": "Today's trading summary: total volume, realised gains/losses, fees paid, and maker rebates received \u2014 all in pUSD."
    },
    {
      "situation": "Maker rebate credited",
      "message": "A portion of the trading fee was returned as a maker rebate, credited in pUSD to reduce the net cost of the trade."
    },
    {
      "situation": "P&L cross-check with on-chain balance passed",
      "message": "The reported P&L matches the actual on-chain pUSD balance. Records are reconciled."
    },
    {
      "situation": "Negative-risk position in report",
      "message": "Some positions are in multi-outcome markets. Their P&L is calculated using the correct multi-outcome payoff formula."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Fill event arrives without fee_bps in the match event (e.g., from a non-standard match path). PnLReporter cannot compute net P&L without the match-event fee.",
    "false_positive_risk": "On-chain balance check fails due to RPC latency, causing a spurious reconciliation mismatch. The report is flagged as unreconciled until the next check.",
    "false_negative_risk": "A negative-risk position is mis-identified as a standard binary position because the negRisk flag is not set on the market metadata, leading to incorrect unrealised P&L.",
    "safe_fallback": "If fee_bps is missing from the match event, quarantine the fill and emit PNL_REPORTER_FEE_MISSING. Do not estimate or default the fee \u2014 the fill must be manually reviewed. If mark-to-market prices are stale, use last known price and emit STALE_DATA warn.",
    "required_dependencies": [
      "CTFExchangeV2 fill events with match-event fee_bps",
      "CLOB public API (mark-to-market prices)",
      "On-chain pUSD balance RPC",
      "gov.portfolio_sync position store",
      "gov.builder_attribution fill log",
      "Postgres P&L ledger"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Fee read from match event, not from signed order",
        "setup": "Fill event with match_event.fee_bps=30 and order.fee_bps=0 (V2 order has no fee field)",
        "expected": "gross_fee_pusd computed from match_event.fee_bps=30"
      },
      {
        "test": "Maker rebate accrued at 20\u201325% of fee",
        "setup": "Maker fill; fee_bps=40; rebate_rate=0.25",
        "expected": "maker_rebate_pusd = size_pusd * 40/10000 * 0.25"
      },
      {
        "test": "Taker fee_bps > 100 flagged",
        "setup": "fill with fee_bps=110, side=taker",
        "expected": "PNL_REPORTER_FEE_CAP_EXCEEDED quarantine; fill not included in P&L"
      },
      {
        "test": "Missing fee_bps in match event quarantines fill",
        "setup": "Fill event with no fee_bps field",
        "expected": "PNL_REPORTER_FEE_MISSING; fill quarantined for manual review"
      },
      {
        "test": "Negative-risk position uses correct payoff function",
        "setup": "Market negRisk=true; position size=100; outcome price=0.4",
        "expected": "unrealised_pnl_pusd computed via NegRiskAdapter payoff, not binary"
      },
      {
        "test": "Realised P&L computed on position close (FIFO)",
        "setup": "Open position: 100 shares at 0.50; close: 100 shares at 0.70",
        "expected": "realised_pnl_pusd = 20 pUSD (before fees)"
      }
    ],
    "integration": [
      {
        "test": "End-to-end fill \u2192 SettlementReport \u2192 Postgres ledger",
        "expected": "SettlementReport emitted per fill; aggregate report at window boundary; 7y retention tag applied"
      },
      {
        "test": "On-chain reconciliation passes for matching balance",
        "expected": "onchain_reconciled=true in aggregate SettlementReport"
      }
    ],
    "property": [
      {
        "property": "Every fill event produces exactly one SettlementReport entry",
        "required": "Always true \u2014 emit-every policy enforced"
      },
      {
        "property": "No fill with missing fee_bps is ever included in a P&L total",
        "required": "Always true \u2014 quarantine on missing fee enforced"
      },
      {
        "property": "All P&L amounts are denominated in pUSD (no USDC.e)",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Compute realised + unrealised PnL per bot, per market, per author.",
  "legacy_pm_signals": [
    "Polymarket fills per token / market",
    "Mark-to-market prices for open positions"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "post_trade",
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "clob_auth",
    "onchain",
    "internal"
  ],
  "reference_implementation": {
    "summary": "Consumes fill events from CTFExchangeV2, reads fee from match event, accrues maker rebates, computes realised and unrealised pUSD P&L with negRisk awareness, emits SettlementReport per fill and on window cadence.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "// ---- PER-FILL HANDLER ----\nFUNCTION onFill(matchEvent):\n  // V2: fee is operator-set at match time \u2014 read from matchEvent, NOT from order\n  IF matchEvent.fee_bps IS NULL:\n    alerting.emit('PNL_REPORTER_FEE_MISSING', { fill_id: matchEvent.fill_id })\n    quarantine(matchEvent)\n    RETURN\n\n  side = matchEvent.side  // 'maker' | 'taker'\n  IF side == 'taker' AND matchEvent.fee_bps > 100:\n    alerting.emit('PNL_REPORTER_FEE_CAP_EXCEEDED', { fill_id: matchEvent.fill_id, fee_bps: matchEvent.fee_bps })\n    quarantine(matchEvent)\n    RETURN\n  IF side == 'maker' AND matchEvent.fee_bps > 50:\n    alerting.emit('PNL_REPORTER_FEE_CAP_EXCEEDED', { fill_id: matchEvent.fill_id, fee_bps: matchEvent.fee_bps })\n    quarantine(matchEvent)\n    RETURN\n\n  size_pusd = toPusdUnits(matchEvent.size_usd)\n  gross_fee_pusd = size_pusd * matchEvent.fee_bps / 10_000\n\n  // Maker rebate: 20\u201325% of fee, paid in pUSD, per market\n  rebate_rate = FETCH clob_public.GET('/market-rebate-rate?market_id=' + matchEvent.market_id)\n  maker_rebate_pusd = 0\n  IF side == 'maker':\n    maker_rebate_pusd = gross_fee_pusd * rebate_rate  // rebate_rate in [0.20, 0.25]\n  net_fee_pusd = gross_fee_pusd - maker_rebate_pusd\n\n  // Market metadata \u2014 check negRisk\n  market = FETCH clob_public.getMarketByConditionId(matchEvent.condition_id)\n\n  // Position delta and realised P&L (FIFO cost basis)\n  position = portfolio_sync.getPosition(matchEvent.market_id, matchEvent.token_id)\n  realised_pnl_pusd = computeRealisedPnL(position, matchEvent, costBasis='FIFO')\n\n  // Unrealised P&L: mark open positions at current mid-price\n  IF market.negRisk:\n    unrealised_pnl_pusd = negRiskPayoff(position, market)\n  ELSE:\n    mid_price = FETCH clob_public.GET('/midprice?token_id=' + matchEvent.token_id)\n    unrealised_pnl_pusd = (mid_price - position.avg_cost) * position.remaining_size\n\n  record = {\n    fill_id:             matchEvent.fill_id,\n    market_id:           matchEvent.market_id,\n    side:                side,\n    size_pusd:           size_pusd,\n    price:               matchEvent.price,\n    fee_bps_match_event: matchEvent.fee_bps,\n    gross_fee_pusd:      gross_fee_pusd,\n    maker_rebate_pusd:   maker_rebate_pusd,\n    net_fee_pusd:        net_fee_pusd,\n    realised_pnl_pusd:   realised_pnl_pusd,\n    unrealised_pnl_pusd: unrealised_pnl_pusd,\n    negRisk:             market.negRisk,\n    fill_confirmed_at_ms: matchEvent.timestamp\n  }\n  postgres.INSERT('pnl_ledger', record)\n\n  EMIT SettlementReport(event_type='FILL_PNL_COMPUTED', fill_id=matchEvent.fill_id, ...record)\n\n// ---- WINDOW REPORT ----\nFUNCTION emitWindowReport(windowStart, windowEnd):\n  rows = postgres.SELECT('pnl_ledger', WHERE fill_confirmed_at_ms BETWEEN windowStart AND windowEnd)\n  agg = aggregatePnL(rows, group_by=config.group_by)\n  onchain_balance = FETCH onchain.getBalance(wallet_address, token='pUSD')\n  onchain_reconciled = abs(agg.net_pnl_pusd - onchain_balance) < RECONCILE_TOLERANCE\n  EMIT SettlementReport(event_type='SETTLEMENT_REPORT', retention='7y', ...agg, onchain_reconciled=onchain_reconciled)\n",
    "sdk_calls": [
      "clob_public.GET('/market-rebate-rate?market_id=...')",
      "clob_public.getMarketByConditionId(condition_id)",
      "clob_public.GET('/midprice?token_id=...')",
      "clob_auth.GET('/fills?order_id=...')",
      "onchain.getBalance(wallet_address, token='pUSD')",
      "toPusdUnits(raw_usd)",
      "postgres.INSERT('pnl_ledger', record)",
      "alerting.emit('PNL_REPORTER_FEE_MISSING', metadata)"
    ],
    "complexity": "O(F) per window where F = fill count; O(1) per fill event"
  },
  "wire_examples": {
    "input": {
      "label": "CTFExchangeV2 match event (fill with operator-set fee)",
      "source": "clob_auth",
      "payload": {
        "fill_id": "fill_00a1b2c3d4e5f6a7",
        "order_id": "ord_00123",
        "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
        "condition_id": "0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f",
        "side": "maker",
        "size_usd": 500.0,
        "price": 0.58,
        "fee_bps": 25,
        "negRisk": false,
        "timestamp": 1746792060000
      }
    },
    "output": {
      "label": "SettlementReport \u2014 FILL_PNL_COMPUTED",
      "payload": {
        "report_id": "settlement_fill_fill_00a1b2c3d4e5f6a7",
        "bot_id": "gov.pnl_reporter",
        "event_type": "FILL_PNL_COMPUTED",
        "fill_id": "fill_00a1b2c3d4e5f6a7",
        "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
        "side": "maker",
        "size_pusd": 500.0,
        "price": 0.58,
        "fee_bps_match_event": 25,
        "gross_fee_pusd": 12.5,
        "maker_rebate_pusd": 3.12,
        "net_fee_pusd": 9.38,
        "realised_pnl_pusd": 0,
        "unrealised_pnl_pusd": 11.0,
        "negRisk": false,
        "fill_confirmed_at_ms": 1746792060000,
        "report_kind": "SettlementReport",
        "retained_until": "2033-05-09"
      }
    }
  },
  "reason_codes": [
    {
      "code": "PNL_REPORTER_FILL_PNL_COMPUTED",
      "severity": "INFO",
      "meaning": "Fill event successfully processed; realised and unrealised P&L computed; SettlementReport emitted.",
      "action": "No action \u2014 routine.",
      "user_message": "A trade was processed and your P&L was updated."
    },
    {
      "code": "PNL_REPORTER_FEE_MISSING",
      "severity": "HARD_REJECT",
      "meaning": "Fill event arrived without fee_bps in the match event. Cannot compute net P&L without match-event fee.",
      "action": "Quarantine fill; emit alert; require manual review before inclusion in ledger.",
      "user_message": ""
    },
    {
      "code": "PNL_REPORTER_FEE_CAP_EXCEEDED",
      "severity": "WARN",
      "meaning": "Observed fee_bps in match event exceeds V2 cap (taker > 100 bps or maker > 50 bps).",
      "action": "Quarantine fill; emit alert for manual review.",
      "user_message": ""
    },
    {
      "code": "PNL_REPORTER_PAPER_IN_REGULATORY",
      "severity": "WARN",
      "meaning": "include_paper=true is set on a report that is flagged as regulatory. Paper fills must not appear in regulatory reports.",
      "action": "Flag report as mixed; emit WARN; do not include in 7-year retention tier.",
      "user_message": ""
    },
    {
      "code": "PNL_REPORTER_ONCHAIN_RECONCILE_FAIL",
      "severity": "WARN",
      "meaning": "Aggregate P&L total diverges from on-chain pUSD balance beyond the reconciliation tolerance.",
      "action": "Flag report as unreconciled; emit alert; retry reconciliation after RPC cooldown.",
      "user_message": "P&L reconciliation with on-chain balance is pending."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "Mark-to-market price for an open position is stale (last fetch > 60s ago). Unrealised P&L uses last known price.",
      "action": "Emit WARN; use stale price for unrealised calculation; retry fetch on next cycle.",
      "user_message": "Unrealised P&L estimate uses the most recently available price."
    },
    {
      "code": "PNL_REPORTER_NEGRISK_DETECTED",
      "severity": "INFO",
      "meaning": "A negative-risk market position was detected; NegRiskAdapter payoff function applied.",
      "action": "No action \u2014 informational.",
      "user_message": "A multi-outcome market position is included in your P&L, valued using the correct payoff formula."
    },
    {
      "code": "MARKET_CLOSED",
      "severity": "INFO",
      "meaning": "A market has resolved; open positions closed; realised P&L finalised at settlement price.",
      "action": "Compute final realised P&L; emit SettlementReport with event_type=MARKET_SETTLED.",
      "user_message": "A market you participated in has resolved. Final P&L has been recorded."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_gov_pnlreporter_fills_processed_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "side",
          "negRisk"
        ],
        "meaning": "Total fill events processed and included in the P&L ledger."
      },
      {
        "name": "polytraders_gov_pnlreporter_gross_volume_pusd_total",
        "type": "counter",
        "unit": "usd",
        "labels": [],
        "meaning": "Cumulative gross pUSD volume across all processed fills."
      },
      {
        "name": "polytraders_gov_pnlreporter_net_fees_pusd_total",
        "type": "counter",
        "unit": "usd",
        "labels": [
          "side"
        ],
        "meaning": "Cumulative net fees (after maker rebates) in pUSD."
      },
      {
        "name": "polytraders_gov_pnlreporter_maker_rebates_pusd_total",
        "type": "counter",
        "unit": "usd",
        "labels": [],
        "meaning": "Cumulative maker rebates accrued in pUSD."
      },
      {
        "name": "polytraders_gov_pnlreporter_quarantine_count",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of fill records currently quarantined (missing fee, fee cap exceeded, etc.)."
      },
      {
        "name": "polytraders_gov_pnlreporter_onchain_reconcile_drift_pusd",
        "type": "gauge",
        "unit": "usd",
        "labels": [],
        "meaning": "Absolute drift between aggregate P&L and on-chain pUSD balance at last reconciliation."
      }
    ],
    "alerts": [
      {
        "name": "PnLReporterFeeMissing",
        "condition": "rate(polytraders_gov_pnlreporter_quarantine_count[5m]) > 0",
        "severity": "page",
        "runbook": "#runbook-pnlreporter-fee-missing"
      },
      {
        "name": "PnLReporterOnchainReconcileDrift",
        "condition": "polytraders_gov_pnlreporter_onchain_reconcile_drift_pusd > 10",
        "severity": "page",
        "runbook": "#runbook-pnlreporter-reconcile-drift"
      },
      {
        "name": "PnLReporterNoFillsIn1h",
        "condition": "rate(polytraders_gov_pnlreporter_fills_processed_total[1h]) == 0",
        "severity": "warn",
        "runbook": "#runbook-pnlreporter-no-fills"
      },
      {
        "name": "PnLReporterFeeCap",
        "condition": "rate(polytraders_gov_pnlreporter_quarantine_count[5m]) > 5",
        "severity": "page",
        "runbook": "#runbook-pnlreporter-fee-cap"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Governance / PnL Reporter daily P&L and fee breakdown",
      "Grafana \u2014 Fee accounting / maker rebates vs gross fees (pUSD)"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "postgres",
    "shape": "pnl_ledger table: { fill_id, market_id, side, size_pusd, price, fee_bps_match_event, gross_fee_pusd, maker_rebate_pusd, net_fee_pusd, realised_pnl_pusd, unrealised_pnl_pusd, negRisk, fill_confirmed_at_ms, quarantined }. Aggregate reports stored in settlement_reports table.",
    "ttl": "7 years (regulatory retention); quarantined records retained indefinitely until manually reviewed",
    "recovery": "Postgres ledger is durable. On restart, in-flight fill events may be reprocessed \u2014 idempotency enforced by fill_id uniqueness constraint.",
    "size_estimate": "~1 KB per fill; ~150 KB/day at 150 fills/day; ~400 MB over 7 years"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop (per-fill hook) + scheduled window report goroutine",
    "max_in_flight": 500,
    "idempotency_key": "fill_id",
    "timeout_ms": 1000,
    "backpressure": "shed fill events beyond max_in_flight; emit WARN; process backlog on recovery",
    "locking": "Postgres unique constraint on fill_id; serializable isolation for aggregate report generation"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "gov.portfolio_sync",
        "why": "Position store snapshot is required to compute unrealised P&L on open positions."
      },
      {
        "bot_id": "gov.builder_attribution",
        "why": "Fill log joined for per-builder P&L breakdown and builder_fee_pusd."
      }
    ],
    "emits_to": [],
    "sibling": [
      {
        "bot_id": "gov.builderattribution",
        "why": "Shares the fill event stream; PnLReporter focuses on P&L, BuilderAttribution focuses on attribution."
      }
    ],
    "external": [
      {
        "service": "Polymarket CLOB v2 (fill events, mid-prices, market metadata)",
        "sla": "99.95% / 200ms p99 (Polymarket-published)",
        "fallback": "Use stale price for unrealised P&L; emit STALE_DATA warn; retry on next cycle."
      },
      {
        "service": "Polygon on-chain RPC (pUSD balance)",
        "sla": "99.9% / 500ms p99",
        "fallback": "Skip on-chain reconciliation for current window; flag report as unreconciled; retry."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Injecting a fill event with artificially low fee_bps to inflate net P&L",
      "Setting include_paper=true to mix paper and live fills in regulatory reports",
      "Manipulating the mark-to-market price feed to inflate unrealised P&L"
    ],
    "mitigations": [
      "Fee is always read from the match event \u2014 order-level fee manipulation is ignored",
      "Fee cap validation (taker <=100 bps, maker <=50 bps) rejects anomalous fills",
      "Regulatory reports are flagged if include_paper=true; paper fills are segregated",
      "On-chain balance reconciliation detects any P&L inflation that does not match actual pUSD flows",
      "Postgres unique constraint on fill_id prevents duplicate processing"
    ],
    "contract_calls": [
      {
        "contract": "CTFExchangeV2",
        "function": "matchOrders (read via event)",
        "purpose": "Reads fill events and operator-set fee_bps from match event for P&L computation."
      }
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2 on Polygon",
    "notes": "Fees are operator-set at match time in V2 \u2014 PnLReporter reads fee_bps exclusively from the CTFExchangeV2 match event, never from the signed order. Maker rebates are 20\u201325% of fees, paid in pUSD, per market. Builder fees: taker max 100 bps, maker max 50 bps, 1 bp granularity. All amounts denominated in pUSD. NegRiskAdapter payoff used for negative-risk market positions."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.0",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1 (USDC.e, feeRateBps on order)",
      "to": "v2 (pUSD, fee from match event)",
      "reason": "CLOB V2 cutover",
      "action_taken": "Removed feeRateBps from signed-order P&L pipeline; now reads fee_bps exclusively from CTFExchangeV2 match event. Updated all P&L amounts from USDC.e to pUSD denomination. Added maker rebate accrual (20\u201325% per market). Added NegRiskAdapter payoff path for negative-risk positions. Updated regulatory retention to 7y. Switched SDK to py-clob-client-v2."
    }
  ],
  "failure_injection": [
    {
      "scenario": "FEE_MISSING_FROM_MATCH_EVENT",
      "how_to_inject": "Emit a synthetic fill event without the fee_bps field",
      "expected_behavior": "PNL_REPORTER_FEE_MISSING raised; fill quarantined; not included in P&L totals",
      "recovery": "Manual review and re-ingestion of corrected fill event required."
    },
    {
      "scenario": "FEE_CAP_EXCEEDED",
      "how_to_inject": "Inject fill with match_event.fee_bps=120 (taker side)",
      "expected_behavior": "PNL_REPORTER_FEE_CAP_EXCEEDED raised; fill quarantined",
      "recovery": "Manual review; correct fee or reject fill."
    },
    {
      "scenario": "STALE_MIDPRICE",
      "how_to_inject": "Block CLOB public API price endpoint for 90s",
      "expected_behavior": "STALE_DATA warn emitted; unrealised P&L uses last known price",
      "recovery": "Once price endpoint recovers, next fill event re-fetches fresh price."
    },
    {
      "scenario": "ONCHAIN_RPC_FAILURE",
      "how_to_inject": "Block Polygon RPC calls during window report generation",
      "expected_behavior": "onchain_reconciled=false in SettlementReport; PNL_REPORTER_ONCHAIN_RECONCILE_FAIL alert",
      "recovery": "Retry reconciliation after RPC recovers; flag previous reports as pending reconciliation."
    },
    {
      "scenario": "NEGRISK_FLAG_MISSING",
      "how_to_inject": "Submit fill for a negative-risk market but set negRisk=false in market metadata",
      "expected_behavior": "Standard binary payoff used; WARN emitted if position size exceeds normal binary range",
      "recovery": "Update market metadata with correct negRisk flag; re-compute unrealised P&L."
    }
  ],
  "runbook": {
    "summary": "PnLReporter incidents involve missing fees in match events (critical \u2014 halts ledger for affected fills), on-chain reconciliation drift (regulatory flag), or stale price feeds (unrealised P&L degraded). Missing fee is always P1.",
    "oncall_actions": [
      {
        "alert": "PnLReporterFeeMissing",
        "first_step": "Identify which fills are quarantined; check match event schema for fee_bps field. May indicate a CLOB API format change.",
        "escalation": "Governance pod lead immediately"
      },
      {
        "alert": "PnLReporterOnchainReconcileDrift",
        "first_step": "Check Polygon RPC status. If RPC healthy, review pnl_ledger for quarantined fills that should be included.",
        "escalation": "Governance pod lead + finance team if drift persists > 1h"
      },
      {
        "alert": "PnLReporterNoFillsIn1h",
        "first_step": "Check CLOB feed and CTFExchangeV2 event stream. May be a market quiet period or a feed disconnection.",
        "escalation": "Exec pod lead if feed disconnection confirmed"
      },
      {
        "alert": "PnLReporterFeeCap",
        "first_step": "Review quarantined fills for fee_bps values. Check for operator mis-configuration of fee rates.",
        "escalation": "Governance pod lead"
      }
    ],
    "manual_overrides": [
      {
        "name": "reprocess_fill",
        "how": "polytraders gov pnl reprocess --fill-id <id> --reviewed-by <operator>",
        "when": "After manually verifying a quarantined fill, re-ingest it with corrected fields.",
        "command": "polytraders gov pnl reprocess --fill-id <id> --reviewed-by <operator>",
        "effect": "After manually verifying a quarantined fill, re-ingest it with corrected fields."
      }
    ],
    "healthcheck": "Endpoint: /internal/health/pnl-reporter | Green: Last fill processed < 300s ago (during active trading); Postgres pnl_ledger writable; quarantine_count not growing. | Red: No fills processed in > 1h during trading hours; Postgres write failure; quarantine_count growing at > 1/min."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass: fee from match event, maker rebate, fee cap, FIFO P&L, negRisk payoff",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Postgres pnl_ledger schema migration verified",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Fill processing latency p99 < 200ms per fill",
        "how_measured": "Performance test on 1000 fills",
        "threshold": "< 200ms"
      },
      {
        "gate": "Missing fee quarantine fires correctly",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "End-to-end: fill \u2192 SettlementReport \u2192 Postgres \u2192 on-chain reconciliation pass",
        "how_measured": "E2E test in staging with live Polygon RPC",
        "threshold": "Pass"
      },
      {
        "gate": "7-year retention tag applied to all SettlementReport records",
        "how_measured": "Postgres retention policy audit",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "SettlementReport"
    ],
    "topics": [
      "polytraders.reports.settlement"
    ],
    "cadence": "every-event",
    "retention_class": "7y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "wal-then-retry",
    "user_visible": "yes",
    "consumes_kinds": [
      "OperationsReport"
    ]
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 3,
    "phase_name": "Reporting & event store",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}