1. Bot Identity
| Layer | Execution Execution |
|---|
| Bot class | Execution Utility |
|---|
| Authority | Reshape |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec started |
|---|
| Runs before | CancelReplaceOptimizer (if remainder requires a new order) |
|---|
| Runs after | OrderLifecycleManager (PARTIAL event received) |
|---|
| Applies to | Every PARTIAL fill event with a non-zero remainder |
|---|
| Default mode | shadow_only |
|---|
| User-visible | yes |
|---|
| Developer owner | Polytraders core — Execution pod |
|---|
Operational profile
| Modes supported | quarantine |
|---|
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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| default_policy | hold | — | — | Default policy for handling the unfilled remainder: hold (leave resting), cancel (remove from book), or chase (submit new order at adjusted price). |
| min_remainder_size | 5 | 2 | 1 | Minimum remainder size in pUSD below which the remainder is automatically cancelled rather than left resting. |
| chase_max_ticks | 3 | 5 | 10 | Maximum number of ticks above the original price at which a chase order may be submitted. Prevents chasing a rapidly moving market. |
| cancel_on_book_thin | True | — | — | If 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
| Condition | Action |
|---|
| policy=hold | Leave remainder order resting on book |
| policy=cancel | Cancel remainder; emit ExecutionReport(CANCELLED_REMAINDER) |
| policy=chase AND remaining_ticks <= chase_max_ticks | Cancel 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
| Condition | Action |
|---|
| remaining_usd >= 5 | Apply default_policy normally |
| 1 <= remaining_usd < 5 | WARN — 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
| Condition | Action |
|---|
| required_ticks <= 3 | Chase permitted; submit new order |
| 3 < required_ticks <= 5 | WARN — 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
| Condition | Action |
|---|
| book_depth >= remaining_usd | Proceed with default_policy |
| book_depth < remaining_usd AND cancel_on_book_thin=true | Cancel 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
- Receive PARTIAL ExecutionReport from OrderLifecycleManager with remaining_usd, original_price, market_id.
- Check KillSwitch; if active, cancel remainder via clob_auth and return.
- If remaining_usd < min_remainder_size: forward to DustAndRoundingCleaner; cancel remainder.
- Fetch CLOB V2 top-of-book from clob_public; compute book_depth on order side.
- If cancel_on_book_thin and book_depth < remaining_usd: cancel remainder; emit PARTIAL_FILL_BOOK_THIN_CANCEL.
- Apply default_policy: hold → leave remainder; cancel → cancel remainder; chase → compute ticks to fill.
- If chase: if ticks_to_fill > chase_max_ticks, cancel; else cancel remainder and submit new order at current best price via CancelReplaceOptimizer.
- 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 OrderLifecycleManager — exec.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
| Code | Severity | Meaning | Action | User-facing message |
|---|
HOLD_REMAINDER | INFO | Remainder left resting on book per hold policy. | No action; emit ExecutionReport. | Your order is still open, waiting to be filled. |
PARTIAL_FILL_DUST_AUTO_CANCEL | HARD_REJECT | Remainder below min_remainder_size; auto-cancelled. | Cancel order; forward to DustAndRoundingCleaner. | The tiny remaining portion of your order was cancelled. |
PARTIAL_FILL_BOOK_THIN_CANCEL | WARN | Book 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_ABORTED | WARN | Chase 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_ACTIVE | HARD_REJECT | KillSwitch active; remainder cancelled immediately. | Cancel remainder; halt. | Trading is currently paused. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_exec_partialfillhandler_decisions_total | counter | count | verdict | Total partial fill decisions by verdict (HOLD/CANCEL/CHASE/DUST). |
polytraders_exec_partialfillhandler_remainder_usd | histogram | pUSD | | Distribution of remainder sizes at time of partial fill decision. |
polytraders_exec_partialfillhandler_chase_ticks | histogram | count | | Distribution of ticks-to-fill on chase decisions; values > chase_max_ticks trigger aborts. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
PFHHighDustRate | rate(polytraders_exec_partialfillhandler_decisions_total{verdict='PARTIAL_FILL_DUST_AUTO_CANCEL'}[5m]) > 0.2 | P2 | #runbook-pfh-dust |
PFHChaseAbortRate | rate(polytraders_exec_partialfillhandler_decisions_total{verdict='PARTIAL_FILL_CHASE_ABORTED'}[5m]) > 0.1 | P2 | #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
| Situation | User-facing explanation |
|---|
| Remainder held | Part of your order was filled. The rest is still open in the market, waiting for a match. |
| Remainder cancelled — dust | The remaining portion of your order was too small to be worth keeping, so it was removed from the market. |
| Remainder cancelled — book thin | The 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_mode | Chase 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_risk | cancel_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_risk | hold policy leaves a large remainder resting when the book has moved significantly past the original price, locking capital indefinitely. |
|---|
| safe_fallback | If clob_public is unavailable for book depth check, apply hold policy conservatively and emit WARN; do not auto-cancel without book data. |
|---|
| required_dependencies | clob_public top-of-book snapshot, PARTIAL ExecutionReport from OrderLifecycleManager, KillSwitch active flag |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
BOOK_THIN_ON_PARTIAL | Drain order book to < 10 pUSD depth on target market side | | Automatic on next partial; book depth re-evaluated |
CHASE_TICKS_EXCEEDED | Move market 15 ticks from original price before partial fill, policy=chase | | Automatic |
KILL_SWITCH_ON_PARTIAL | Activate KillSwitch during active partial fill | | Manual KillSwitch reset |
20. State & Persistence
Cold-start recovery
State rebuilt from OrderLifecycleManager on restart via PARTIAL events.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | per-order goroutine |
| Max in-flight | 50 |
| Idempotency key | order_id + event_ts_ms |
| Per-call timeout (ms) | 300 |
| Backpressure strategy | shed excess if > 50 partials in flight |
| Locking / mutual exclusion | per-order_id mutex to prevent double-cancel on rapid partial events |
22. Dependencies
Depends on (must run first)
Emits to (downstream consumers)
Used by (auto-aggregated)
2.13 2.9
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| CLOB V2 auth API | https://clob.polymarket.com | 99.95% / 200ms p99 | If cancel fails, retry up to 2 times; emit WARN on persistent failure. |
| CLOB V2 public API | https://clob.polymarket.com | 99.9% / 200ms p99 | If 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
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Chase 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
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q4-2026 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | n/a | v2-spec | Spec drafted post-CLOB-V2 cutover; bot not yet implemented | Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain) |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Hold policy: remainder left resting when book is deep | book_depth=800, remaining_usd=250, policy=hold | HOLD_REMAINDER; no cancel issued |
| Auto-cancel when remaining_usd < min_remainder_size | remaining_usd=3, min_remainder_size=5 | PARTIAL_FILL_DUST_AUTO_CANCEL; cancel issued |
| Book-thin cancel when book_depth < remaining_usd | book_depth=100, remaining_usd=200, cancel_on_book_thin=true | PARTIAL_FILL_BOOK_THIN_CANCEL; cancel issued |
Integration Tests
| Test | Expected result |
|---|
| Chase policy: remainder cancelled and new order submitted within chase_max_ticks | New order submitted at current_best_ask; CancelReplaceOptimizer invoked |
| KillSwitch active: remainder cancelled immediately on KillSwitch signal | Cancel issued; KILL_SWITCH_ACTIVE emitted; no new order |
Property Tests
| Property | Required behaviour |
|---|
| Remainder size in ExecutionReport always equals original_size - filled_usd | Always true |
| Chase order price never exceeds original_price + chase_max_ticks * tick_size | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
PFHHighDustRate | Check strategy min order sizes; if too small, increase min_remainder_size or adjust strategy minimum. | | | Strategy pod lead |
PFHChaseAbortRate | Check 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
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 |