Sitemap

Advanced Polymarket Market Making Strategy: Dynamic Spreads, Volatility, and Market Conditions in Python

11 min read2 hours ago

--

Day 8: Taking Your Polymarket Market Maker Bot Beyond the Basics

In the first seven articles, we built the foundation of a Polymarket market maker bot.

We learned how to understand the order book, retrieve market data, calculate bid and ask prices, manage orders, handle partial fills, control inventory risk, and prepare a bot for deployment.

Press enter or click to view image in full size
polymarket Trading bot

But there is still one important question:

Should your market maker use the same spread and order size in every market condition?

Imagine two situations.

Situation A: A quiet market

BTC is moving slowly. The Polymarket order book has reasonable liquidity, and the best bid and ask prices are relatively stable.

Situation B: A volatile market

BTC suddenly moves sharply. The order book changes quickly, liquidity disappears, and the probability of a YES outcome changes rapidly.

Should your bot use exactly the same bid, ask, and order size in both situations?

Probably not.

A fixed-spread market maker is easier to build, but it does not automatically adapt to changing market conditions.

In this article, we’ll explore how to build a more adaptive market maker using dynamic spreads, volatility measurements, liquidity checks, and market-regime filters.

We’ll cover:

  • Why fixed spreads can be dangerous.
  • How to estimate short-term volatility.
  • How to adjust spreads dynamically.
  • How to adapt order size to market liquidity.
  • How to identify unstable market conditions.
  • How to combine volatility and inventory controls.
  • How to implement a basic adaptive pricing model in Python.
  • How to evaluate whether dynamic quoting actually improves performance.

The goal is to make the bot respond intelligently to changing conditions instead of using one fixed configuration all the time.

1. Why Fixed-Spread Market Making Has Limitations

Let’s start with a simple market maker.

Suppose the YES token has a midpoint of $0.50.

Your bot always uses a half-spread of $0.02.

It calculates:

[
Bid=0.50–0.02=0.48
]

[
Ask=0.50+0.02=0.52
]

The resulting spread is four cents.

This is easy to understand and implement.

However, market conditions do not remain constant.

When volatility is low, a four-cent spread might be unnecessarily wide. Other traders may offer better prices, making your orders less competitive.

When volatility is high, the same four-cent spread might be too narrow. Your orders may be filled just before the market moves against you.

This creates two different problems:

In calm markets: Your quotes may be too conservative.

In volatile markets: Your quotes may be too aggressive.

A dynamic market maker attempts to adjust its behavior based on the current market environment.

It does not guarantee better results, but it gives the strategy a way to respond to changing risk.

2. Understand Market Volatility

Volatility describes how much a price changes over time.

A market with small, gradual price movements has relatively low short-term volatility.

A market with large and frequent price movements has relatively high short-term volatility.

For a Polymarket crypto market maker, you might monitor two different types of prices:

  1. The Polymarket YES or NO token price.
  2. The underlying cryptocurrency reference price, such as BTC.

These prices provide different information.

The Polymarket token price reflects the market’s changing probability estimate and available trading liquidity.

The BTC price reflects movement in the underlying asset.

For a BTC Up/Down market, both can be useful, but they should not be mixed into one measurement without a clearly defined model.

A simple example

Imagine these BTC prices:

TimeBTC price10:00:00$100,00010:00:05$100,01010:00:10$100,00810:00:15$100,012

The movement is relatively small.

Now imagine:

TimeBTC price10:00:00$100,00010:00:05$100,15010:00:10$99,92010:00:15$100,180

The second sequence shows much larger price changes.

A market maker should consider whether its current spread and order size are appropriate for each environment.

3. Calculate Short-Term Volatility in Python

One simple approach is to calculate price returns.

A return measures the percentage change between two consecutive prices.

[
r_t=\frac{P_t-P_{t-1}}{P_{t-1}}
]

For example, if BTC moves from $100,000 to $100,100:

[
r_t=\frac{100100–100000}{100000}=0.001
]

That is a return of 0.1%.

We can calculate the standard deviation of recent returns to estimate short-term volatility.

from collections import deque
from decimal import Decimal
from statistics import pstdev
class VolatilityTracker:
def __init__(self, max_samples=30):
self.prices = deque(maxlen=max_samples)
def add_price(self, price: Decimal):
if price <= 0:
raise ValueError("Price must be positive")
self.prices.append(price) def calculate_volatility(self):
if len(self.prices) < 3:
return None
returns = [] for previous, current in zip(
list(self.prices)[:-1],
list(self.prices)[1:],
):
returns.append(
float((current - previous) / previous)
)
return pstdev(returns)

This example calculates the population standard deviation of the observed returns.

It is intentionally simple. A production implementation should define its sampling interval, handle missing data, use consistent timestamps, and consider whether a rolling or exponentially weighted volatility estimate is more appropriate.

Also remember that the output depends on the data frequency.

Thirty price observations collected every second describe a different time horizon from thirty observations collected every minute.

Do not compare volatility values without considering their sampling intervals.

4. Use Volatility to Adjust the Spread

Now let’s connect volatility to quote calculation.

In Day 4, we used a fixed half-spread.

Now we want the half-spread to change according to market conditions.

A simplified formula is:

[
H=H_{\min}+\alpha V
]

Where:

  • (H) is the dynamic half-spread.
  • (H_{\min}) is the minimum half-spread.
  • (V) is the measured volatility.
  • (\alpha) controls how strongly volatility affects the spread.

This is an illustrative model. The volatility measure must be scaled appropriately before using it in a pricing formula.

Let’s look at a simplified example.

Suppose:

  • Minimum half-spread = $0.01.
  • Volatility measurement = 0.002.
  • Volatility coefficient = 5.

Then:

[
H=0.01+(5 \times 0.002)
]

[
H=0.02
]

If the midpoint is $0.50, the candidate quotes become:

  • Bid: $0.48.
  • Ask: $0.52.

When volatility increases, the half-spread increases.

This gives the bot more room between its buy and sell prices.

However, a wider spread does not eliminate adverse selection. If the market is moving extremely quickly, even a wide quote can become outdated.

Sometimes the safer decision is not to quote at all.

5. Implement Dynamic Spread Calculation

Let’s turn the basic formula into Python.

from decimal import Decimal
def calculate_dynamic_half_spread(
min_half_spread: Decimal,
volatility: Decimal,
volatility_factor: Decimal,
max_half_spread: Decimal,
):
if min_half_spread <= 0:
raise ValueError("Minimum spread must be positive")
if volatility < 0:
raise ValueError("Volatility cannot be negative")
if volatility_factor < 0:
raise ValueError("Volatility factor cannot be negative")
if max_half_spread < min_half_spread:
raise ValueError("Invalid maximum spread")
dynamic_spread = (
min_half_spread
+ volatility * volatility_factor
)
return min(dynamic_spread, max_half_spread)

Example:

half_spread = calculate_dynamic_half_spread(
min_half_spread=Decimal("0.01"),
volatility=Decimal("0.002"),
volatility_factor=Decimal("5"),
max_half_spread=Decimal("0.05"),
)
print("Half spread:", half_spread)

The function calculates a candidate half-spread and prevents it from exceeding the configured maximum.

A production bot should also define what happens when the calculated spread is too wide to quote competitively or violates the market’s valid price increments.

Do not assume that a larger spread is always better.

The objective is to balance execution probability against adverse-selection and inventory risk.

6. Adjust Order Size Based on Liquidity

Spread is only one part of the strategy.

Order size matters too.

Imagine the order book looks like this:

Market A

PriceAvailable quantity$0.491,000$0.481,500$0.472,000

Market B

PriceAvailable quantity$0.4920$0.4835$0.4750

Both markets may show similar best prices, but their available liquidity is very different.

Placing a 500-token order in Market A may be reasonable under your strategy’s limits.

Placing the same order in Market B could represent a much larger portion of the available liquidity.

This is why order size should consider:

  • Available order-book depth.
  • Current inventory.
  • Available funds.
  • Outstanding orders.
  • Volatility.
  • Maximum permitted exposure.

A basic liquidity ratio

You can estimate how large your order is compared with available depth:

[
L=\frac{Q_{\text{order}}}{Q_{\text{available}}}
]

For example:

  • Proposed order size: 50 tokens.
  • Available quantity near the relevant price: 500 tokens.

[
L=\frac{50}{500}=0.10
]

Your order is 10% of the selected available quantity.

This ratio does not predict whether the order will fill. It is simply a way to understand how large your order is relative to the visible liquidity you chose to measure.

Visible liquidity can disappear or change, and it does not necessarily represent the full amount available at execution time.

7. Create a Basic Adaptive Order Size Function

We can also adjust the proposed order size according to volatility.

Get BenjaminCup’s stories in your inbox

Join Medium for free to get updates from this writer.

For example, we might reduce the size when volatility rises.

from decimal import Decimal
def calculate_order_size(
base_size: Decimal,
volatility: Decimal,
volatility_sensitivity: Decimal,
minimum_size: Decimal,
):
if base_size <= 0:
raise ValueError("Base size must be positive")
if volatility < 0 or volatility_sensitivity < 0:
raise ValueError("Invalid volatility parameters")
size = base_size / (
Decimal("1")
+ volatility * volatility_sensitivity
)
return max(minimum_size, size)

Suppose the base size is 100 tokens.

As volatility increases, the calculated size decreases.

This is a simple educational model. It does not include inventory, liquidity, balance, minimum order sizes, or the market’s actual execution constraints.

In a real market maker, order size should be calculated only after the risk manager checks all relevant exposure.

8. Combine Dynamic Spreads With Inventory Skew

Now let’s combine the ideas from Day 4 and Day 6.

A market maker needs to consider at least two things:

  1. How risky is the current market?
  2. How much inventory does the bot already hold?

Volatility can influence the spread.

Inventory can influence the reference price.

A simplified model is:

[
P_{\text{adjusted}}=P_{\text{mid}}-kI
]

[
Bid=P_{\text{adjusted}}-H
]

[
Ask=P_{\text{adjusted}}+H
]

Where:

  • (P_{\text{mid}}) is the reference midpoint.
  • (I) is the inventory deviation from the target.
  • (k) is the inventory-skew coefficient.
  • (H) is the dynamic half-spread.

Let’s consider an example.

Assume:

  • Midpoint = $0.50.
  • Inventory deviation = 100 tokens.
  • Inventory skew = 0.0001.
  • Dynamic half-spread = $0.03.

The adjusted midpoint becomes:

[
0.50-(100 \times 0.0001)=0.49
]

The quotes become:

[
Bid=0.49–0.03=0.46
]

[
Ask=0.49+0.03=0.52
]

The bot has both widened its spread and shifted its reference price downward.

This is a basic way to combine market-condition risk with inventory risk.

But there is an important limitation: the formula assumes that the volatility and inventory measurements are meaningful and correctly scaled.

It also assumes that a two-sided quote remains appropriate.

If the market is unstable or the bot has exceeded its risk limit, it may be better to reduce exposure or pause quoting instead.

9. Detect Market Regimes

Not every market condition requires the same behavior.

You can divide market conditions into simple categories.

Market regimePossible characteristicsExample responseCalmStable prices and reasonable depthNormal quotingActiveMore frequent price changesRecalculate quotes more oftenVolatileLarge and rapid movementsWider spreads and smaller ordersIlliquidThin order book and large gapsReduce size or pauseUncertainStale data or unreliable stateStop new quoting until reconciled

These are conceptual categories, not universal thresholds.

Your bot needs measurable rules to determine which category applies.

For example, a volatility filter might compare recent volatility with a configured threshold.

A liquidity filter might compare available depth with the intended order size.

A data-quality filter might check how old the latest market update is.

A production system should also use hysteresis or minimum time-in-regime rules where appropriate.

Why?

Because a market can move around a threshold.

If your bot switches between normal and volatile mode every second, its behavior may become unstable.

Hysteresis means using different conditions to enter and exit a regime.

For example:

  • Enter volatile mode when volatility rises above 0.003.
  • Return to normal mode only when volatility falls below 0.002.

The gap between the thresholds helps prevent rapid switching.

10. Know When Not to Make a Market

An advanced market maker must understand that sometimes the correct decision is to do nothing.

Consider these situations:

Situation 1: Market data is stale

Your bot has not received a fresh order-book update.

It cannot safely assume that its previous quotes are still appropriate.

Situation 2: Liquidity disappears

The visible order book becomes extremely thin.

A normal order size may now represent a large share of available liquidity.

Situation 3: Volatility becomes extreme

The underlying crypto price moves rapidly, and the Polymarket token price changes faster than your strategy can safely react.

Situation 4: Inventory exceeds the limit

The bot already holds more tokens than its strategy allows.

Submitting additional buy orders could increase the exposure.

Situation 5: The order state is uncertain

A previous submission or cancellation timed out, and the bot cannot determine the current exchange state.

In these cases, the bot should follow its configured safety policy.

This might mean reducing orders, canceling certain resting orders, or pausing new submissions.

The exact action should depend on the risk condition and the bot’s current exposure.

A market maker should not be forced to quote at every moment.

11. Build a Simple Adaptive Quote Engine

Let’s combine the ideas into one educational function.

It will:

  1. Calculate the inventory-adjusted midpoint.
  2. Calculate a volatility-adjusted spread.
  3. Produce candidate bid and ask prices.
  4. Check basic price boundaries.

It will not place orders or replace the risk manager.

from decimal import Decimal
def calculate_adaptive_quotes(
midpoint: Decimal,
inventory: Decimal,
inventory_target: Decimal,
volatility: Decimal,
base_half_spread: Decimal,
volatility_factor: Decimal,
inventory_skew: Decimal,
):
if not Decimal("0") < midpoint < Decimal("1"):
raise ValueError("Invalid midpoint")
if volatility < 0:
raise ValueError("Volatility cannot be negative")
if base_half_spread <= 0:
raise ValueError("Base spread must be positive")
inventory_deviation = inventory - inventory_target adjusted_midpoint = (
midpoint
- inventory_deviation * inventory_skew
)
dynamic_half_spread = (
base_half_spread
+ volatility * volatility_factor
)
bid = adjusted_midpoint - dynamic_half_spread
ask = adjusted_midpoint + dynamic_half_spread
min_price = Decimal("0.01")
max_price = Decimal("0.99")
bid = max(min_price, min(bid, max_price))
ask = max(min_price, min(ask, max_price))
if bid >= ask:
raise ValueError("Invalid quote relationship")
return {
"bid": bid,
"ask": ask,
"adjusted_midpoint": adjusted_midpoint,
"half_spread": dynamic_half_spread,
"inventory_deviation": inventory_deviation,
}

Example:

quotes = calculate_adaptive_quotes(
midpoint=Decimal("0.50"),
inventory=Decimal("100"),
inventory_target=Decimal("0"),
volatility=Decimal("0.002"),
base_half_spread=Decimal("0.01"),
volatility_factor=Decimal("5"),
inventory_skew=Decimal("0.0001"),
)
print(quotes)

This is a starting point for experimentation, not a complete production quoting algorithm.

A real implementation must additionally validate:

  • The actual market tick size.
  • Minimum and maximum order sizes.
  • Market status.
  • Available balance.
  • Current order-book liquidity.
  • Outstanding orders and possible fills.
  • Maximum inventory and loss limits.
  • Data freshness.
  • Whether quoting should be paused.

It should also account for the fact that YES and NO tokens may need separate pricing and inventory models.

12. How Do You Know Dynamic Quoting Is Better?

Adding more parameters does not automatically improve a trading strategy.

A complicated bot can perform worse than a simple bot if its filters are poorly calibrated or its data is unreliable.

You should compare the fixed-spread and dynamic-spread strategies under the same evaluation conditions.

For example:

MetricFixed spreadDynamic spreadNet P&LMeasureMeasureFill rateMeasureMeasureMaximum inventoryMeasureMeasureAverage order sizeMeasureMeasurePost-fill price movementMeasureMeasureFees and execution costsMeasureMeasureTime spent quotingMeasureMeasureRisk-limit breachesMeasureMeasure

Do not fill this table with expected improvements. Collect actual results from your own tests.

A dynamic strategy might reduce inventory exposure but also receive fewer fills.

It might improve post-fill price movement while missing some trading opportunities.

It might perform better in volatile markets but worse in calm markets.

These are trade-offs to measure, not assumptions to make.

Run controlled experiments

A useful process is:

  1. Build a fixed-spread baseline.
  2. Record the data and decisions it produces.
  3. Add volatility-based spread adjustment.
  4. Compare the results using the same data and execution assumptions.
  5. Add inventory-aware size adjustment.
  6. Evaluate the new version separately.
  7. Test on data that was not used to tune the parameters.

Change one major feature at a time where possible.

This helps you understand which modification actually caused a performance difference.

Conclusion: Build a Market Maker That Adapts to the Market

A basic market maker can calculate a bid and ask using a fixed spread.

But real markets do not remain constant.

Volatility changes. Liquidity changes. Inventory changes. Order-book conditions change.

A more advanced market maker should recognize these changes and adjust its behavior accordingly.

The key lessons from Day 8 are:

  1. Fixed spreads have limitations. The same quote may be too wide in a calm market and too narrow in a volatile market.
  2. Measure volatility consistently. The sampling interval and data source matter.
  3. Adjust spreads carefully. Wider quotes may reduce some execution risks but do not guarantee better results.
  4. Adapt order size to risk and liquidity. A large order can be inappropriate in a thin market.
  5. Combine inventory and volatility controls. Both influence how aggressively the bot should quote.
  6. Recognize unsafe market conditions. Sometimes pausing is better than placing another order.
  7. Measure every improvement. More sophisticated logic is useful only when testing demonstrates a meaningful benefit.

The next step is not simply adding more filters or more complicated formulas.

It is building a strategy that can explain why it is quoting, how much risk it is taking, and when it should stop.

That is the difference between a basic trading script and a more sophisticated market-making system.

Continue the project

Follow the project and experiment with the concepts in this tutorial series.

#polymarket #trading #bot #strategy #tutorial #architecture

--

--

BenjaminCup
BenjaminCup

Written by BenjaminCup

Blockchain and Full-Stack DEV | Polymarket and Robinhood Chain Trading Bot Builder https://www.youtube.com/@BenjaminCup Telegram : @BenjaminCup