{
  "schema_version": "1.0.0",
  "bot_id": "0.2",
  "bot_name": "MarketQualityRanker",
  "slug": "marketqualityranker",
  "layer": "Discovery",
  "layer_key": "disc",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only",
    "Recommend"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": true,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Discovery",
    "bot_class": "Signal Service",
    "authority": "Read-only, Recommend",
    "runs_before": "Strategy OrderIntent generation",
    "runs_after": "MarketScanner scan cycle",
    "applies_to": "All markets surfaced by MarketScanner",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Intelligence pod"
  },
  "purpose": "Score every market across volume, spread, depth, dispute history, rule clarity, implied volatility, and time-to-resolution into a single quality rank. Sits before every strategy and replaces ad-hoc per-strategy quality checks with a shared, consistent score.",
  "why_it_matters": [
    {
      "failure": "No shared quality score",
      "consequence": "Each strategy re-derives its own quality filter, producing inconsistent rankings and wasted per-strategy compute.",
      "worked_example": {
        "setup": "A strategy is allowed to trade any market with `quality_score >= 0.6`. Market 0x9c2 has quality_score=0.42 (low depth, wide spread, recent rule edit pending review).",
        "without_bot": "Without a ranker, the strategy uses its own heuristics. Two strategies disagree about whether 0x9c2 is tradeable, one enters, the other doesn't, and the team has no shared definition of 'tradeable' to debug against.",
        "with_bot": "MarketQualityRanker emits `quality_score=0.42, breakdown={depth: 0.3, spread: 0.5, rule_state: 0.2}`. All strategies read the same number, the entry is rejected by Risk with reason `QUALITY_BELOW_THRESHOLD`, and the breakdown explains why."
      }
    },
    {
      "failure": "High-dispute markets not penalised",
      "consequence": "Markets with frequent UMA disputes carry elevated resolution risk; without a dispute-penalty weight they appear equivalent to clean markets."
    },
    {
      "failure": "Short-horizon markets ignored",
      "consequence": "Markets resolving in <24h may have inflated spreads or thin books that are artefacts of the final trading window, not genuine signals."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Book depth, spread, and 24h volume per market",
      "source": "CLOB + Data API",
      "required": true,
      "use": "Compute liquidity sub-score."
    },
    {
      "input": "Market resolution rules text and neg-risk flag",
      "source": "Gamma API",
      "required": true,
      "use": "Derive rule-clarity sub-score and flag ambiguous resolution criteria."
    },
    {
      "input": "Historical dispute frequency for UMA markets",
      "source": "Data API / onchain",
      "required": false,
      "use": "Penalise markets with elevated dispute history in the composite score."
    }
  ],
  "internal_inputs": [
    {
      "input": "MarketScanner candidate list",
      "source": "disc.marketscanner",
      "required": true,
      "use": "Only score markets that have already passed MarketScanner tradability filters."
    },
    {
      "input": "KillSwitch active flag",
      "source": "risk.kill_switch",
      "required": true,
      "use": "Suppress quality report emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "min_quality_score \u00b7 0\u20131",
    "weight_liquidity \u00b7 float",
    "weight_rule_clarity \u00b7 float",
    "weight_resolution_horizon \u00b7 float"
  ],
  "parameters": [
    {
      "name": "min_quality_score",
      "default": 0.4,
      "warning": 0.3,
      "hard": 0.2,
      "controls": "Minimum composite quality score a market must achieve to be forwarded to strategies.",
      "why_default_matters": "A floor of 0.4 filters the bottom tier of markets while leaving a broad set available for diverse strategies.",
      "threshold_logic": [
        {
          "condition": "score >= 0.4",
          "action": "Forward to strategy layer"
        },
        {
          "condition": "0.3\u20130.4",
          "action": "Forward with WARN annotation"
        },
        {
          "condition": "< 0.2",
          "action": "Drop \u2014 LOW_QUALITY_SCORE hard reject"
        }
      ],
      "dev_check": "if (score < params.hard) emit(HARD_REJECT, 'LOW_QUALITY_SCORE');",
      "user_facing": "Markets that score poorly across volume, spreads, and clarity are not surfaced as opportunities."
    },
    {
      "name": "weight_liquidity",
      "default": 0.4,
      "warning": 0.1,
      "hard": 0.0,
      "controls": "Weight applied to the liquidity sub-score (depth \u00d7 volume) in the composite.",
      "why_default_matters": "Liquidity is the strongest predictor of execution quality, so it receives the highest default weight.",
      "threshold_logic": [
        {
          "condition": "weight >= 0.1",
          "action": "Normal"
        },
        {
          "condition": "< 0.0",
          "action": "Reject config \u2014 weight must be non-negative"
        }
      ],
      "dev_check": "if (w < 0) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Liquid markets with deep order books are ranked higher."
    },
    {
      "name": "weight_rule_clarity",
      "default": 0.35,
      "warning": 0.1,
      "hard": 0.0,
      "controls": "Weight applied to the rule-clarity sub-score derived from resolution text analysis.",
      "why_default_matters": "Ambiguous resolution criteria are a leading cause of disputed outcomes; high weight keeps them penalised.",
      "threshold_logic": [
        {
          "condition": "weight >= 0.1",
          "action": "Normal"
        },
        {
          "condition": "< 0.0",
          "action": "Reject config"
        }
      ],
      "dev_check": "if (w < 0) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Markets with clear, unambiguous resolution rules are ranked higher."
    }
  ],
  "default_config": {
    "bot_id": "disc.market_quality_ranker",
    "version": "0.1.0",
    "mode": "shadow_only",
    "defaults": {
      "min_quality_score": 0.4,
      "weight_liquidity": 0.4,
      "weight_rule_clarity": 0.35,
      "weight_resolution_horizon": 0.25
    }
  },
  "implementation_flow": [
    "On each scoring cycle, receive the current MarketScanner candidate list.",
    "Check KillSwitch; if active, halt emissions.",
    "For each candidate, fetch rule text from Gamma API and compute rule_clarity_score via heuristics (ambiguity keywords, missing resolution date, etc.).",
    "Fetch 30-day dispute history from Data API; compute dispute_penalty = disputes_30d / 10, capped at 0.3.",
    "Compute liquidity_score = normalise(volume_24h, book_depth_usd) against rolling 30-day distribution.",
    "Compute resolution_horizon_score = 1 / (1 + exp(-time_to_res_days)); short horizons score lower.",
    "Composite score = weight_liquidity * liquidity_score + weight_rule_clarity * (rule_clarity_score - dispute_penalty) + weight_resolution_horizon * resolution_horizon_score.",
    "Emit ObservationReport for each market with composite score, sub-scores, and any WARN/HARD_REJECT codes.",
    "Log cycle summary with top-ranked and bottom-ranked markets."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 MarketQualityRanker emits ObservationReports, not approvals.",
    "reshape_required": "Not applicable \u2014 read-only scoring bot.",
    "reject": "Markets scoring below min_quality_score hard floor receive LOW_QUALITY_SCORE and are not forwarded to strategies.",
    "warning_only": "Markets between warning and hard threshold are forwarded with a LOW_QUALITY_SCORE WARN annotation."
  },
  "decision_output_schema": "ObservationReport",
  "decision_output_example": {
    "report_id": "0xaabb1122334455667788990011223344aabb1122334455667788990011223344",
    "bot_id": "disc.market_quality_ranker",
    "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
    "quality_score": 0.71,
    "sub_scores": {
      "liquidity": 0.82,
      "rule_clarity": 0.65,
      "resolution_horizon": 0.6
    },
    "dispute_penalty": 0.0,
    "warnings": [],
    "ranked_at_ms": 1746789000000
  },
  "developer_log": {
    "bot_id": "disc.market_quality_ranker",
    "cycle": 42,
    "markets_scored": 47,
    "markets_forwarded": 38,
    "markets_dropped_low_quality": 9,
    "top_market": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
    "top_score": 0.71,
    "killswitch_active": false,
    "scored_at": "2026-05-09T11:30:00Z"
  },
  "user_explanations": [
    {
      "situation": "A market does not appear as an opportunity",
      "message": "This market scored below the quality threshold \u2014 it may have low trading volume, ambiguous resolution rules, or an elevated dispute history."
    },
    {
      "situation": "Opportunity shown with a quality warning",
      "message": "This market passed the minimum quality floor but ranks in the lower tier. Strategies will apply additional size restrictions."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Rule clarity scoring may incorrectly penalise well-written but uncommon resolution language, causing valid markets to score below threshold.",
    "false_positive_risk": "A market with temporarily high volume inflating its liquidity sub-score could receive a high quality rank despite underlying fragility.",
    "false_negative_risk": "Data API dispute history unavailable causes dispute_penalty to default to 0, potentially over-ranking disputable markets.",
    "safe_fallback": "If Gamma API or Data API are unavailable, halt emissions for the affected cycle with STALE_MARKET_DATA rather than emitting stale scores.",
    "required_dependencies": [
      "MarketScanner candidate list",
      "Gamma API market rules text",
      "Data API volume and dispute history",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Market below hard floor emits LOW_QUALITY_SCORE HARD_REJECT",
        "setup": "composite_score=0.15, hard=0.2",
        "expected": "Market dropped from candidate list with reason LOW_QUALITY_SCORE"
      },
      {
        "test": "Dispute penalty caps at 0.3",
        "setup": "disputes_30d=50",
        "expected": "dispute_penalty=0.3, not 5.0"
      },
      {
        "test": "KillSwitch suppresses emissions",
        "setup": "killswitch.active=true",
        "expected": "No ObservationReports emitted; scoring runs internally"
      }
    ],
    "integration": [
      {
        "test": "End-to-end: MarketScanner candidate becomes ranked ObservationReport",
        "expected": "ObservationReport includes all sub-scores and is consumed downstream by OpportunityQueue"
      },
      {
        "test": "Gamma API unavailability halts cycle with STALE_MARKET_DATA",
        "expected": "No reports emitted; next cycle resumes when API recovers"
      }
    ],
    "property": [
      {
        "property": "composite_score always in [0, 1]",
        "required": "Always true"
      },
      {
        "property": "No report emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Score markets across volume, spread, depth, dispute risk, rule clarity, volatility, and time-to-resolution. Sits before every strategy.",
  "legacy_pm_signals": [
    "Book depth and spread per market",
    "Market resolution rules + ambiguity score",
    "Dispute state and historical dispute frequency",
    "Mid-price drift over rolling windows"
  ],
  "legacy_external_feeds": [
    "UMA Optimistic Oracle history"
  ],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "data",
    "clob_public",
    "internal"
  ],
  "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)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "All volume and depth figures denominated in pUSD; neg-risk flag from Gamma API used to apply stricter rule-clarity checks on augmented open-set markets."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION scoringCycle():\n  ks = FETCH internal.killswitch.status\n  IF ks.active: RETURN  // halt emissions\n\n  candidates = FETCH disc.marketscanner.latest_candidates()\n  IF candidates IS NULL:\n    LOG ERROR 'MarketScanner candidates unavailable'\n    RETURN\n\n  FOR market IN candidates:\n    rules_text = FETCH gamma.GET('/market/' + market.condition_id + '/rules')\n    IF rules_text IS NULL: CONTINUE  // skip; STALE_MARKET_DATA\n\n    rule_clarity = scoreRuleClarity(rules_text)\n    disputes = FETCH data_api.GET('/disputes?market=' + market.condition_id + '&window=30d')\n    dispute_penalty = MIN(0.3, (disputes.count OR 0) / 10)\n\n    liq_score = normalise(market.volume_24h_usd, market.book_depth_usd)\n    horizon_score = horizonScore(market.resolution_date)\n\n    score = (params.weight_liquidity * liq_score\n           + params.weight_rule_clarity * (rule_clarity - dispute_penalty)\n           + params.weight_resolution_horizon * horizon_score)\n    score = CLAMP(score, 0.0, 1.0)\n\n    IF score < params.min_quality_score.hard:\n      LOG reason=LOW_QUALITY_SCORE; CONTINUE\n\n    warnings = []\n    IF score < params.min_quality_score.default:\n      warnings.append('LOW_QUALITY_SCORE')\n\n    EMIT ObservationReport(market_id, score, liq_score, rule_clarity, horizon_score,\n                           dispute_penalty, warnings)\n\n  LOG cycle summary",
    "sdk_calls": [
      "gamma.GET('/market/<condition_id>/rules')",
      "data_api.GET('/disputes?market=<condition_id>&window=30d')",
      "data_api.GET('/volume?market=<condition_id>&window=24h')"
    ],
    "complexity": "O(M) where M = number of MarketScanner candidates per cycle"
  },
  "wire_examples": {
    "input": [
      {
        "label": "MarketScanner candidate passed to ranker",
        "source": "disc.marketscanner",
        "payload": {
          "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
          "volume_24h_usd": 8540,
          "book_depth_usd": 3200,
          "spread_bps": 210,
          "neg_risk": false,
          "resolution_date": "2026-06-01T00:00:00Z"
        }
      }
    ],
    "output": [
      {
        "label": "ObservationReport \u2014 high-quality market",
        "payload": {
          "report_id": "0xaabb1122334455667788990011223344aabb1122334455667788990011223344",
          "bot_id": "disc.market_quality_ranker",
          "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
          "quality_score": 0.71,
          "sub_scores": {
            "liquidity": 0.82,
            "rule_clarity": 0.65,
            "resolution_horizon": 0.6
          },
          "dispute_penalty": 0.0,
          "warnings": [],
          "ranked_at_ms": 1746789000000
        }
      }
    ],
    "curl": "curl 'https://gamma-api.polymarket.com/market/0x7f8a9b.../rules'"
  },
  "reason_codes": [
    {
      "code": "LOW_QUALITY_SCORE",
      "severity": "HARD_REJECT",
      "meaning": "Composite quality score is below the hard floor; market dropped from candidate list.",
      "action": "Do not forward to strategy layer; log with sub-scores for debugging.",
      "user_message": "This market scored too low across volume, clarity, and depth to be surfaced as an opportunity."
    },
    {
      "code": "LOW_QUALITY_SCORE",
      "severity": "WARN",
      "meaning": "Score is between warning and hard floor; market forwarded with quality warning.",
      "action": "Include in ObservationReport with WARN annotation.",
      "user_message": "This market passed minimum quality but ranks in the lower tier."
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Gamma API or Data API unavailable; scoring cycle halted.",
      "action": "Halt emissions for this cycle; retry next cycle.",
      "user_message": ""
    },
    {
      "code": "RULE_AMBIGUITY_DETECTED",
      "severity": "EXPLAIN",
      "meaning": "Market resolution text contains ambiguity keywords that lower the rule_clarity sub-score.",
      "action": "Penalise rule_clarity sub-score; annotate report.",
      "user_message": "The resolution rules for this market contain language that may be open to interpretation."
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch is active; all report emissions suppressed.",
      "action": "Return immediately; do not emit any ObservationReports.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_disc_marketqualityranker_markets_scored_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "cycle"
        ],
        "meaning": "Total markets scored per cycle."
      },
      {
        "name": "polytraders_disc_marketqualityranker_reports_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "ObservationReports successfully emitted to the bus."
      },
      {
        "name": "polytraders_disc_marketqualityranker_quality_score",
        "type": "histogram",
        "unit": "ratio",
        "labels": [],
        "meaning": "Distribution of composite quality scores across all markets in a cycle."
      },
      {
        "name": "polytraders_disc_marketqualityranker_dropped_low_quality_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Markets dropped due to LOW_QUALITY_SCORE hard reject."
      }
    ],
    "alerts": [
      {
        "name": "MarketQualityRankerAllDropped",
        "condition": "polytraders_disc_marketqualityranker_dropped_low_quality_total / polytraders_disc_marketqualityranker_markets_scored_total > 0.9",
        "severity": "P2",
        "runbook": "#runbook-marketqualityranker-all-dropped"
      },
      {
        "name": "MarketQualityRankerNoEmissions",
        "condition": "rate(polytraders_disc_marketqualityranker_reports_emitted_total[10m]) == 0",
        "severity": "P1",
        "runbook": "#runbook-marketqualityranker-no-emissions"
      },
      {
        "name": "MarketQualityRankerLowScoreSkew",
        "condition": "histogram_quantile(0.5, polytraders_disc_marketqualityranker_quality_score) < 0.3",
        "severity": "P3",
        "runbook": "#runbook-marketqualityranker-low-score-skew"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Discovery / MarketQualityRanker score distribution"
    ],
    "log_levels": {
      "DEBUG": "Per-market sub-scores (liquidity, rule_clarity, horizon, dispute_penalty).",
      "INFO": "Cycle summary: markets_scored, markets_forwarded, markets_dropped.",
      "WARN": "Gamma API slow; >90% markets dropped.",
      "ERROR": "Gamma API unavailable; Data API unavailable."
    }
  },
  "state": {
    "store": "in-memory rolling 30-day volume distribution for normalisation",
    "shape": "{ condition_id -> { p50_vol: float, p95_vol: float, updated_at: ts } }",
    "ttl": "30 days rolling; evict on market closure",
    "recovery": "On cold start, normalisation uses the first cycle's values as a warm-up baseline.",
    "size_estimate": "~500 B per market; ~500 markets \u2192 ~250 KB"
  },
  "concurrency": {
    "execution_model": "single-threaded async loop",
    "max_in_flight": 1,
    "idempotency_key": "cycle_id",
    "timeout_ms": 8000,
    "backpressure": "drop newest",
    "locking": "none"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "disc.marketscanner",
        "why": "Provides the candidate market list that MarketQualityRanker scores.",
        "contract": "Expects a list of markets with volume_24h_usd, book_depth_usd, spread_bps, and resolution_date."
      },
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate suppresses all emissions when active.",
        "contract": "If KillSwitch active, scoring runs but no ObservationReports are emitted."
      }
    ],
    "emits_to": [
      {
        "bot_id": "disc.opportunityqueue",
        "why": "OpportunityQueue consumes quality scores to rank its output.",
        "contract": "ObservationReport includes quality_score and sub-scores."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "Halt cycle; retry next interval."
      },
      {
        "service": "Data API",
        "endpoint": "https://data-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "Default dispute_penalty to 0; log warning."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Gamma API returning crafted resolution text to manipulate rule_clarity scores",
      "Data API returning artificially low dispute counts to inflate quality scores"
    ],
    "mitigations": [
      "Rule clarity scoring uses local heuristics, not API-provided scores",
      "All ObservationReports are recommendations only; downstream bots independently validate"
    ]
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block TCP to gamma-api.polymarket.com",
      "expected_behaviour": "Cycle halted; STALE_MARKET_DATA logged; no reports emitted",
      "recovery": "Automatic on next cycle when API recovers."
    },
    {
      "scenario": "ALL_MARKETS_LOW_QUALITY",
      "how_to_inject": "Set weight_rule_clarity=0.9 and inject markets with ambiguous rules text",
      "expected_behaviour": "All markets dropped; MarketQualityRankerAllDropped alert fires",
      "recovery": "Tune weights or fix resolution text."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behaviour": "Scoring runs internally; zero ObservationReports emitted",
      "recovery": "Emissions resume on next cycle after KillSwitch reset."
    }
  ],
  "runbook": {
    "summary": "MarketQualityRanker incidents are usually upstream data issues or threshold drift. Bot is read-only; incidents do not affect active positions.",
    "oncall_actions": [
      {
        "alert": "MarketQualityRankerNoEmissions",
        "first_action": "Check KillSwitch status and Gamma API health.",
        "escalate_to": "Intelligence pod lead after 10 minutes."
      },
      {
        "alert": "MarketQualityRankerAllDropped",
        "first_action": "Inspect cycle log for sub-score distribution; check if weight config changed.",
        "escalate_to": "Risk pod review before adjusting weights."
      }
    ],
    "manual_overrides": [
      {
        "name": "pause-scoring",
        "how": "polytraders bot pause disc.market_quality_ranker",
        "when": "Upstream data degradation causing spurious quality drops."
      }
    ],
    "healthcheck": "GET /internal/health/marketqualityranker \u2192 green if Last cycle completed within 2\u00d7 scoring interval and at least one report emitted.; red if No cycle completed in 2\u00d7 interval or zero reports for 10 minutes."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for all threshold cases and dispute penalty cap",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Quality score distribution is stable over 48h shadow run",
        "how_measured": "polytraders_disc_marketqualityranker_quality_score histogram p50 > 0.35",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero false STALE_MARKET_DATA halts during normal operation over 7 days",
        "how_measured": "Alert history",
        "threshold": "0 firings"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "retention_class": "30d",
    "cadence": "every-N",
    "sampling_rule": "sample-1/N",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "summary-only",
    "consumes_kinds": []
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 2,
    "phase_name": "Data normalisation",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}