Day 15 — Polymarket Market Maker Bot Development Series
Learn how to calculate realized and unrealized P&L, track inventory, measure execution quality, analyze adverse selection, and build a performance monitoring system using Python and PostgreSQL.
Your Polymarket trading bot has been running for a week.
It has submitted 2,000 orders, completed hundreds of trades, and continuously updated its quotes.
Everything looks healthy.
But there's one question you still need to answer:
Is the bot actually making money?
A bot can execute thousands of orders and still lose money.
Maybe the spreads are too narrow. Maybe fees consume most of the trading profits. Or perhaps the bot keeps buying YES tokens immediately before their prices fall.
Without proper analytics, you won't know what's happening.
In this tutorial, we'll build the foundation of a performance analytics system for a Polymarket market maker.
We'll cover:
- Realized and unrealized profit and loss
- Inventory tracking and cost basis
- Trading fees and execution costs
- Fill rates and cancellation rates
- Adverse selection and post-fill markouts
- Python-based P&L calculations
- PostgreSQL performance tables
- Multi-market performance reporting
- Strategy evaluation and capital efficiency
The goal isn't to build a dashboard full of impressive numbers.
It's to build a system that tells us whether our trading strategy is working.
1. Why a Market Maker Needs Performance Analytics
Market making is relatively straightforward in theory.
You place buy orders below the current market price and sell orders above it.
When both sides execute, you capture the difference.
For example:
Buy YES: $0.47
Sell YES: $0.50
Spread captured: $0.03
If we buy 100 YES shares at $0.47 and sell them at $0.50:
Purchase cost: 100 × $0.47 = $47
Sale proceeds: 100 × $0.50 = $50
Gross P&L: $3.00
That looks good.
But suppose we paid $0.20 in total fees and other attributable costs.
Our net profit becomes:
Gross P&L: $3.00
Costs: $0.20
-----------------
Net P&L: $2.80
Now consider a different situation.
Our bot buys 100 YES shares at $0.47, but only sells 50 of them.
The remaining shares are still exposed to price movements.
If the market drops, our realized trading profit might look positive while the overall position is losing money.
This is why we need to separate different types of P&L.
2. Understanding Realized and Unrealized P&L
There are two important measurements that every trading bot should calculate.
Realized P&L
Realized P&L represents profit or loss from positions that have been closed or settled.
Example:
Bought: 100 YES @ $0.47
Sold: 100 YES @ $0.50
Cost: $47.00
Proceeds: $50.00
Realized gross P&L: +$3.00
After subtracting attributable fees and costs, we get realized net P&L.
Unrealized P&L
Unrealized P&L represents the estimated profit or loss on positions we still hold.
Suppose we have:
YES inventory: 100 shares
Average cost: $0.47
Current mark: $0.51
The current estimated market value is:
100 × $0.51 = $51.00
Our original cost was $47.
Therefore:
Unrealized gross P&L = $51 - $47
= +$4.00
But this is not guaranteed profit.
The order book might not have enough liquidity to sell all 100 shares at $0.51.
Actual liquidation proceeds may be lower because of slippage, market depth, and applicable fees.
For this reason, a production analytics system should track both midpoint valuations and estimated executable liquidation values.
Resolution P&L
Polymarket positions can also be held until market resolution.
For a standard binary contract, a winning share generally pays $1, while a losing share pays $0.
If we hold 100 YES shares:
YES wins:
Payout = $100
YES loses:
Payout = $0
The final profit or loss depends on the cost basis of those shares and applicable costs.
A good accounting system needs to handle trading P&L and resolution P&L consistently.
3. Tracking Inventory and Average Cost
Let's walk through a more realistic trading sequence.
Our bot executes four trades:
| Trade | Side | Quantity | Price |
|---|---|---|---|
| 1 | Buy YES | 100 | $0.47 |
| 2 | Sell YES | 60 | $0.50 |
| 3 | Buy YES | 40 | $0.48 |
| 4 | Sell YES | 30 | $0.51 |
First, calculate the total quantity purchased.
100 + 40 = 140 shares
Total purchase cost:
100 × $0.47 = $47.00
40 × $0.48 = $19.20
Total = $66.20
Total quantity sold:
60 + 30 = 90 shares
Total sale proceeds:
60 × $0.50 = $30.00
30 × $0.51 = $15.30
Total = $45.30
Remaining inventory:
140 - 90 = 50 YES shares
Using the simplified weighted-average calculation from our example:
Average cost = $66.20 / 140
≈ $0.472857
The cost basis of the remaining 50 shares is approximately:
50 × $0.472857 = $23.642857
The cost basis of the 90 sold shares is approximately:
90 × $0.472857 = $42.557143
Therefore:
Realized gross P&L:
$45.30 - $42.557143
= +$2.742857
If the current midpoint is $0.49:
Remaining inventory value:
50 × $0.49 = $24.50
Unrealized gross P&L:
$24.50 - $23.642857
= +$0.857143
Total estimated gross P&L:
Realized: +$2.742857
Unrealized: +$0.857143
----------------------
Total: +$3.600000
These figures are before fees.
Important implementation detail: This example uses a simplified pooled weighted-average calculation. A live accounting engine should update its running average cost after every buy and reduce the remaining cost basis after every sell. The order of fills matters, so we shouldn't calculate realized P&L from a final average that includes later purchases.
Let's implement that properly.
4. Building a Python P&L Calculator
Python's Decimal class is useful for financial calculations because it avoids many of the rounding problems associated with binary floating-point arithmetic.
We'll build a small inventory tracker that maintains:
- Current quantity
- Remaining cost basis
- Realized gross P&L
- Average entry price
from decimal import Decimal
class PositionTracker:
def __init__(self):
self.quantity = Decimal("0")
self.cost_basis = Decimal("0")
self.realized_pnl = Decimal("0")
@property
def average_cost(self):
if self.quantity == 0:
return Decimal("0")
return self.cost_basis / self.quantity
def buy(self, quantity, price):
quantity = Decimal(str(quantity))
price = Decimal(str(price))
if quantity <= 0:
raise ValueError("Quantity must be positive")
self.cost_basis += quantity * price
self.quantity += quantity
def sell(self, quantity, price):
quantity = Decimal(str(quantity))
price = Decimal(str(price))
if quantity <= 0:
raise ValueError("Quantity must be positive")
if quantity > self.quantity:
raise ValueError("Insufficient inventory")
matched_cost = quantity * self.average_cost
proceeds = quantity * price
self.realized_pnl += proceeds - matched_cost
self.cost_basis -= matched_cost
self.quantity -= quantity
if self.quantity == 0:
self.cost_basis = Decimal("0")
def unrealized_pnl(self, mark_price):
mark_price = Decimal(str(mark_price))
return (
self.quantity * mark_price
- self.cost_basis
)
def total_gross_pnl(self, mark_price):
return (
self.realized_pnl
+ self.unrealized_pnl(mark_price)
)
Let's test it with our previous trades.
position = PositionTracker()
position.buy("100", "0.47")
position.sell("60", "0.50")
position.buy("40", "0.48")
position.sell("30", "0.51")
mark_price = Decimal("0.49")
print("Quantity:", position.quantity)
print("Average cost:", position.average_cost)
print("Realized P&L:", position.realized_pnl)
print(
"Unrealized P&L:",
position.unrealized_pnl(mark_price)
)
print(
"Total gross P&L:",
position.total_gross_pnl(mark_price)
)
Expected results, rounded:
Quantity: 50
Average cost: 0.475
Realized P&L: 2.85
Unrealized P&L: 0.75
Total gross P&L: 3.60
Notice that realized and unrealized P&L differ slightly from the simplified pooled-average example.
That's expected.
The running weighted-average method updates the cost basis as trades occur, whereas the earlier illustration pooled all purchases together.
Both produce the same total estimated gross P&L in this example, but they allocate it differently between realized and unrealized results.
For production, we need additional handling for fees, partial fills, market resolution, transfers, reconciliation, and separate YES/NO positions.
The tracker above is an educational starting point, not a complete exchange accounting engine.
5. Measuring Fill Rate and Execution Quality
Profit is important, but it doesn't explain everything about how the strategy performs.
We also need to understand execution quality.
Fill rate
Fill rate tells us what percentage of submitted order quantity was executed.
Fill Rate =
Filled Quantity / Submitted Quantity × 100
Example:
Submitted quantity: 1,000
Filled quantity: 400
Fill rate: 40%
Python implementation:
from decimal import Decimal
def calculate_fill_rate(submitted, filled):
submitted = Decimal(str(submitted))
filled = Decimal(str(filled))
if submitted <= 0:
return Decimal("0")
return filled / submitted * Decimal("100")
print(calculate_fill_rate(1000, 400))
Output:
40.0
However, a higher fill rate isn't always better.
Imagine two strategies:
| Metric | Strategy A | Strategy B |
|---|---|---|
| Fill rate | 75% | 40% |
| Net P&L | -$15 | +$20 |
Strategy A receives more fills, but loses money.
One possible explanation is that its quotes are frequently stale.
Other traders may be executing against those quotes because the market is moving against the market maker.
This is called adverse selection.
Also, make sure the denominator in your fill-rate calculation is consistent. Repeated order replacements can inflate submitted quantity if they are counted incorrectly.
Cancellation rate
Another useful metric is the percentage of submitted orders that were canceled.
Cancellation Rate =
Canceled Orders / Submitted Orders × 100
A high cancellation rate can indicate excessive quote updates, unstable pricing parameters, or rapidly changing markets.
But cancellations can also be necessary for risk control.
We should evaluate cancellation rate alongside execution quality and profitability, rather than treating it as a standalone success metric.
6. Understanding Adverse Selection
Adverse selection is one of the biggest challenges in market making.
Here's a simple example.
Our bot places a YES buy order at $0.48.
Another trader sells into that order.
A few seconds later, the market midpoint falls to $0.44.
Our bot now holds shares worth less than the execution price.
Execution price: $0.48
Future midpoint: $0.44
Markout: -$0.04
This is an unfavorable fill.
To measure this systematically, we can calculate post-fill markouts.
For a buy:
Buy Markout =
Future Midpoint - Execution Price
For a sell:
Sell Markout =
Execution Price - Future Midpoint
A negative markout means the market moved against our execution.
Python markout calculator
from decimal import Decimal
def calculate_markout(
side,
execution_price,
future_midpoint
):
execution_price = Decimal(str(execution_price))
future_midpoint = Decimal(str(future_midpoint))
if side == "BUY":
return future_midpoint - execution_price
if side == "SELL":
return execution_price - future_midpoint
raise ValueError("Invalid trade side")
result = calculate_markout(
"BUY",
"0.48",
"0.44"
)
print("Markout:", result)
Output:
Markout: -0.04
A production system should calculate markouts at several intervals after each fill.
For example:
| Interval | Markout |
|---|---|
| 1 second | -0.002 |
| 5 seconds | -0.005 |
| 30 seconds | -0.012 |
| 60 seconds | -0.018 |
These are illustrative values.
Tracking multiple intervals helps us understand how the market behaves after our orders execute.
If our fill rate is high but markouts are consistently negative, our quotes may be too aggressive or slow to react to new information.
Midpoint-based markouts are diagnostic measurements, not guaranteed liquidation values.
For deeper analysis, we should also consider available order book depth and executable prices.
7. Tracking Inventory Risk
A market maker can show positive realized P&L while carrying a losing position.
Consider:
Realized P&L: +$20
Unrealized P&L: -$35
Total P&L: -$15
If our dashboard only displays realized profit, it will give us the wrong impression.
Inventory analytics should include:
- YES and NO positions
- Average acquisition cost
- Current mark-to-market value
- Estimated liquidation value
- Unrealized P&L
- Maximum position exposure
- Resolution scenario exposure
For multi-market bots, these values should be calculated separately for each market and then aggregated at the portfolio level.
YES and NO positions represent different outcome exposures, so we shouldn't simply combine their quantities into one inventory number.
8. Designing a PostgreSQL Analytics Database
Once our calculations work, we need somewhere to store the results.
In the previous article, we discussed database tables for markets, orders, fills, and positions.
Now we'll add analytics-specific tables.
P&L snapshots
Snapshots record portfolio performance at a particular point in time.
CREATE TABLE pnl_snapshots (
id BIGSERIAL PRIMARY KEY,
market_id BIGINT,
realized_pnl NUMERIC(20, 10),
unrealized_pnl NUMERIC(20, 10),
fees NUMERIC(20, 10),
total_pnl NUMERIC(20, 10),
recorded_at TIMESTAMPTZ
NOT NULL DEFAULT NOW()
);
We can record snapshots every minute, every five minutes, or at the end of each trading session.
The frequency depends on the level of detail we need.
Execution metrics
CREATE TABLE execution_metrics (
id BIGSERIAL PRIMARY KEY,
market_id BIGINT,
submitted_orders INTEGER
NOT NULL DEFAULT 0,
filled_orders INTEGER
NOT NULL DEFAULT 0,
canceled_orders INTEGER
NOT NULL DEFAULT 0,
submitted_quantity NUMERIC(20, 10),
filled_quantity NUMERIC(20, 10),
recorded_at TIMESTAMPTZ
NOT NULL DEFAULT NOW()
);
This table allows us to analyze order execution over time.
Fill markouts
CREATE TABLE fill_markouts (
id BIGSERIAL PRIMARY KEY,
exchange_fill_id TEXT NOT NULL,
interval_seconds INTEGER NOT NULL,
execution_price NUMERIC(20, 10) NOT NULL,
future_midpoint NUMERIC(20, 10),
markout NUMERIC(20, 10),
recorded_at TIMESTAMPTZ
NOT NULL DEFAULT NOW(),
UNIQUE (
exchange_fill_id,
interval_seconds
)
);
The unique constraint prevents us from accidentally storing multiple observations for the same fill and interval.
For a production database, we'd also add appropriate indexes, foreign keys, consistent market identifiers, and clearly documented calculation rules.
9. Building a Simple Performance Report in Python
Now let's combine some of our metrics into a reporting function.
from decimal import Decimal
def calculate_performance(
realized_pnl,
unrealized_pnl,
fees,
submitted_quantity,
filled_quantity
):
total_pnl = (
realized_pnl
+ unrealized_pnl
- fees
)
if submitted_quantity > 0:
fill_rate = (
filled_quantity / submitted_quantity
) * Decimal("100")
else:
fill_rate = Decimal("0")
return {
"realized_pnl": realized_pnl,
"unrealized_pnl": unrealized_pnl,
"fees": fees,
"total_pnl": total_pnl,
"fill_rate": fill_rate
}
report = calculate_performance(
realized_pnl=Decimal("20"),
unrealized_pnl=Decimal("-5"),
fees=Decimal("2"),
submitted_quantity=Decimal("1000"),
filled_quantity=Decimal("400")
)
for key, value in report.items():
print(f"{key}: {value}")
Output:
realized_pnl: 20
unrealized_pnl: -5
fees: 2
total_pnl: 13
fill_rate: 40.0
Our total estimated net P&L is $13.
The reporting function assumes the P&L inputs are gross figures and that fees have not already been deducted.
In a production system, we need to be explicit about those assumptions to avoid double-counting costs.
We also need to account for deposits, withdrawals, settlements, and transfers when calculating account-level returns.
10. Comparing Performance Across Markets
Suppose our bot trades BTC, ETH, SOL, and XRP prediction markets.
We collect the following results:
| Market | Realized P&L | Unrealized P&L | Fees |
|---|---|---|---|
| BTC | +$15 | -$3 | $2 |
| ETH | +$8 | +$2 | $1 |
| SOL | -$5 | -$4 | $1 |
| XRP | +$4 | +$1 | $0.50 |
Portfolio totals:
Realized P&L: +$22.00
Unrealized P&L: -$4.00
Fees: $4.50
Estimated net P&L: +$13.50
The overall result is positive.
But notice that SOL is losing money.
That gives us something specific to investigate.
Maybe SOL has worse liquidity. Maybe our quotes are too aggressive. Maybe adverse selection is higher, or inventory management is weaker.
Instead of immediately changing the entire strategy, we can analyze the SOL market separately.
However, we shouldn't disable a market after one poor result without considering sample size, volatility, and whether the difference is statistically meaningful.
11. Measuring Capital Efficiency
Profit alone doesn't tell us how efficiently the bot uses its capital.
Suppose two configurations generate the same profit.
Strategy A:
Capital allocated: $1,000
Net P&L: $20
Strategy B:
Capital allocated: $5,000
Net P&L: $20
Their returns on allocated capital are different.
Return on Capital =
Net P&L / Capital Base × 100
Strategy A:
$20 / $1,000 × 100 = 2%
Strategy B:
$20 / $5,000 × 100 = 0.4%
Strategy A appears more capital-efficient under this definition.
But we also need to consider the time period, drawdown, inventory exposure, and risk.
The capital base itself must be defined consistently.
It might represent initial allocated capital, average deployed capital, or total account equity.
Comparing returns without matching these definitions can be misleading.
12. What Should a Trading Analytics Dashboard Display?
Once we have reliable calculations and stored metrics, we can build a dashboard.
A useful dashboard should answer three questions:
- Is the bot making money?
- Where are the profits and losses coming from?
- Is the strategy taking too much risk?
A simplified portfolio overview might look like this:
POLYMARKET MARKET MAKER
--------------------------------
Portfolio P&L: +$13.00
Realized P&L: +$20.00
Unrealized P&L: -$5.00
Fees: $2.00
Fill Rate: 40%
Open Orders: 8
Active Markets: 4
Risk Status: WITHIN LIMITS
--------------------------------
For market-level analysis:
| Market | Net P&L | Fill Rate | Inventory |
|---|---|---|---|
| BTC | +$10 | 45% | 80 YES |
| ETH | +$9 | 52% | 40 NO |
| SOL | -$10 | 25% | 100 YES |
| XRP | +$4 | 38% | 20 YES |
These are illustrative figures.
For visualization, I would prioritize:
- Cumulative net P&L
- Realized versus unrealized P&L
- P&L by market
- Fill rate over time
- Inventory exposure
- Fees over time
- Post-fill markouts
- Order cancellations and rejections
Each chart should answer a specific question.
For example, a cumulative P&L chart shows whether the bot is generating consistent returns.
An inventory chart shows whether positions are growing too large.
A markout chart helps identify whether fills are occurring just before unfavorable price movements.
13. Evaluating Different Market-Making Strategies
Once we can measure performance, we can start comparing configurations.
Imagine three strategies:
| Parameter | Strategy A | Strategy B | Strategy C |
|---|---|---|---|
| Half-spread | $0.01 | $0.02 | $0.03 |
| Inventory skew | Low | Medium | High |
After testing them under comparable conditions, we obtain:
| Metric | A | B | C |
|---|---|---|---|
| Fill rate | 65% | 45% | 25% |
| Net P&L | -$5 | +$12 | +$8 |
| Avg. markout | -0.020 | -0.005 | +0.001 |
| Max inventory | 200 | 120 | 70 |
These are hypothetical results.
Strategy A has the highest fill rate, but it loses money.
Strategy B produces the highest net P&L.
Strategy C has the lowest inventory exposure and slightly positive average markout, but fewer fills.
Which is best?
We can't answer that using a single metric.
We need to evaluate profitability, inventory risk, drawdown, capital usage, execution quality, and stability across different market conditions.
And we should avoid changing several parameters at once.
If we adjust spread, inventory skew, quote frequency, and order size simultaneously, it becomes difficult to identify which change improved performance.
Controlled experiments and out-of-sample testing are much more useful.
14. Common Mistakes When Measuring Trading Bot Performance
Before trusting the dashboard, check for these mistakes.
Ignoring unrealized losses
A bot may show positive realized profit while carrying losing inventory.
Ignoring fees
A strategy can capture positive spreads and still lose money after trading costs.
Treating midpoint prices as executable prices
A midpoint is a valuation reference, not a guaranteed sale price.
Optimizing only for fill rate
Frequent fills may indicate stale quotes and adverse selection.
Comparing different market conditions
A configuration tested during low volatility may behave very differently during a volatile market.
Mixing accounting methods
FIFO, weighted-average cost, and other methods can produce different realized P&L figures.
Use a consistent approach.
Changing too many parameters
Make controlled changes so that performance improvements can be attributed to specific adjustments.
15. Production Analytics Checklist
Before relying on performance reports, make sure your bot can:
- [ ] Record every completed fill
- [ ] Track partial executions correctly
- [ ] Maintain inventory cost basis
- [ ] Calculate realized P&L
- [ ] Calculate unrealized P&L
- [ ] Account for trading fees
- [ ] Track YES and NO inventory separately
- [ ] Estimate executable liquidation value
- [ ] Record resolution payouts
- [ ] Calculate fill and cancellation rates
- [ ] Measure post-fill markouts
- [ ] Monitor inventory exposure
- [ ] Calculate portfolio-level P&L
- [ ] Compare performance across markets
- [ ] Track capital efficiency
- [ ] Monitor drawdown
- [ ] Store historical performance snapshots
- [ ] Generate daily and weekly reports
- [ ] Reconcile calculations with exchange data
The most important part is consistency.
If the same trade produces different P&L results depending on which report we open, we have an accounting problem that needs to be fixed before optimizing the strategy.
Conclusion
Building a Polymarket market maker isn't just about placing orders and capturing spreads.
We also need to understand what happens after those orders execute.
A proper analytics system tells us:
- How much money the bot actually earns
- How much profit is still exposed to market movements
- Whether trading fees are reducing returns
- Which markets perform best
- Whether our quotes suffer from adverse selection
- How efficiently we're using capital
- Whether parameter changes improve results
A bot that places 10,000 orders isn't necessarily better than one that places 500.
What matters is the quality of execution, the amount of risk taken, and the net result.
Once we can measure those things reliably, we have a much stronger foundation for improving our trading strategy.
What's Next?
Day 16: Polymarket Market Maker Bot — Advanced Strategy Optimization, Parameter Tuning, and Adaptive Trading
In the next article, we'll explore how to use performance data to adjust spreads, inventory skew, order sizes, and quote behavior.
We'll also discuss why blindly optimizing parameters against historical results can lead to overfitting.
Follow the Project
If you're building a Polymarket trading bot or working on algorithmic trading infrastructure, you can follow the project here:
GitHub:
Benj4minCup
/
Polymarket-Trading-Bot-python
polymarket Twap Bot polymarket Arbitrage Bot polymarket bot
Polymarket Trading bot system
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.
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 bot development and customization are also available.
Features
…YouTube:
Telegram: https://telegram.me/BenjaminCup
Original article on Medium:


Top comments (0)