DEV Community

Benjamin-Cup
Benjamin-Cup

Posted on

# Polymarket Trading Bot Arbitrage: How to Calculate the Real Edge Before Your Bot Trades

polymarket Trading Bot

Finding a 3¢ arbitrage opportunity on Polymarket looks simple.

Your bot sees:

YES + NO < $1.00
Enter fullscreen mode Exit fullscreen mode

and assumes the difference is profit.

But the displayed gap is not your real edge.

Two costs can destroy the trade:

  1. The spread
  2. Taker fees

And when you're trading both sides of an arbitrage, those costs can add up quickly.

This article shows how to calculate the actual cost before your bot sends an order.


1. The spread is a hidden trading cost

Every order book has:

  • Best bid — highest price someone will pay
  • Best ask — lowest price someone will sell for

The difference is the spread.

For example:

Best bid: 0.52
Best ask: 0.55

Spread: 0.03
Enter fullscreen mode Exit fullscreen mode

You don't see "0.03" as a fee on your trading statement.

But if you buy at 0.55 instead of 0.52, you're paying that spread.

A simple calculation:

def spread(book):
    best_bid = book.bids[0].price
    best_ask = book.asks[0].price

    return round(best_ask - best_bid, 4)
Enter fullscreen mode Exit fullscreen mode

The important point for an arbitrage bot is:

The spread is already inside the price you pay.


2. Arbitrage crosses the spread twice

This becomes more important when you're trading both sides of a market.

Suppose your bot finds:

YES
Ask: 0.54

NO
Ask: 0.43
Enter fullscreen mode Exit fullscreen mode

The pair costs:

0.54 + 0.43 = 0.97
Enter fullscreen mode Exit fullscreen mode

So the gross arbitrage edge appears to be:

1.00 - 0.97 = 0.03
Enter fullscreen mode Exit fullscreen mode

A 3¢ edge.

But don't calculate this using mid-prices.

Your bot actually has to buy at the ask.

def true_entry_cost(yes_book, no_book):
    yes_ask = yes_book.asks[0].price
    no_ask = no_book.asks[0].price

    return round(yes_ask + no_ask, 4)
Enter fullscreen mode Exit fullscreen mode

The rule is simple:

Measure your arbitrage using executable prices, not theoretical mid-prices.

The mid is a quote.

The ask is the bill.


3. Taker fees are not a flat percentage

The second major cost is the taker fee.

The fee depends on the price and market category rather than simply being one fixed percentage.

The basic calculation used in this model is:

def taker_fee(shares, price, fee_rate):
    return round(
        shares * fee_rate * price * (1 - price),
        6
    )
Enter fullscreen mode Exit fullscreen mode

The important part is:

price × (1 - price)
Enter fullscreen mode Exit fullscreen mode

This reaches its maximum around:

price = 0.50
Enter fullscreen mode Exit fullscreen mode

and becomes smaller as the price approaches 0 or 1.

That means a trade around 50¢ can have a significantly different fee cost from a trade near the edges of the probability range.


4. Market category matters

The fee rate is category-dependent.

For the rates used in this example:

FEE_RATES = {
    "crypto":      0.07,
    "economics":   0.05,
    "culture":     0.05,
    "weather":     0.05,
    "politics":    0.04,
    "finance":     0.04,
    "tech":        0.04,
    "sports":      0.03,
    "geopolitics": 0.00,
}
Enter fullscreen mode Exit fullscreen mode

At a price of 0.50, the maximum fee per 100 shares in this model is:

def max_fee_per_100(category):
    return round(
        100 * FEE_RATES[category] * 0.5 * 0.5,
        4
    )
Enter fullscreen mode Exit fullscreen mode

For example:

Crypto:      $1.75 / 100 shares
Sports:      $0.75 / 100 shares
Geopolitics: $0.00 / 100 shares
Enter fullscreen mode Exit fullscreen mode

This matters because crypto Up/Down markets can have a much larger fee impact than the raw arbitrage gap suggests.


5. Maker vs. taker changes the calculation

This is one of the most important parts of the strategy.

If your order rests on the book and another trader fills it, you're a maker.

If your order crosses the spread and immediately takes liquidity, you're a taker.

The distinction matters because the fee treatment is different.

Maker

Order rests
↓
Someone fills you
↓
Maker
Enter fullscreen mode Exit fullscreen mode

Taker

Order crosses the book
↓
You immediately consume liquidity
↓
Taker
Enter fullscreen mode Exit fullscreen mode

And there is an important trap:

A limit order is not automatically a maker order.

For example:

def is_taker(order_price, side, book):

    if side == "BUY":
        return order_price >= book.asks[0].price

    return order_price <= book.bids[0].price
Enter fullscreen mode Exit fullscreen mode

If your buy limit price reaches the current ask, you're crossing the book.

That order can execute as a taker.


6. The real arbitrage calculation

Now we can combine everything.

The gross edge is:

1 - YES ask - NO ask
Enter fullscreen mode Exit fullscreen mode

Then we subtract the taker fees when the orders are taking liquidity.

def real_edge(yes_book, no_book, shares, category, taking=True):

    yes_ask = yes_book.asks[0].price
    no_ask = no_book.asks[0].price

    # Spread is already reflected in executable ask prices
    pair_cost = yes_ask + no_ask

    gross_edge = 1.0 - pair_cost

    fees = 0.0

    if taking:
        rate = FEE_RATES[category]

        fees += (
            taker_fee(shares, yes_ask, rate)
            / shares
        )

        fees += (
            taker_fee(shares, no_ask, rate)
            / shares
        )

    net = gross_edge - fees

    return {
        "gross_edge": round(gross_edge, 4),
        "fees_per_share": round(fees, 4),
        "net_edge": round(net, 4),
    }
Enter fullscreen mode Exit fullscreen mode

The important flow is:

Order book
    ↓
Executable YES ask + NO ask
    ↓
Gross edge
    ↓
Taker fee on YES
    ↓
Taker fee on NO
    ↓
NET EDGE
Enter fullscreen mode Exit fullscreen mode

Your bot should make its trading decision based on the net edge, not the gross gap.


7. Worked example: the 3¢ arbitrage that isn't profitable

Let's use:

YES ask = 0.54
NO ask  = 0.43
Enter fullscreen mode Exit fullscreen mode

The pair costs:

0.54 + 0.43 = 0.97
Enter fullscreen mode Exit fullscreen mode

Gross edge:

1.00 - 0.97 = 0.03
Enter fullscreen mode Exit fullscreen mode

So it looks like a 3¢ arbitrage.

Now assume:

100 shares
Crypto market
Both orders execute as takers
Fee rate = 0.07
Enter fullscreen mode Exit fullscreen mode

YES fee

100 × 0.07 × 0.54 × 0.46
≈ $1.74
Enter fullscreen mode Exit fullscreen mode

Per share:

≈ $0.017
Enter fullscreen mode Exit fullscreen mode

NO fee

100 × 0.07 × 0.43 × 0.57
≈ $1.72
Enter fullscreen mode Exit fullscreen mode

Per share:

≈ $0.017
Enter fullscreen mode Exit fullscreen mode

Total fee per share:

≈ $0.034
Enter fullscreen mode Exit fullscreen mode

But the gross arbitrage edge was only:

$0.030
Enter fullscreen mode Exit fullscreen mode

So:

Gross edge:  +$0.030
Fees:        -$0.034
--------------------
Net edge:    -$0.004
Enter fullscreen mode Exit fullscreen mode

The supposed 3¢ arbitrage is actually negative after fees.

That's the kind of trade an automated system should reject before placing the orders.


8. Build a minimum-edge filter

You can turn this calculation into a simple safety filter.

def survives(
    yes_book,
    no_book,
    shares,
    category,
    min_net=0.005,
    taking=True
):
    result = real_edge(
        yes_book,
        no_book,
        shares,
        category,
        taking
    )

    return result["net_edge"] >= min_net, result
Enter fullscreen mode Exit fullscreen mode

Now your bot can ask:

Is the net edge large enough?
Enter fullscreen mode Exit fullscreen mode

instead of:

Does YES + NO look smaller than $1?
Enter fullscreen mode Exit fullscreen mode

That is a much more useful condition for an arbitrage engine.


9. The real decision: speed vs. cost

There is a trade-off here.

Aggressive execution

You cross the spread and execute immediately.

Advantages:

  • Faster execution
  • Higher chance of capturing the current gap
  • Both legs can potentially execute quickly

Disadvantages:

  • You pay taker fees
  • You consume liquidity
  • Slippage can reduce the edge

Passive execution

You place resting orders.

Advantages:

  • Different fee treatment
  • You avoid immediately crossing the spread
  • You may receive a rebate

Disadvantages:

  • Your order may never fill
  • The arbitrage gap can disappear
  • One leg may fill while the other doesn't

So the strategy isn't simply:

Find arbitrage → buy both sides
Enter fullscreen mode Exit fullscreen mode

It's closer to:

Find candidate
      ↓
Check executable prices
      ↓
Calculate gross edge
      ↓
Calculate fees
      ↓
Check liquidity
      ↓
Check execution type
      ↓
Calculate net edge
      ↓
Trade only if the expected edge survives
Enter fullscreen mode Exit fullscreen mode

10. What an arbitrage bot should actually monitor

A production bot shouldn't rely on one number.

At minimum, the execution engine should track:

  • Best bid
  • Best ask
  • Spread
  • YES executable price
  • NO executable price
  • Available liquidity
  • Market category
  • Expected fee
  • Maker/taker status
  • Gross edge
  • Net edge
  • Minimum required edge

For larger orders, top-of-book prices aren't enough.

If your order consumes several levels of the order book, your actual average execution price can be worse than the first ask.

That means a more advanced system should calculate depth-aware execution cost, not just the first level.


Conclusion

The biggest mistake in automated arbitrage is confusing a visible price gap with actual profit.

A market can show:

YES + NO = $0.97
Enter fullscreen mode Exit fullscreen mode

and appear to offer:

$0.03
Enter fullscreen mode Exit fullscreen mode

of arbitrage.

But your real result depends on:

Executable prices
+ spread
+ liquidity
+ execution type
+ fees
= actual trading cost
Enter fullscreen mode Exit fullscreen mode

The practical rules are:

  1. Use executable ask prices, not mid-prices.
  2. Calculate both legs of the arbitrage.
  3. Include the applicable taker fee.
  4. Determine whether your orders actually execute as makers or takers.
  5. Check liquidity and potential slippage.
  6. Trade based on net edge, not gross edge.

The gross gap is what your detector sees.

The net edge is what your account actually gets.

If you're building automated Polymarket infrastructure, this calculation belongs inside the execution engine—not after the trade.


Project

I'm building Polymarket trading infrastructure and bots in Python.

GitHub:

Polymarket Trading Bot | Polymarket Arbitrage Bot | Polymarket TWAP Trading Bot

An open-source and Strong Strategy collection of Polymarket trading bot and Polymarket arbitrage bot and Polymarket TWAP trading bot in Python for high-performance automated trading on polymarket crypto 5min and 15min markets.

Polymarket-benjamincup-bot-dashboard

This repository is primarily intended for educational and research purposes. It includes strategy concepts, implementation approaches, and selected performance screenshots to help developers understand how different automated trading strategies can be designed and tested.

The repository does not provide a complete production-ready trading bot source code. Instead, it provides strategy descriptions and research materials that you can use as a foundation for developing your own system.

If you are interested in building a Polymarket Trading Bot, you can follow my tutorials and use the concepts in this repository to develop your own implementation.

For users who prefer a ready-to-deploy solution or require custom strategy development, commercial…




Telegram: https://telegram.me/BenjaminCup

If you're working on Polymarket bots, prediction-market infrastructure, market-data systems, or algorithmic trading tools, feel free to connect.

polymarket #tradingbot #python #algorithmictrading #cryptotrading #arbitrage #predictionmarkets #trading

Top comments (1)

Collapse
 
arhancanli profile image
Arhan Canli •

The worked example (a 3¢ gap that's -0.4¢ after fees in a crypto market) is the most useful thing in the post, and the price × (1 − price) shape of the fee is easy to miss.

Two more costs the filter should see before it says yes:

  1. Depth. real_edge prices all 100 shares at asks[0].price, but the best ask on each side might only have 20 shares behind it. The remaining 80 fill at the next levels, and the gap is usually gone after the first level. Walking the book on both sides for the actual size (volume-weighted ask for shares) and recomputing the edge on that gives the number the bot will really pay.

  2. Legging. The two orders don't fill at the same instant. If YES fills and NO moves before your second order lands, you're holding a directional position instead of an arbitrage. With passive orders it's worse, because the leg that fills first is usually the one the market was happy to sell you. Logging how often only one leg filled, and what that leg was worth at resolution, tells you whether the "risk-free" trades are carrying hidden directional risk. In practice that number decides whether the strategy works more than the fee model does.