DEV Community

Benjamin-Cup
Benjamin-Cup

Posted on

Building a Two-Sided Limit Order Bot for Polymarket BTC 5-Minute Markets

After the TWAP upgrade, I started testing a different approach for Polymarket's BTC 5-minute Up/Down markets.

The main thing I noticed was that short-term prices became much more active.

Instead of continuously taking the current price, I'm experimenting with placing limit orders on both outcomes and trying to capture temporary price movement.

This post explains the idea and the engineering behind it.

This is an experimental strategy, not a guaranteed-profit system.


The Basic Idea

A BTC 5-minute market has two outcomes:

  • UP
  • DOWN

For example:

UP   best ask = 0.60
DOWN best ask = 0.40
Enter fullscreen mode Exit fullscreen mode

Instead of buying at the current ask, the bot places orders below it.

For example, with an 0.08 offset:

UP order   = 0.60 - 0.08 = 0.52
DOWN order = 0.40 - 0.08 = 0.32
Enter fullscreen mode Exit fullscreen mode

So the bot places:

BUY UP   @ 0.52
BUY DOWN @ 0.32
Enter fullscreen mode Exit fullscreen mode

The goal is to have both orders filled during short-term market movement.


Why Both Sides?

The interesting part is the combined position.

Suppose both orders fill:

UP   = 0.52
DOWN = 0.32

Total = 0.84
Enter fullscreen mode Exit fullscreen mode

If both outcomes are held through settlement, the combined position has a $1 settlement value.

So the basic calculation is:

combined_cost = up_price + down_price

potential_edge = 1.00 - combined_cost
Enter fullscreen mode Exit fullscreen mode

In this example:

potential_edge = 1.00 - 0.84
               = 0.16
Enter fullscreen mode Exit fullscreen mode

This is only a gross theoretical edge. Real execution needs to account for fees, partial fills, timing, and other costs.


The Real Problem: Partial Fills

This is where the strategy gets interesting.

Suppose:

UP   → FILLED @ 0.52
DOWN → NOT FILLED
Enter fullscreen mode Exit fullscreen mode

Now the bot isn't holding a paired position.

It has directional exposure to UP.

Therefore, the bot needs to track inventory separately:

UP inventory
DOWN inventory
Paired inventory
Unpaired inventory
Enter fullscreen mode Exit fullscreen mode

For example:

UP   = 100
DOWN = 70

Paired = 70
Unpaired UP = 30
Enter fullscreen mode Exit fullscreen mode

This distinction should be part of the position manager.


Dynamic Order Distance

I don't want to hardcode:

best_ask - 0.08
Enter fullscreen mode Exit fullscreen mode

forever.

Market activity changes, so the order distance should be configurable and potentially dynamic.

A simple first version could use:

Low movement     → smaller offset
Medium movement  → medium offset
High movement    → larger offset
Enter fullscreen mode Exit fullscreen mode

The bot can measure short-term movement:

price_change_1s
price_change_3s
price_change_5s
Enter fullscreen mode Exit fullscreen mode

and use that information when calculating the next order price.

The exact values should come from historical/live testing.


Chainlink / TWAP Data

The order book isn't the only input.

The bot also needs the reference price information:

Chainlink price
TWAP
UP price
DOWN price
Time remaining
Enter fullscreen mode Exit fullscreen mode

A simplified flow is:

Chainlink / TWAP
       ↓
Market state
       ↓
Order book
       ↓
Strategy engine
       ↓
Limit orders
Enter fullscreen mode Exit fullscreen mode

This helps the bot understand the market state instead of reacting only to the latest order-book update.


Order Lifecycle

The execution loop can be kept relatively simple.

1. Read market data

UP bid/ask
DOWN bid/ask
Chainlink/TWAP
time remaining
inventory
Enter fullscreen mode Exit fullscreen mode

2. Calculate target prices

UP target
DOWN target
combined cost
potential edge
Enter fullscreen mode Exit fullscreen mode

3. Submit orders

BUY UP
BUY DOWN
Enter fullscreen mode Exit fullscreen mode

4. Monitor

Watch:

  • order status
  • fills
  • price movement
  • inventory

5. Reprice

If the market moves enough:

cancel old orders
calculate new prices
submit new orders
Enter fullscreen mode Exit fullscreen mode

6. Manage inventory

If only one side fills, the risk manager decides what to do with the unpaired position.


Avoiding Stale Orders

Fast markets make stale orders dangerous.

For example:

UP ask = 0.60

Our bid = 0.52
Enter fullscreen mode Exit fullscreen mode

A few seconds later:

UP ask = 0.72
Enter fullscreen mode Exit fullscreen mode

The original order may no longer make sense relative to the current market state.

The bot therefore needs an order-refresh mechanism.

Conceptually:

if market_move > repricing_threshold:
    cancel_order()
    calculate_new_price()
    submit_order()
Enter fullscreen mode Exit fullscreen mode

The threshold should be tested rather than chosen arbitrarily.


Time Remaining

The same strategy behaves differently depending on how much time is left in the market.

For example:

4 minutes remaining
Enter fullscreen mode Exit fullscreen mode

gives the bot considerably more time to complete a pair than:

10 seconds remaining
Enter fullscreen mode Exit fullscreen mode

So time_remaining should be a first-class strategy variable.

A simple approach:

More time
    → normal quoting

Less time
    → reduce new exposure

Very close to settlement
    → strict inventory controls
Enter fullscreen mode Exit fullscreen mode

Architecture

For the implementation, I would separate the system into several components:

┌─────────────────────┐
│    Market Data      │
│                     │
│ Order Book          │
│ Chainlink / TWAP    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   Strategy Engine   │
│                     │
│ Price calculation   │
│ Volatility          │
│ Pair calculation    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│    Order Manager    │
│                     │
│ Place               │
│ Cancel              │
│ Reprice             │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Position Manager   │
│                     │
│ UP inventory        │
│ DOWN inventory      │
│ Paired inventory    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│    Risk Manager     │
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Keeping these components separate makes it much easier to test the strategy without connecting the execution layer immediately.


Start With a Simulator

Before running this with real capital, I would first replay live order-book data.

For every market update, store:

timestamp
Chainlink price
TWAP
time remaining

UP bid
UP ask

DOWN bid
DOWN ask

simulated UP order
simulated DOWN order

UP fill
DOWN fill
Enter fullscreen mode Exit fullscreen mode

Then test different offsets:

0.05
0.06
0.07
0.08
0.09
0.10
Enter fullscreen mode Exit fullscreen mode

The important metrics are:

Two-sided fill rate
One-sided fill rate
Average paired cost
Average time to pair
Maximum unpaired inventory
Enter fullscreen mode Exit fullscreen mode

This is more useful than looking only at total P&L.


The Core Calculation

The first version of the strategy can be reduced to:

up_target = up_best_ask - offset
down_target = down_best_ask - offset

combined_cost = up_target + down_target

potential_edge = 1.0 - combined_cost
Enter fullscreen mode Exit fullscreen mode

But the execution engine should not automatically trade just because potential_edge is positive.

It also needs to consider:

fill probability
time remaining
inventory
market movement
fees
order-book liquidity
Enter fullscreen mode Exit fullscreen mode

The goal is not simply to find a cheap theoretical pair.

The goal is to determine whether the pair can actually be executed.


What I Want to Measure

The main questions I'm testing are:

  1. How often do both orders fill?
  2. How often does only one side fill?
  3. How long does it take to complete a pair?
  4. Which offset gives the best fill behavior?
  5. How does TWAP movement affect fills?
  6. How does the strategy behave near settlement?
  7. How much unpaired inventory accumulates?

These measurements should tell us whether the strategy is actually viable as an execution system.


Final Thoughts

The interesting part of this strategy isn't simply:

"Buy both sides."

The difficult engineering problem is managing everything that happens between the two fills.

Market data
     ↓
Calculate quote
     ↓
Place both orders
     ↓
One order fills
     ↓
Track exposure
     ↓
Wait / reprice / manage
     ↓
Second order fills
     ↓
Paired position
Enter fullscreen mode Exit fullscreen mode

That's the part I'm currently experimenting with after the TWAP upgrade.

I'll be testing the strategy using live market data first, then refining the execution and risk-management logic based on actual fill behavior.


Resources

I'm documenting my Polymarket bot research and experiments here:

GitHub:
https://github.com/Benjam1nCup/Polymarket-trading-bot-python-V2

The repository contains educational material, strategy research, and implementation experiments around automated Polymarket trading.

Contact:
https://telegram.me/BenjaminCup

If you're interested in discussing Polymarket bot development, trading infrastructure, or strategy research, feel free to reach out.

Top comments (0)