Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerDiscovery0.2 MarketQualityRanker

0.2 MarketQualityRanker

Discovery Signal Service Read-onlyRecommend PLANNED Spec started capital · Indirect P2 · Data normalisation pending flagship stub

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.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerDiscovery  Discovery
Bot classSignal Service
AuthorityRead-onlyRecommend
StatusPLANNED
ReadinessSpec started
Runs beforeStrategy OrderIntent generation
Runs afterMarketScanner scan cycle
Applies toAll markets surfaced by MarketScanner
Default modeshadow_only
User-visibleAdvanced details only
Developer ownerPolytraders core — Intelligence pod

2. 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.

3. Why This Bot Matters

  • No shared quality score

    Each strategy re-derives its own quality filter, producing inconsistent rankings and wasted per-strategy compute.

  • High-dispute markets not penalised

    Markets with frequent UMA disputes carry elevated resolution risk; without a dispute-penalty weight they appear equivalent to clean markets.

  • Short-horizon markets ignored

    Markets resolving in <24h may have inflated spreads or thin books that are artefacts of the final trading window, not genuine signals.

4. Required Polymarket Inputs

InputSourceRequired?Use
Book depth, spread, and 24h volume per marketCLOB + Data APIYesCompute liquidity sub-score.
Market resolution rules text and neg-risk flagGamma APIYesDerive rule-clarity sub-score and flag ambiguous resolution criteria.
Historical dispute frequency for UMA marketsData API / onchainNoPenalise markets with elevated dispute history in the composite score.

5. Required Internal Inputs

InputSourceRequired?Use
MarketScanner candidate listdisc.marketscannerYesOnly score markets that have already passed MarketScanner tradability filters.
KillSwitch active flagrisk.kill_switchYesSuppress quality report emissions when KillSwitch is active.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_quality_score0.40.30.2Minimum composite quality score a market must achieve to be forwarded to strategies.
weight_liquidity0.40.10.0Weight applied to the liquidity sub-score (depth × volume) in the composite.
weight_rule_clarity0.350.10.0Weight applied to the rule-clarity sub-score derived from resolution text analysis.

7. Detailed Parameter Instructions

min_quality_score

What it means

Minimum composite quality score a market must achieve to be forwarded to strategies.

Default

{ "min_quality_score": 0.4 }

Why this default matters

A floor of 0.4 filters the bottom tier of markets while leaving a broad set available for diverse strategies.

Threshold logic

ConditionAction
score >= 0.4Forward to strategy layer
0.3–0.4Forward with WARN annotation
< 0.2Drop — LOW_QUALITY_SCORE hard reject

Developer check

if (score < params.hard) emit(HARD_REJECT, 'LOW_QUALITY_SCORE');

User-facing English

Markets that score poorly across volume, spreads, and clarity are not surfaced as opportunities.

weight_liquidity

What it means

Weight applied to the liquidity sub-score (depth × volume) in the composite.

Default

{ "weight_liquidity": 0.4 }

Why this default matters

Liquidity is the strongest predictor of execution quality, so it receives the highest default weight.

Threshold logic

ConditionAction
weight >= 0.1Normal
< 0.0Reject config — weight must be non-negative

Developer check

if (w < 0) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Liquid markets with deep order books are ranked higher.

weight_rule_clarity

What it means

Weight applied to the rule-clarity sub-score derived from resolution text analysis.

Default

{ "weight_rule_clarity": 0.35 }

Why this default matters

Ambiguous resolution criteria are a leading cause of disputed outcomes; high weight keeps them penalised.

Threshold logic

ConditionAction
weight >= 0.1Normal
< 0.0Reject config

Developer check

if (w < 0) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Markets with clear, unambiguous resolution rules are ranked higher.

8. Default Configuration

{
  "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
  }
}

9. Implementation Flow

  1. On each scoring cycle, receive the current MarketScanner candidate list.
  2. Check KillSwitch; if active, halt emissions.
  3. For each candidate, fetch rule text from Gamma API and compute rule_clarity_score via heuristics (ambiguity keywords, missing resolution date, etc.).
  4. Fetch 30-day dispute history from Data API; compute dispute_penalty = disputes_30d / 10, capped at 0.3.
  5. Compute liquidity_score = normalise(volume_24h, book_depth_usd) against rolling 30-day distribution.
  6. Compute resolution_horizon_score = 1 / (1 + exp(-time_to_res_days)); short horizons score lower.
  7. Composite score = weight_liquidity * liquidity_score + weight_rule_clarity * (rule_clarity_score - dispute_penalty) + weight_resolution_horizon * resolution_horizon_score.
  8. Emit ObservationReport for each market with composite score, sub-scores, and any WARN/HARD_REJECT codes.
  9. Log cycle summary with top-ranked and bottom-ranked markets.

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.

FUNCTION scoringCycle():
  ks = FETCH internal.killswitch.status
  IF ks.active: RETURN  // halt emissions

  candidates = FETCH disc.marketscanner.latest_candidates()
  IF candidates IS NULL:
    LOG ERROR 'MarketScanner candidates unavailable'
    RETURN

  FOR market IN candidates:
    rules_text = FETCH gamma.GET('/market/' + market.condition_id + '/rules')
    IF rules_text IS NULL: CONTINUE  // skip; STALE_MARKET_DATA

    rule_clarity = scoreRuleClarity(rules_text)
    disputes = FETCH data_api.GET('/disputes?market=' + market.condition_id + '&window=30d')
    dispute_penalty = MIN(0.3, (disputes.count OR 0) / 10)

    liq_score = normalise(market.volume_24h_usd, market.book_depth_usd)
    horizon_score = horizonScore(market.resolution_date)

    score = (params.weight_liquidity * liq_score
           + params.weight_rule_clarity * (rule_clarity - dispute_penalty)
           + params.weight_resolution_horizon * horizon_score)
    score = CLAMP(score, 0.0, 1.0)

    IF score < params.min_quality_score.hard:
      LOG reason=LOW_QUALITY_SCORE; CONTINUE

    warnings = []
    IF score < params.min_quality_score.default:
      warnings.append('LOW_QUALITY_SCORE')

    EMIT ObservationReport(market_id, score, liq_score, rule_clarity, horizon_score,
                           dispute_penalty, warnings)

  LOG cycle summary

SDK calls used

  • 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

11. Wire Examples

Input — what arrives on the wire

MarketScanner candidate passed to rankerdisc.marketscanner

{
  "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 — what the bot emits

ObservationReport — high-quality market

{
  "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
}

Reproduce locally

curl 'https://gamma-api.polymarket.com/market/0x7f8a9b.../rules'

12. Decision Logic

APPROVE

Not applicable — MarketQualityRanker emits ObservationReports, not approvals.

RESHAPE_REQUIRED

Not applicable — 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.

13. Standard Decision Output

This bot returns a ObservationReport object. See ObservationReport schema.

{
  "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
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
LOW_QUALITY_SCOREHARD_REJECTComposite quality score is below the hard floor; market dropped from candidate list.Do not forward to strategy layer; log with sub-scores for debugging.This market scored too low across volume, clarity, and depth to be surfaced as an opportunity.
LOW_QUALITY_SCOREWARNScore is between warning and hard floor; market forwarded with quality warning.Include in ObservationReport with WARN annotation.This market passed minimum quality but ranks in the lower tier.
STALE_MARKET_DATAHARD_REJECTGamma API or Data API unavailable; scoring cycle halted.Halt emissions for this cycle; retry next cycle.
RULE_AMBIGUITY_DETECTEDEXPLAINMarket resolution text contains ambiguity keywords that lower the rule_clarity sub-score.Penalise rule_clarity sub-score; annotate report.The resolution rules for this market contain language that may be open to interpretation.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch is active; all report emissions suppressed.Return immediately; do not emit any ObservationReports.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_disc_marketqualityranker_markets_scored_totalcountercountcycleTotal markets scored per cycle.
polytraders_disc_marketqualityranker_reports_emitted_totalcountercountObservationReports successfully emitted to the bus.
polytraders_disc_marketqualityranker_quality_scorehistogramratioDistribution of composite quality scores across all markets in a cycle.
polytraders_disc_marketqualityranker_dropped_low_quality_totalcountercountMarkets dropped due to LOW_QUALITY_SCORE hard reject.

Alerts

AlertConditionSeverityRunbook
MarketQualityRankerAllDroppedpolytraders_disc_marketqualityranker_dropped_low_quality_total / polytraders_disc_marketqualityranker_markets_scored_total > 0.9P2#runbook-marketqualityranker-all-dropped
MarketQualityRankerNoEmissionsrate(polytraders_disc_marketqualityranker_reports_emitted_total[10m]) == 0P1#runbook-marketqualityranker-no-emissions
MarketQualityRankerLowScoreSkewhistogram_quantile(0.5, polytraders_disc_marketqualityranker_quality_score) < 0.3P3#runbook-marketqualityranker-low-score-skew

Dashboards

  • Grafana — Discovery / MarketQualityRanker score distribution

Log levels

LevelWhat gets logged
DEBUGPer-market sub-scores (liquidity, rule_clarity, horizon, dispute_penalty).
INFOCycle summary: markets_scored, markets_forwarded, markets_dropped.
WARNGamma API slow; >90% markets dropped.
ERRORGamma API unavailable; Data API unavailable.

16. Developer Reporting

{
  "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"
}

17. Plain-English Reporting

SituationUser-facing explanation
A market does not appear as an opportunityThis market scored below the quality threshold — it may have low trading volume, ambiguous resolution rules, or an elevated dispute history.
Opportunity shown with a quality warningThis market passed the minimum quality floor but ranks in the lower tier. Strategies will apply additional size restrictions.

18. Failure-Mode Block

main_failure_modeRule clarity scoring may incorrectly penalise well-written but uncommon resolution language, causing valid markets to score below threshold.
false_positive_riskA market with temporarily high volume inflating its liquidity sub-score could receive a high quality rank despite underlying fragility.
false_negative_riskData API dispute history unavailable causes dispute_penalty to default to 0, potentially over-ranking disputable markets.
safe_fallbackIf Gamma API or Data API are unavailable, halt emissions for the affected cycle with STALE_MARKET_DATA rather than emitting stale scores.
required_dependenciesMarketScanner candidate list, Gamma API market rules text, Data API volume and dispute history, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
GAMMA_API_DOWNBlock TCP to gamma-api.polymarket.comAutomatic on next cycle when API recovers.
ALL_MARKETS_LOW_QUALITYSet weight_rule_clarity=0.9 and inject markets with ambiguous rules textTune weights or fix resolution text.
KILL_SWITCH_ONSet killswitch.active=trueEmissions resume on next cycle after KillSwitch reset.

20. State & Persistence

Cold-start recovery

On cold start, normalisation uses the first cycle's values as a warm-up baseline.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded async loop
Max in-flight1
Idempotency keycycle_id
Per-call timeout (ms)8000
Backpressure strategydrop newest
Locking / mutual exclusionnone

22. Dependencies

Depends on (must run first)

BotWhyContract
disc.marketscannerProvides the candidate market list that MarketQualityRanker scores.Expects a list of markets with volume_24h_usd, book_depth_usd, spread_bps, and resolution_date.
risk.kill_switchKillSwitch gate suppresses all emissions when active.If KillSwitch active, scoring runs but no ObservationReports are emitted.

Emits to (downstream consumers)

BotWhyContract
disc.opportunityqueueOpportunityQueue consumes quality scores to rank its output.ObservationReport includes quality_score and sub-scores.

Used by (auto-aggregated)

0.4

External services

ServiceEndpointSLA assumedOn failure
Gamma APIhttps://gamma-api.polymarket.com99.9% / 500ms p99Halt cycle; retry next interval.
Data APIhttps://data-api.polymarket.com99.9% / 500ms p99Default dispute_penalty to 0; log warning.

23. Security Surfaces

Abuse vectors considered

  • 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

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesAll 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.

API surfaces declared

gammadataclob_publicinternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation0.1.0
schema2
releasedNone
planned_releaseQ4-2026

Migration history

DateFromToReasonAction taken
2026-04-28n/av2-specSpec drafted post-CLOB-V2 cutover; bot not yet implementedDesigned against V2 schema (pUSD, builder codes, V2 EIP-712 domain)

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Market below hard floor emits LOW_QUALITY_SCORE HARD_REJECTcomposite_score=0.15, hard=0.2Market dropped from candidate list with reason LOW_QUALITY_SCORE
Dispute penalty caps at 0.3disputes_30d=50dispute_penalty=0.3, not 5.0
KillSwitch suppresses emissionskillswitch.active=trueNo ObservationReports emitted; scoring runs internally

Integration Tests

TestExpected result
End-to-end: MarketScanner candidate becomes ranked ObservationReportObservationReport includes all sub-scores and is consumed downstream by OpportunityQueue
Gamma API unavailability halts cycle with STALE_MARKET_DATANo reports emitted; next cycle resumes when API recovers

Property Tests

PropertyRequired behaviour
composite_score always in [0, 1]Always true
No report emitted when KillSwitch is activeAlways true

27. Operational Runbook

MarketQualityRanker incidents are usually upstream data issues or threshold drift. Bot is read-only; incidents do not affect active positions.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
MarketQualityRankerNoEmissions
MarketQualityRankerAllDropped

Manual overrides

Healthcheck

GET /internal/health/marketqualityranker → green if Last cycle completed within 2× scoring interval and at least one report emitted.; red if No cycle completed in 2× interval or zero reports for 10 minutes.

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

GateHow measuredThreshold
Unit tests pass for all threshold cases and dispute penalty capCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
Quality score distribution is stable over 48h shadow runpolytraders_disc_marketqualityranker_quality_score histogram p50 > 0.35Pass

Promote to General live

GateHow measuredThreshold
Zero false STALE_MARKET_DATA halts during normal operation over 7 daysAlert history0 firings

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
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