6.16 ExposureExplainer
Produces a plain-English narrative of current portfolio exposure: which markets, which outcomes, how much pUSD is at risk, why each position was opened (linked back to the originating strategy and OrderIntent). Read by the Admin UI, the daily ops digest, and any incident review.
v3 readiness
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Governance Governance |
|---|---|
| Bot class | Governance |
| Authority | Explain |
| Status | PLANNED |
| Readiness | Spec ready |
| Runs before | — |
| Runs after | exec.order_lifecycle_manager |
| Applies to | Continuous |
| Default mode | shadow |
| User-visible | Yes |
| Developer owner | Governance pod |
Operational profile
| Ownership | Governance pod · on-call gov-oncall · #polytraders-gov · escalates to Head of Governance · P3 |
|---|---|
| Latency budget | 1500ms |
| Modes supported | offshadowadvisoryenforced |
| Data freshness | max_market_data_age_ms=60000 · max_orderbook_age_ms=60000 · on stale → Emit fallback narrative; do not render misleading data. |
| Human override | yes · by Governance on-call · logs GOV_EXPOSURE_OVERRIDE · time-bound: Single emission · scope: Single narrative · single approver |
2. Purpose
Produces a plain-English narrative of current portfolio exposure: which markets, which outcomes, how much pUSD is at risk, why each position was opened (linked back to the originating strategy and OrderIntent). Read by the Admin UI, the daily ops digest, and any incident review.
3. Why This Bot Matters
Opaque positions
A page of raw outcome IDs and notionals does not tell anyone whether the portfolio is balanced, concentrated, or running through resolution.
Audit trail
When something goes wrong, the first question is always 'why was that position open?' — without ExposureExplainer, the answer requires correlating five logs by hand.
Daily review fatigue
A narrative rendering compresses 200 line items into the 5 things a reviewer actually needs to read.
No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.
4. Required Polymarket Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Resolved outcome positions | On-chain CTFExchangeV2 + PortfolioGuard | Yes | Quantities by token. |
| Market metadata | Polymarket REST | Yes | Friendly market names and outcome labels. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Trade history | exec.order_lifecycle_manager | Yes | Originating intent_id and strategy_id per fill. |
| Strategy registry | Config | Yes | Maps strategy_id to friendly description. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| digest_cadence_minutes | 60 | — | — | How often a fresh narrative is emitted to the ops feed. |
| concentration_warn_pct | 25 | 15 | 25 | Single-market exposure (% of bankroll) at which the narrative flags concentration risk. |
7. Detailed Parameter Instructions
digest_cadence_minutes
What it means
How often a fresh narrative is emitted to the ops feed.
Default
{ "digest_cadence_minutes": 60 }
Why this default matters
Hourly is the right grain for trading hours; daily for overnight.
Threshold logic
| Condition | Action |
|---|---|
| 60 | Default |
Developer check
schedule.every(p.digest_cadence_minutes).do(emit_narrative);
User-facing English
(Internal — not user-facing.)
concentration_warn_pct
What it means
Single-market exposure (% of bankroll) at which the narrative flags concentration risk.
Default
{ "concentration_warn_pct": 25 }
Why this default matters
25% in one market is significant; flagging keeps reviewers honest.
Threshold logic
| Condition | Action |
|---|---|
| ≤ 15% | Silent |
| 15–25% | MENTION |
| > 25% | FLAG |
Developer check
if (mkt.exposure_pct > p.concentration_warn_pct) flag(mkt);
User-facing English
(Internal — not user-facing.)
8. Default Configuration
{
"digest_cadence_minutes": 60,
"concentration_warn_pct": 25
}9. Implementation Flow
— not yet authored —
10. Reference Implementation
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.
positions = portfolio.snapshot()
rows = [(p.strategy_id, p.market_id, p.outcome_id, p.size_usd) for p in positions]
rows.sort(key=lambda r: -r[3])
for r in rows:
narrative.append(render_row(r))
if exposure_pct(r) > p.concentration_warn_pct: flags.append(r.market_id)
emit('ExposureNarrative', narrative, flags)11. Wire Examples
Input — what arrives on the wire
{
"snapshot_ts_ms": 1715260000000,
"positions": [
{
"strategy_id": "event_drift",
"market_id": "0xabc",
"outcome_id": "YES",
"size_usd": 1200
}
]
}
Output — what the bot emits
{
"kind": "ExposureNarrative",
"summary_markdown": "## Exposure as of 14:00 UTC\n- Strategy 'event_drift' is long $1,200 on YES in market 0xabc.\n",
"concentration_flags": []
}12. Decision Logic
APPROVE
Group by strategy → market → outcome. Annotate each row with originating intent and decision rationale.
RESHAPE_REQUIRED
This bot does not reshape orders.
REJECT
No reject path defined for this bot — it is observe-only.
WARNING_ONLY
Highlight concentration above warn threshold.
13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"kind": "ExposureNarrative",
"summary_markdown": "## Exposure as of 14:00 UTC\n- Strategy 'event_drift' is long $1,200 on YES in market 0xabc (Trump Q1 announcement); concentration 18%.\n- Strategy 'late_unwind' is short $400 on NO in market 0xdef.\n",
"concentration_flags": []
}14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
GOV_EXPOSURE_NARRATIVE | P3 | Gov Exposure Narrative | See decision output and developer log for context. | Hourly summary of every open position the system holds, in plain language. |
GOV_EXPOSURE_CONCENTRATION_FLAG | P3 | Gov Exposure Concentration Flag | See decision output and developer log for context. | Hourly summary of every open position the system holds, in plain language. |
GOV_EXPOSURE_FALLBACK | P3 | Gov Exposure Fallback | See decision output and developer log for context. | Hourly summary of every open position the system holds, in plain language. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
narratives_emitted_total | counter | event | bot_id | Narratives emitted total. |
concentration_flags_total | counter | event | bot_id | Concentration flags total. |
narrative_render_failures_total | counter | event | bot_id | Narrative render failures total. |
Dashboards
- 6.16 overview dashboard
16. Developer Reporting
"Per emission: digest_id, ts_ms, position_count, total_exposure_usd, flagged_markets."17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| When this bot acts | Hourly summary of every open position the system holds, in plain language. |
18. Failure-Mode Block
| main_failure_mode | Stale strategy registry produces narratives that label positions with the wrong strategy. |
|---|---|
| false_positive_risk | Concentration flag fires for a position that is intentionally large; mitigation: allow per-strategy whitelist with required reviewer notes. |
| false_negative_risk | On-chain position is held outside the standard portfolio path and is missed; mitigation: reconcile against on-chain CTFExchangeV2 token balances every digest. |
| safe_fallback | On any data fetch failure, emit a narrative containing only the failure summary plus the last successful narrative's timestamp. Never produce a misleading narrative. |
| required_dependencies | — |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
Drop the strategy registry and assert fallback narrative renders | Drop the strategy registry and assert fallback narrative renders. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
Inject a position with no originating intent and assert it is flagged as ORPHAN | Inject a position with no originating intent and assert it is flagged as ORPHAN. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
20. State & Persistence
Last successful narrative cached for fallback emission.
State stores
| Name | Kind | Key | Value shape | TTL | Durability |
|---|---|---|---|---|---|
exposure_explainer_state | in-memory + fast KV mirror | bot_id | Last successful narrative cached for fallback emission. | 24h | crash-safe via KV mirror |
Cold-start recovery
Cold-start hydrates from fast KV; missing keys default to safe fallback.
On restart
All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | Single emitter. Reads upstream snapshots only. |
| Max in-flight | 32 |
| Idempotency key | order_intent_id |
| Replay-safe | True |
| Deduplication | By idempotency_key within a 60s window. |
| Ordering guarantees | Per-market_id FIFO; cross-market unordered. |
| Per-call timeout (ms) | 250 |
| Backpressure strategy | Bounded queue; oldest-dropped with metric increment when full. |
| Locking / mutual exclusion | Per-market_id mutex; no global locks. |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| exec.order_lifecycle_manager |
Requires (graph.requires)
| Consumes | PortfolioSnapshot TradeHistory |
|---|---|
| Emits | OperationsReport(kind=ExposureNarrative) |
| Blocks orders | no |
23. Security Surfaces
Read-only on portfolio + history. Emits to internal ops feed only.
Signing surface
None — bot does not sign or submit.
Mitigations
- Rate-limit per source
- Audit-log every override
- Require role-based authz on admin paths
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|---|
| CLOB version | V2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | yes |
| SDK used | Polymarket CLOB V2 SDK |
| Settlement contract | CTFExchangeV2 |
| Notes | Reads V2 token balances and lifecycle state. |
25. Versioning & Migration
| Field | Value |
|---|---|
| current | 0.1.0 |
| contract_version | 1.0.0 |
| last_breaking_change | none |
| deprecation_window_days | 30 |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Concentration > 25% flags the market. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
| Empty portfolio renders 'No open positions.'. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
Integration Tests
| Test | Expected result |
|---|---|
| Run against a recorded portfolio snapshot; the rendered narrative matches the golden file. | End-to-end behaviour matches the spec without manual intervention. |
Property Tests
| Property | Required behaviour |
|---|---|
| Narrative line count is bounded by position_count + flagged_markets + 5. | Always true across all generated inputs. |
27. Operational Runbook
If narratives stop emitting: check the digest scheduler heartbeat in monitoring.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
6.16_anomaly | Open the bot's reporting page and confirm the alert is real (not a metric hiccup). | Inspect developer log entries for the affected market_id over the last 30 minutes. | Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner. | Governance pod |
Manual overrides
polytraders bot pause 6.16— Disables the bot's enforcement layer; downstream consumers fall back to safe defaults.
Healthcheck
GET /healthz/exposure_explainer → 200 if last successful evaluation < 60s ago.28. Promotion Gates
A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.
Promote to Shadow
| Gate | How measured | Threshold |
|---|---|---|
| Stub | golden-file diff. | Documented threshold met for the full window. |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| Shadow | 14 days; narratives reviewed by ops. | Documented threshold met for the full window. |
| Advisory | 7 days. | Documented threshold met for the full window. |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| Enforced | feeds the daily ops digest. | Documented threshold met for the full window. |
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|---|
| Purpose defined | ✓ done |
| Required inputs listed | ✓ done |
| Parameters defined | ✓ done |
| Defaults defined | ✓ done |
| Warning thresholds defined | ✓ done |
| Hard thresholds defined | ✓ done |
| Safe fallback defined | ✓ done |
| Structured output defined | ✓ done |
| Developer log defined | ✓ done |
| Plain-English explanation | ✓ done |
| Unit tests defined | ✓ done |
| Integration tests defined | ✓ done |
| Property tests defined | ✓ done |
| Failure-mode block complete | ✓ done |
| Reference implementation pseudocode | ✓ done |
| Wire examples (input + output) | ✓ done |
| Reason codes listed | ✓ done |
| Metrics & logs defined | ✓ done |
| State & persistence defined | ✓ done |
| Concurrency & idempotency defined | ✓ done |
| Dependencies declared | ✓ done |
| Security surfaces declared | ✓ done |
| Polymarket V2 compatibility declared | ✓ done |
| Version & migration history declared | ✓ done |
| Operational runbook defined | ✓ done |
| Promotion gates defined | ✓ done |
| Failure-injection recipes defined | ✓ done |