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.4 OpportunityQueue

0.4 OpportunityQueue

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

Maintain a ranked queue of the best candidate markets per registered strategy type by combining MarketQualityRanker scores, EventCalendarMapper proximity signals, and per-strategy fit-scores into a single ordered list that strategies consume without re-running discovery.

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 afterMarketQualityRanker and EventCalendarMapper
Applies toAll markets that have passed quality scoring
Default modeshadow_only
User-visibleAdvanced details only
Developer ownerPolytraders core — Intelligence pod

2. Purpose

Maintain a ranked queue of the best candidate markets per registered strategy type by combining MarketQualityRanker scores, EventCalendarMapper proximity signals, and per-strategy fit-scores into a single ordered list that strategies consume without re-running discovery.

3. Why This Bot Matters

  • Strategies independently re-rank markets

    Each strategy duplicates discovery logic, wasting compute and producing inconsistent rankings across the system.

  • Existing positions not suppressed

    A strategy may generate a new intent on a market where the user already has an open position, causing unintended double-up exposure.

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

InputSourceRequired?Use
Top-of-book size and current spreadCLOB + ws_marketYesRefresh queue entries with current execution cost estimates.

5. Required Internal Inputs

InputSourceRequired?Use
MarketQualityRanker ObservationReportsdisc.marketqualityrankerYesBase quality score for each queue entry.
EventCalendarMapper ObservationReportsdisc.eventcalendarmapperNoBoost ranking for markets near a calendar event.
Open positions per market (double-up suppression)exec.position_trackerNoSuppress markets where the user already has an open position if suppress_existing_position=true.
KillSwitch active flagrisk.kill_switchYesFreeze queue and suppress emissions when KillSwitch is active.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
queue_depth2051Maximum number of ranked market entries to maintain per strategy type.
refresh_interval_s60155How often the queue is re-ranked from fresh quality scores and book data.

7. Detailed Parameter Instructions

queue_depth

What it means

Maximum number of ranked market entries to maintain per strategy type.

Default

{ "queue_depth": 20 }

Why this default matters

A depth of 20 gives strategies enough alternatives without overwhelming them with marginal candidates.

Threshold logic

ConditionAction
>= 20Normal queue depth
5–20Shallow queue — WARN
< 1Reject config — queue_depth must be >= 1

Developer check

if (d < params.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

The system maintains a shortlist of the best available markets for each strategy type.

refresh_interval_s

What it means

How often the queue is re-ranked from fresh quality scores and book data.

Default

{ "refresh_interval_s": 60 }

Why this default matters

60-second refresh keeps rankings reasonably current without hammering upstream services.

Threshold logic

ConditionAction
>= 60sNormal refresh cadence
15–60sFast refresh — WARN
< 5sReject — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (s < params.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

The opportunity list is updated regularly to reflect changing market conditions.

8. Default Configuration

{
  "bot_id": "disc.opportunity_queue",
  "version": "0.1.0",
  "mode": "shadow_only",
  "defaults": {
    "queue_depth": 20,
    "refresh_interval_s": 60,
    "strategy_filters": [],
    "suppress_existing_position": true
  }
}

9. Implementation Flow

  1. On each refresh cycle, ingest latest MarketQualityRanker ObservationReports.
  2. Check KillSwitch; if active, freeze queue and suppress emissions.
  3. Optionally ingest EventCalendarMapper reports; apply hours_to_event proximity boost.
  4. If suppress_existing_position=true, fetch open positions from exec.position_tracker and exclude those markets.
  5. For each registered strategy type, compute per-strategy fit_score using strategy_filters (e.g. time-to-res, vol range, spread tolerance).
  6. Final rank = quality_score * fit_score * proximity_boost.
  7. Retain top queue_depth entries per strategy type; evict stale entries older than 2× refresh_interval_s.
  8. Emit ObservationReport with ranked queue snapshot per strategy type.
  9. Log cycle summary with queue depth, evictions, and top entry per strategy.

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 queueRefreshCycle():
  ks = FETCH internal.killswitch.status
  IF ks.active: EMIT STALE_QUEUE; RETURN

  quality_reports = FETCH disc.marketqualityranker.latest_reports()
  IF quality_reports IS NULL OR age(quality_reports) > 2 * params.refresh_interval_s:
    EMIT ObservationReport(kind='STALE_QUEUE'); RETURN

  calendar_boosts = FETCH disc.eventcalendarmapper.latest_reports() OR {}
  open_positions = FETCH exec.position_tracker.open() OR {}

  FOR strategy_type IN registered_strategies:
    candidates = []
    FOR report IN quality_reports:
      IF params.suppress_existing_position AND report.market_id IN open_positions:
        CONTINUE
      fit = computeFitScore(strategy_type, report)
      boost = calendarBoost(report.market_id, calendar_boosts)
      final_score = report.quality_score * fit * boost
      candidates.append({ market_id, quality_score, fit, boost, final_score })

    candidates.sort(key=final_score, desc=True)
    queue = candidates[:params.queue_depth]

    IF len(queue) < params.queue_depth.warning:
      LOG WARN 'QUEUE_TOO_SHALLOW'

    EMIT ObservationReport(strategy_type, queue, refreshed_at)

  LOG cycle summary

SDK calls used

  • fetchClobPublic('/book?market=<condition_id>&depth=1')
  • ws_market.current_spread('<condition_id>')

Complexity: O(M × S) where M=candidate markets, S=registered strategy types

11. Wire Examples

Input — what arrives on the wire

MarketQualityRanker ObservationReport consumed by queuedisc.marketqualityranker

{
  "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
  "quality_score": 0.71,
  "sub_scores": {
    "liquidity": 0.82,
    "rule_clarity": 0.65,
    "resolution_horizon": 0.6
  }
}

Output — what the bot emits

ObservationReport — ranked queue for mean-reversion strategy

{
  "report_id": "0xeeff33445566778899001122334455eeff33445566778899001122334455eeff",
  "bot_id": "disc.opportunity_queue",
  "strategy_type": "mean-reversion",
  "queue": [
    {
      "rank": 1,
      "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
      "quality_score": 0.71,
      "fit_score": 0.88,
      "proximity_boost": 1.0,
      "final_rank_score": 0.625
    }
  ],
  "queue_depth": 1,
  "refreshed_at_ms": 1746789000000
}

Reproduce locally

curl 'https://clob.polymarket.com/book?market=0x7f8a9b...&depth=1'

12. Decision Logic

APPROVE

Not applicable — OpportunityQueue emits ObservationReports, not approvals.

RESHAPE_REQUIRED

Not applicable — read-only ranking bot.

REJECT

Markets already held as open positions are suppressed when suppress_existing_position=true.

WARNING_ONLY

Markets near the queue_depth warning threshold trigger shallow-queue warnings.

13. Standard Decision Output

This bot returns a ObservationReport object. See ObservationReport schema.

{
  "report_id": "0xeeff33445566778899001122334455eeff33445566778899001122334455eeff",
  "bot_id": "disc.opportunity_queue",
  "strategy_type": "mean-reversion",
  "queue": [
    {
      "rank": 1,
      "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
      "quality_score": 0.71,
      "fit_score": 0.88,
      "proximity_boost": 1.0,
      "final_rank_score": 0.625
    }
  ],
  "queue_depth": 1,
  "refreshed_at_ms": 1746789000000
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
STALE_QUEUEWARNMarketQualityRanker reports are older than 2× refresh_interval_s; queue frozen.Freeze queue; emit STALE_QUEUE warning; notify downstream strategies.Opportunity rankings are temporarily unavailable while upstream data refreshes.
QUEUE_TOO_SHALLOWWARNActive queue depth has dropped below the warning threshold.Log warning; relax strategy_filters if configured to do so.Fewer opportunities than usual are available for your strategy type.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch is active; queue frozen and emissions suppressed.Return immediately without emitting any reports.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTqueue_depth or refresh_interval_s below locked hard minimum.Reject config change; do not apply.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_disc_opportunityqueue_queue_depth_gaugegaugecountstrategy_typeCurrent number of entries in the opportunity queue per strategy type.
polytraders_disc_opportunityqueue_reports_emitted_totalcountercountstrategy_typeTotal queue refresh ObservationReports emitted.
polytraders_disc_opportunityqueue_evictions_totalcountercountMarkets evicted from queue due to staleness or position suppression.

Alerts

AlertConditionSeverityRunbook
OpportunityQueueEmptypolytraders_disc_opportunityqueue_queue_depth_gauge == 0P2#runbook-opportunityqueue-empty
OpportunityQueueStaletime() - polytraders_disc_opportunityqueue_last_refresh_ts > 120P1#runbook-opportunityqueue-stale

Dashboards

  • Grafana — Discovery / OpportunityQueue depth and evictions

Log levels

LevelWhat gets logged
DEBUGPer-strategy-type queue snapshot with scores.
INFOCycle summary: strategy_types_served, total_entries, evictions.
WARNQueue shallow; upstream quality reports stale.
ERRORKillSwitch active; MarketQualityRanker unreachable.

16. Developer Reporting

{
  "bot_id": "disc.opportunity_queue",
  "cycle": 88,
  "strategy_types_served": 3,
  "total_queue_entries": 45,
  "evictions": 3,
  "suppressed_existing": 2,
  "killswitch_active": false,
  "refreshed_at": "2026-05-09T11:30:00Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Fewer opportunities than expected in queueThe queue may have few entries because of thin markets, open position suppression, or a narrow strategy filter configuration.
Opportunity disappeared from queueA market may have dropped out of the queue because its quality score declined, it was excluded due to an open position, or the queue was refreshed with new data.

18. Failure-Mode Block

main_failure_modeQueue becomes stale if MarketQualityRanker stops emitting; strategies continue consuming an outdated ranked list.
false_positive_riskA market with a temporarily high fit_score (e.g. unusual spread) could rank near the top, causing strategies to target a marginal market.
false_negative_riskA market matching the strategy filter may be suppressed if the position tracker incorrectly reports an open position.
safe_fallbackIf MarketQualityRanker reports are stale (>2× refresh_interval_s), freeze queue and emit STALE_QUEUE warning rather than serving stale rankings.
required_dependenciesMarketQualityRanker ObservationReports, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
UPSTREAM_QUALITY_REPORTS_STALEStop disc.marketqualityranker for >2× refresh_interval_sQueue resumes normal operation when quality reports resume.
KILL_SWITCH_ONSet killswitch.active=trueQueue unfreezes on next cycle after KillSwitch reset.
QUEUE_EMPTIES_DUE_TO_SUPPRESSIONSet suppress_existing_position=true with all markets having open positionsAutomatic when positions close or new markets become available.

20. State & Persistence

Cold-start recovery

On cold start, queue is empty; first refresh cycle populates it.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded async loop
Max in-flight1
Idempotency keyrefresh_cycle_id
Per-call timeout (ms)6000
Backpressure strategydrop newest
Locking / mutual exclusionread-write lock on queue state

22. Dependencies

Depends on (must run first)

BotWhyContract
disc.marketqualityrankerPrimary source of quality scores for queue entries.ObservationReport must include quality_score, sub_scores, and market_id.
risk.kill_switchKillSwitch gate freezes queue and suppresses emissions.If active, queue frozen; no reports emitted.

Emits to (downstream consumers)

BotWhyContract
strat.*Registered strategies consume the ranked queue to select their next OrderIntent candidate.ObservationReport includes strategy_type, ranked queue entries, and refreshed_at timestamp.

Sibling bots (same OrderIntent)

External services

ServiceEndpointSLA assumedOn failure
CLOB API (read)https://clob.polymarket.com99.95% / 200ms p99Use cached spread; log warning if cache is stale.

23. Security Surfaces

Abuse vectors considered

  • Injection of crafted quality scores via compromised MarketQualityRanker to manipulate queue ranking

Mitigations

  • Queue entries clamped to [0,1] score range regardless of upstream values
  • All outputs are ObservationReports; execution layer independently validates before acting

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
NotesQueue entries include neg_risk flag from upstream quality reports; all size and cost estimates denominated in pUSD.

API surfaces declared

clob_publicws_marketinternal

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
queue_depth=1 returns only the top-ranked market per strategy typequeue_depth=1, 10 candidate marketsObservationReport with queue.length=1
suppress_existing_position removes held marketssuppress_existing_position=true; open position on market AMarket A absent from queue output
KillSwitch freezes queuekillswitch.active=trueNo ObservationReports emitted; existing queue frozen

Integration Tests

TestExpected result
MarketQualityRanker → OpportunityQueue → Strategy pipelineStrategy receives ranked queue ObservationReport and generates OrderIntent using top-ranked market
Stale MarketQualityRanker reports trigger STALE_QUEUEQueue frozen; STALE_QUEUE warning emitted; strategies notified

Property Tests

PropertyRequired behaviour
Queue never exceeds queue_depth entries per strategy typeAlways true
No market with open position appears in queue when suppress_existing_position=trueAlways true

27. Operational Runbook

OpportunityQueue incidents are typically upstream data staleness or KillSwitch activation. Bot is read-only; incidents delay strategy opportunity discovery but do not affect active positions.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
OpportunityQueueStale
OpportunityQueueEmpty

Manual overrides

Healthcheck

GET /internal/health/opportunityqueue → green if Queue depth > 0 for at least one strategy type; last refresh within 2× refresh_interval_s.; red if Queue depth = 0 for all strategy types or no refresh in 2× interval.

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 queue_depth enforcement and position suppressionCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
Queue depth stable ≥5 for primary strategy type over 48h shadowpolytraders_disc_opportunityqueue_queue_depth_gaugep50 >= 5

Promote to General live

GateHow measuredThreshold
Zero spurious STALE_QUEUE events during normal operation over 7 daysAlert history0 false 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