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 LayerExecution2.8 PartialFillHandler

2.8 PartialFillHandler

Execution Execution Utility Reshape PLANNED Spec started capital · Direct P5 · Execution rails pending stub

PartialFillHandler decides what to do with the unfilled remainder after a partial fill: hold and wait, cancel the remainder, or chase the market by submitting a new order. It respects the strategy's declared partial-fill policy and current book state.

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

LayerExecution  Execution
Bot classExecution Utility
AuthorityReshape
StatusPLANNED
ReadinessSpec started
Runs beforeCancelReplaceOptimizer (if remainder requires a new order)
Runs afterOrderLifecycleManager (PARTIAL event received)
Applies toEvery PARTIAL fill event with a non-zero remainder
Default modeshadow_only
User-visibleyes
Developer ownerPolytraders core — Execution pod

Operational profile

Modes supportedquarantine

2. Purpose

PartialFillHandler decides what to do with the unfilled remainder after a partial fill: hold and wait, cancel the remainder, or chase the market by submitting a new order. It respects the strategy's declared partial-fill policy and current book state.

3. Why This Bot Matters

  • Remainder left resting indefinitely

    Capital locked in a GTC order that will never fill at the original price if the market has moved; position budget exhausted.

  • Remainder always cancelled

    Aggressive cancel policy causes the strategy to underfill its intended position size, leading to execution shortfall.

  • Chase price too aggressive

    Chasing too many ticks above the original price to fill the remainder results in a worse blended fill price than if the order had been left resting.

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
CLOB V2 top-of-book snapshotclob_publicYesDetermine current best bid/ask to decide whether book is thin and whether chasing is viable.
Recent fill rate on marketws_marketNoEstimate time-to-fill for remainder if left resting.

5. Required Internal Inputs

InputSourceRequired?Use
PARTIAL ExecutionReport from OrderLifecycleManagerexec.orderlifecyclemanagerYesGet remaining_usd, original price, market_id, and strategy partial-fill policy.
KillSwitch active flagrisk.kill_switchYesCancel remainder immediately if KillSwitch active; emit no new order.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
default_policyholdDefault policy for handling the unfilled remainder: hold (leave resting), cancel (remove from book), or chase (submit new order at adjusted price).
min_remainder_size521Minimum remainder size in pUSD below which the remainder is automatically cancelled rather than left resting.
chase_max_ticks3510Maximum number of ticks above the original price at which a chase order may be submitted. Prevents chasing a rapidly moving market.
cancel_on_book_thinTrueIf True, automatically cancel the remainder when the visible book depth is less than the remainder size (thin book condition).

7. Detailed Parameter Instructions

default_policy

What it means

Default policy for handling the unfilled remainder: hold (leave resting), cancel (remove from book), or chase (submit new order at adjusted price).

Default

{ "default_policy": "hold" }

Why this default matters

Hold is safest: it preserves queue position and fills if the market returns to the original price level.

Threshold logic

ConditionAction
policy=holdLeave remainder order resting on book
policy=cancelCancel remainder; emit ExecutionReport(CANCELLED_REMAINDER)
policy=chase AND remaining_ticks <= chase_max_ticksCancel remainder; submit new order at current best price

Developer check

if params.default_policy == 'chase': submitChaseOrder(remainder, book)

User-facing English

The unfilled portion of your order was left in the market to continue looking for a match.

min_remainder_size

What it means

Minimum remainder size in pUSD below which the remainder is automatically cancelled rather than left resting.

Default

{ "min_remainder_size": 5 }

Why this default matters

Remainders below $5 pUSD are economically dust-like; the transaction cost of a fill exceeds the position value.

Threshold logic

ConditionAction
remaining_usd >= 5Apply default_policy normally
1 <= remaining_usd < 5WARN — PARTIAL_FILL_DUST_REMAINDER; forward to DustAndRoundingCleaner
remaining_usd < 1 (hard)Auto-cancel; PARTIAL_FILL_DUST_AUTO_CANCEL

Developer check

if remaining_usd < params.min_remainder_size: cancelRemainder(order)

User-facing English

The remaining part of your order was too small to be worth keeping open, so it was cancelled.

chase_max_ticks

What it means

Maximum number of ticks above the original price at which a chase order may be submitted. Prevents chasing a rapidly moving market.

Default

{ "chase_max_ticks": 3 }

Why this default matters

Chasing more than 3 ticks means paying significantly more for the remainder than the original intent price.

Threshold logic

ConditionAction
required_ticks <= 3Chase permitted; submit new order
3 < required_ticks <= 5WARN — PARTIAL_FILL_CHASE_WIDE; proceed if strategy allows
required_ticks > 10 (hard)Cancel remainder; PARTIAL_FILL_CHASE_ABORTED

Developer check

if ticksToFill > params.chase_max_ticks: cancelRemainder(order)

User-facing English

The remaining portion of your order could not be filled at a reasonable price and was cancelled.

cancel_on_book_thin

What it means

If True, automatically cancel the remainder when the visible book depth is less than the remainder size (thin book condition).

Default

{ "cancel_on_book_thin": true }

Why this default matters

A thin book indicates the remainder will not fill soon; leaving a resting order in a thin book risks a fill when the book refills at an adverse price.

Threshold logic

ConditionAction
book_depth >= remaining_usdProceed with default_policy
book_depth < remaining_usd AND cancel_on_book_thin=trueCancel remainder; PARTIAL_FILL_BOOK_THIN_CANCEL

Developer check

if bookDepth < remaining_usd and params.cancel_on_book_thin: cancelRemainder(order)

User-facing English

The order book became too thin to fill the remaining portion of your order, which was cancelled.

8. Default Configuration

{
  "bot_id": "exec.partialfillhandler",
  "version": "0.1.0",
  "mode": "shadow_only",
  "defaults": {
    "default_policy": "hold",
    "min_remainder_size": 5,
    "chase_max_ticks": 3,
    "cancel_on_book_thin": true
  },
  "locked": {
    "min_remainder_size": {
      "min": 1
    },
    "chase_max_ticks": {
      "max": 10
    }
  }
}

9. Implementation Flow

  1. Receive PARTIAL ExecutionReport from OrderLifecycleManager with remaining_usd, original_price, market_id.
  2. Check KillSwitch; if active, cancel remainder via clob_auth and return.
  3. If remaining_usd < min_remainder_size: forward to DustAndRoundingCleaner; cancel remainder.
  4. Fetch CLOB V2 top-of-book from clob_public; compute book_depth on order side.
  5. If cancel_on_book_thin and book_depth < remaining_usd: cancel remainder; emit PARTIAL_FILL_BOOK_THIN_CANCEL.
  6. Apply default_policy: hold → leave remainder; cancel → cancel remainder; chase → compute ticks to fill.
  7. If chase: if ticks_to_fill > chase_max_ticks, cancel; else cancel remainder and submit new order at current best price via CancelReplaceOptimizer.
  8. Emit ExecutionReport with verdict, policy applied, remaining_usd, and reason_code.

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 handlePartial(execReport):
  ks = FETCH internal.killswitch.status
  IF ks.active:
    clob_auth.DELETE('/order/' + execReport.order_id)
    EMIT ExecutionReport(KILL_SWITCH_ACTIVE)
    RETURN

  remaining = execReport.remaining_usd
  IF remaining < params.min_remainder_size:
    clob_auth.DELETE('/order/' + execReport.order_id)
    EMIT ExecutionReport(PARTIAL_FILL_DUST_AUTO_CANCEL)
    RETURN

  book = FETCH clob_public.GET('/book?market=' + execReport.market_id)
  bookDepth = SUM(level.size FOR level IN book.side(execReport.side)[:5])

  IF params.cancel_on_book_thin AND bookDepth < remaining:
    clob_auth.DELETE('/order/' + execReport.order_id)
    EMIT ExecutionReport(PARTIAL_FILL_BOOK_THIN_CANCEL)
    RETURN

  policy = execReport.strategy.partial_fill_policy OR params.default_policy

  IF policy == 'hold':
    EMIT ExecutionReport(HOLD_REMAINDER)
  ELIF policy == 'cancel':
    clob_auth.DELETE('/order/' + execReport.order_id)
    EMIT ExecutionReport(CANCELLED_REMAINDER)
  ELIF policy == 'chase':
    bestPrice = book.best_ask IF execReport.side == 'BUY' ELSE book.best_bid
    ticksAway = abs(bestPrice - execReport.original_price) / market.tick_size
    IF ticksAway > params.chase_max_ticks:
      clob_auth.DELETE('/order/' + execReport.order_id)
      EMIT ExecutionReport(PARTIAL_FILL_CHASE_ABORTED)
    ELSE:
      clob_auth.DELETE('/order/' + execReport.order_id)
      newOrder = buildOrderTypedData({price: bestPrice, size: remaining,
                   builder: execReport.builder_code})
      clob_auth.POST('/order', sign(newOrder))
      EMIT ExecutionReport(CHASE_ORDER_SUBMITTED)

SDK calls used

  • clob_public.GET('/book?market=' + market_id)
  • clob_auth.DELETE('/order/' + order_id)
  • clob_auth.POST('/order', signed_chase_order)
  • buildOrderTypedData({price, size, builder_code})

Complexity: O(1) per partial fill event

11. Wire Examples

Input — what arrives on the wire

PARTIAL ExecutionReport from OrderLifecycleManagerexec.orderlifecyclemanager

{
  "order_id": "0xcccc3333dddd4444eeee5555ffff6666aaaa7777bbbb8888cccc9999dddd0000",
  "status": "PARTIAL",
  "filled_usd": 200,
  "remaining_usd": 250,
  "original_price": 0.62,
  "side": "BUY",
  "collateral": "pUSD",
  "builder_code": "0x706f6c7974726164657273000000000000000000000000000000000000000000"
}

Output — what the bot emits

ExecutionReport — HOLD_REMAINDER

{
  "report_id": "rep_3c4d5e6f7a8b9c0d",
  "bot_id": "exec.partialfillhandler",
  "verdict": "HOLD_REMAINDER",
  "remaining_usd": 250,
  "policy_applied": "hold",
  "collateral": "pUSD",
  "evaluated_at_ms": 1746770100000
}

12. Decision Logic

APPROVE

Remainder above min_remainder_size, book not thin, policy=hold. Leave remainder resting.

RESHAPE_REQUIRED

Chase policy: cancel remainder and submit new order at adjusted price (ticks within chase_max_ticks).

REJECT

Remainder below min_remainder_size, book thin, ticks exceed chase_max_ticks, or KillSwitch active.

WARNING_ONLY

Chase ticks between warning and hard threshold; WARN emitted but chase proceeds if strategy allows.

13. Standard Decision Output

This bot returns a ExecutionReport object. See ExecutionReport schema.

{
  "report_id": "rep_3c4d5e6f7a8b9c0d",
  "trace_id": "trc_2b3c4d5e6f7a8b9c",
  "bot_id": "exec.partialfillhandler",
  "order_id": "0xcccc3333dddd4444eeee5555ffff6666aaaa7777bbbb8888cccc9999dddd0000",
  "market_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
  "filled_usd": 200,
  "remaining_usd": 250,
  "policy_applied": "hold",
  "verdict": "HOLD_REMAINDER",
  "collateral": "pUSD",
  "builder_code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
  "evaluated_at_ms": 1746770100000
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
HOLD_REMAINDERINFORemainder left resting on book per hold policy.No action; emit ExecutionReport.Your order is still open, waiting to be filled.
PARTIAL_FILL_DUST_AUTO_CANCELHARD_REJECTRemainder below min_remainder_size; auto-cancelled.Cancel order; forward to DustAndRoundingCleaner.The tiny remaining portion of your order was cancelled.
PARTIAL_FILL_BOOK_THIN_CANCELWARNBook depth < remaining_usd and cancel_on_book_thin=true.Cancel remainder.The market was too thin to fill your remaining order; it was cancelled.
PARTIAL_FILL_CHASE_ABORTEDWARNChase ticks exceeded chase_max_ticks; remainder cancelled instead.Cancel remainder; do not chase.The remaining order could not be filled at a reasonable price and was cancelled.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch active; remainder cancelled immediately.Cancel remainder; halt.Trading is currently paused.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_exec_partialfillhandler_decisions_totalcountercountverdictTotal partial fill decisions by verdict (HOLD/CANCEL/CHASE/DUST).
polytraders_exec_partialfillhandler_remainder_usdhistogrampUSDDistribution of remainder sizes at time of partial fill decision.
polytraders_exec_partialfillhandler_chase_tickshistogramcountDistribution of ticks-to-fill on chase decisions; values > chase_max_ticks trigger aborts.

Alerts

AlertConditionSeverityRunbook
PFHHighDustRaterate(polytraders_exec_partialfillhandler_decisions_total{verdict='PARTIAL_FILL_DUST_AUTO_CANCEL'}[5m]) > 0.2P2#runbook-pfh-dust
PFHChaseAbortRaterate(polytraders_exec_partialfillhandler_decisions_total{verdict='PARTIAL_FILL_CHASE_ABORTED'}[5m]) > 0.1P2#runbook-pfh-chase

16. Developer Reporting

{
  "order_id": "0xcccc3333dddd4444eeee5555ffff6666aaaa7777bbbb8888cccc9999dddd0000",
  "remaining_usd": 250,
  "book_depth_usd": 800,
  "ticks_to_fill": 1,
  "policy_applied": "hold",
  "cancel_on_book_thin": true,
  "verdict": "HOLD_REMAINDER"
}

17. Plain-English Reporting

SituationUser-facing explanation
Remainder heldPart of your order was filled. The rest is still open in the market, waiting for a match.
Remainder cancelled — dustThe remaining portion of your order was too small to be worth keeping, so it was removed from the market.
Remainder cancelled — book thinThe market did not have enough volume to fill the remainder, so the order was removed to avoid an unfavourable fill later.

18. Failure-Mode Block

main_failure_modeChase policy submits a new order at a tick-adjusted price while the original remainder order is still being cancelled, briefly creating a duplicate open order.
false_positive_riskcancel_on_book_thin fires on a momentarily thin book snapshot, cancelling a remainder that would have filled seconds later when the book refreshed.
false_negative_riskhold policy leaves a large remainder resting when the book has moved significantly past the original price, locking capital indefinitely.
safe_fallbackIf clob_public is unavailable for book depth check, apply hold policy conservatively and emit WARN; do not auto-cancel without book data.
required_dependenciesclob_public top-of-book snapshot, PARTIAL ExecutionReport from OrderLifecycleManager, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
BOOK_THIN_ON_PARTIALDrain order book to < 10 pUSD depth on target market sideAutomatic on next partial; book depth re-evaluated
CHASE_TICKS_EXCEEDEDMove market 15 ticks from original price before partial fill, policy=chaseAutomatic
KILL_SWITCH_ON_PARTIALActivate KillSwitch during active partial fillManual KillSwitch reset

20. State & Persistence

Cold-start recovery

State rebuilt from OrderLifecycleManager on restart via PARTIAL events.

21. Concurrency & Idempotency

AspectSpecification
Execution modelper-order goroutine
Max in-flight50
Idempotency keyorder_id + event_ts_ms
Per-call timeout (ms)300
Backpressure strategyshed excess if > 50 partials in flight
Locking / mutual exclusionper-order_id mutex to prevent double-cancel on rapid partial events

22. Dependencies

Depends on (must run first)

BotWhyContract
exec.orderlifecyclemanagerSource of PARTIAL ExecutionReport with remaining_usd and original_price.Activates only on PARTIAL status events.
risk.kill_switchKillSwitch forces immediate remainder cancel.No remainder survives active KillSwitch.

Emits to (downstream consumers)

BotWhyContract
exec.cancelreplaceoptimizerChase policy delegates to CancelReplaceOptimizer for optimal cancel+replace sequencing.CancelReplaceOptimizer receives cancel+new_order instruction.
exec.dustandroundingcleanerDust remainders forwarded for sweep handling.Dust remainder passed with original order_id.

Used by (auto-aggregated)

2.13 2.9

External services

ServiceEndpointSLA assumedOn failure
CLOB V2 auth APIhttps://clob.polymarket.com99.95% / 200ms p99If cancel fails, retry up to 2 times; emit WARN on persistent failure.
CLOB V2 public APIhttps://clob.polymarket.com99.9% / 200ms p99If book unavailable, apply hold policy conservatively.

23. Security Surfaces

Abuse vectors considered

  • Injecting a PARTIAL event with inflated remaining_usd to trigger an oversized chase order
  • Forcing book-thin cancel by poisoning the book depth snapshot

Mitigations

  • PARTIAL ExecutionReport validated against OrderLifecycleManager HMAC
  • Chase order size capped at original remaining_usd; cannot exceed intent size

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesChase orders carry the same builder_code bytes32 as the original order for continuous attribution. Remainder sizes are denominated in pUSD; minimum sizes respect CTFExchangeV2 minimum order constraints.

API surfaces declared

clob_authclob_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
Hold policy: remainder left resting when book is deepbook_depth=800, remaining_usd=250, policy=holdHOLD_REMAINDER; no cancel issued
Auto-cancel when remaining_usd < min_remainder_sizeremaining_usd=3, min_remainder_size=5PARTIAL_FILL_DUST_AUTO_CANCEL; cancel issued
Book-thin cancel when book_depth < remaining_usdbook_depth=100, remaining_usd=200, cancel_on_book_thin=truePARTIAL_FILL_BOOK_THIN_CANCEL; cancel issued

Integration Tests

TestExpected result
Chase policy: remainder cancelled and new order submitted within chase_max_ticksNew order submitted at current_best_ask; CancelReplaceOptimizer invoked
KillSwitch active: remainder cancelled immediately on KillSwitch signalCancel issued; KILL_SWITCH_ACTIVE emitted; no new order

Property Tests

PropertyRequired behaviour
Remainder size in ExecutionReport always equals original_size - filled_usdAlways true
Chase order price never exceeds original_price + chase_max_ticks * tick_sizeAlways true

27. Operational Runbook

PartialFillHandler incidents are usually high dust-cancel rates (strategy sizing too small) or chase-abort spikes (market moving too fast for chase policy).

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
PFHHighDustRateCheck strategy min order sizes; if too small, increase min_remainder_size or adjust strategy minimum.Strategy pod lead
PFHChaseAbortRateCheck market volatility; if high, consider switching affected strategies to hold or cancel policy.Exec pod lead

Manual overrides

  • polytraders bot cancel-remainder exec.partialfillhandler --order <order_id> — Manual remainder cancellation needed when auto-policy is stuck.

Healthcheck

GET /internal/health/partialfillhandler → green if clob_auth reachable, clob_public reachable, dust_cancel_rate < 0.2/min, chase_abort_rate < 0.1/min; red if clob_auth unreachable, dust_cancel_rate > 1/min, chase_abort_rate > 0.5/min

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
All unit tests pass including dust and book-thin scenariosCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
Chase order price invariant verified: no chase order exceeds original_price + chase_max_ticks * tick_sizeProperty test over 48h shadow runZero violations

Promote to General live

GateHow measuredThreshold
Dust cancel rate < 5% of all partial fills over 7-day limited-livepolytraders_exec_partialfillhandler_decisions_total< 5%

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