Day 14 of Building a Polymarket Market Maker Bot with Python
Your trading bot is running perfectly. It monitors BTC, ETH, SOL, and XRP markets, places orders, tracks inventory, and manages portfolio risk.
Then your VPS restarts.
Your Python process disappears. In-memory variables are reset. WebSocket connections are gone.
But here's the problem: your orders on Polymarket don't necessarily disappear with your bot.
Some might have been filled. Others might have been canceled. Your available balance could have changed while the bot was offline.
If your application simply restarts and resumes trading using outdated information, things can go wrong very quickly.
That's what we're tackling in Day 14.
Today, we're moving beyond trading strategies and building the infrastructure that makes a trading bot reliable.
We'll cover:
- PostgreSQL database design for orders, fills, and positions
- Persistent trading state
- Structured logging in Python
- Bot health monitoring
- Automated crash recovery
- Exchange reconciliation
- Duplicate-event prevention
- Safe trading resumption
The goal is simple: build a bot that doesn't lose track of its money when something breaks.
1. The Problem With In-Memory Trading State
Let's start with a simple Python example.
balance = 1000
yes_inventory = 50
no_inventory = 20
print("Bot started")
Nothing unusual here.
But these variables only exist while the Python process is running.
Imagine the bot currently has:
YES inventory: 50
NO inventory: 20
Open orders: 3
The VPS crashes.
While the bot is offline, one YES order gets filled and another order is canceled.
The exchange now reports:
YES inventory: 60
NO inventory: 20
Open orders: 1
But after restarting, the bot still assumes its previous values.
That difference is dangerous.
It could submit duplicate orders, incorrectly calculate available capital, or exceed its portfolio exposure limits.
The first rule of production trading infrastructure is that application memory must not be your only source of trading state.
We need persistent storage.
2. Choosing the Right Database
For a Python-based Polymarket trading system, PostgreSQL is a solid starting point.
Here's how the main storage options compare:
| Technology | Best Use |
|---|---|
| PostgreSQL | Orders, fills, positions, balances, and trading history |
| Redis | Temporary state, caching, and coordination |
| SQLite | Local development and lightweight testing |
| JSON files | Configuration and small local records |
For this project, we'll use PostgreSQL for durable trading records and optionally Redis for temporary operational state.
Our simplified architecture looks like this:
Polymarket Exchange
|
v
Execution Engine
|
v
Trading Events
|
+--------+--------+
| |
v v
PostgreSQL Redis
Persistent Data Temporary State
|
v
Recovery Engine
|
v
Exchange Reconciliation
|
v
Portfolio State
PostgreSQL records what happened.
Redis can help the application coordinate what is happening right now.
And the recovery engine makes sure the bot understands its current account state before placing new orders.
3. Designing the Trading Database
Before writing SQL, let's identify the information we need to preserve.
A practical database design includes these tables:
| Table | Responsibility |
|---|---|
markets |
Monitored market information |
orders |
Submitted and updated orders |
fills |
Individual trade executions |
positions |
Current inventory snapshots |
balance_snapshots |
Account balance history |
risk_events |
Risk decisions and warnings |
bot_sessions |
Startup and shutdown history |
Let's focus on the most important ones.
Markets
Each market needs a stable identifier.
For example:
| Market | Asset | Active |
|---|---|---|
| BTC 5-minute | BTC | true |
| ETH 5-minute | ETH | true |
| SOL 5-minute | SOL | true |
Don't rely entirely on market names or URLs.
Store the actual exchange market and token identifiers.
Orders
Every submitted order needs a record containing:
- Internal order ID
- Exchange order ID
- Market and token identifiers
- Buy or sell direction
- Price and quantity
- Filled quantity
- Current order status
- Creation and update timestamps
Fills
An order doesn't always execute all at once.
Imagine submitting an order for 100 tokens.
Order quantity: 100
Fill #1: 30
Fill #2: 20
Fill #3: 50
Those are three separate execution events.
Each fill affects inventory, capital, and potentially trading fees.
We need to preserve every one of them.
Positions
The positions table stores the latest calculated inventory.
BTC YES: 120 tokens
BTC NO: 40 tokens
ETH YES: 60 tokens
However, positions are snapshots.
We should still retain the underlying execution history so that we can reconstruct inventory if something becomes inconsistent.
4. Creating the PostgreSQL Schema
Let's build a simplified schema.
We'll start with markets.
CREATE TABLE markets (
id BIGSERIAL PRIMARY KEY,
exchange_market_id TEXT NOT NULL UNIQUE,
market_name TEXT NOT NULL,
asset TEXT,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Next, orders.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
exchange_order_id TEXT UNIQUE,
market_id BIGINT REFERENCES markets(id),
token_id TEXT NOT NULL,
side TEXT NOT NULL,
price NUMERIC(20, 10) NOT NULL,
quantity NUMERIC(20, 10) NOT NULL,
filled_quantity NUMERIC(20, 10)
NOT NULL DEFAULT 0,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Now the fills table.
CREATE TABLE fills (
id BIGSERIAL PRIMARY KEY,
exchange_fill_id TEXT UNIQUE,
exchange_order_id TEXT NOT NULL,
token_id TEXT NOT NULL,
price NUMERIC(20, 10) NOT NULL,
quantity NUMERIC(20, 10) NOT NULL,
fee NUMERIC(20, 10),
executed_at TIMESTAMPTZ NOT NULL
);
And positions.
CREATE TABLE positions (
id BIGSERIAL PRIMARY KEY,
market_id BIGINT REFERENCES markets(id),
token_id TEXT NOT NULL,
quantity NUMERIC(20, 10) NOT NULL,
average_cost NUMERIC(20, 10),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (market_id, token_id)
);
A few implementation details are worth mentioning.
Why NUMERIC instead of FLOAT?
Financial calculations require predictable decimal precision.
Floating-point representation can introduce rounding differences that become problematic when calculating quantities, balances, or execution prices.
Why TIMESTAMPTZ?
Trading events arrive from multiple systems.
Consistent timestamps, typically stored in UTC, make debugging and reconciliation much easier.
Why unique exchange IDs?
Because events can arrive more than once.
Unique identifiers help prevent duplicate database records.
These schemas are educational starting points. A production implementation also needs indexes, validation constraints, migrations, and proper concurrency handling.
5. Recording Orders in Python
Let's represent an order using a Python dataclass.
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class OrderRecord:
exchange_order_id: str
market: str
token_id: str
side: str
price: Decimal
quantity: Decimal
filled_quantity: Decimal
status: str
We can create an order record like this:
order = OrderRecord(
exchange_order_id="exchange-order-123",
market="BTC-5M",
token_id="yes-token-id",
side="BUY",
price=Decimal("0.48"),
quantity=Decimal("20"),
filled_quantity=Decimal("0"),
status="OPEN",
)
This is our internal data model, not a Polymarket SDK order object.
The next step would be persisting it through a repository or database access layer.
But there's an interesting edge case.
What if the exchange accepts an order, but the response never arrives?
Consider this sequence:
- The bot submits an order.
- Polymarket accepts it.
- The network connection breaks.
- The bot never receives confirmation.
Did the order succeed?
We don't know.
Automatically submitting the same order again could create duplicate exposure.
This is an uncertain order state.
The bot should investigate the exchange's current state before retrying.
That principle becomes especially important during crash recovery.
6. Structured Logging: Know What Your Bot Is Doing
A database preserves trading records.
Logs explain the events surrounding those records.
This isn't particularly useful:
Order submitted
Order filled
Error
Bot stopped
Which order?
Which market?
What price?
What caused the error?
We need structured logs.
For example:
{
"timestamp": "2026-11-13T10:30:00Z",
"level": "INFO",
"event": "order_submitted",
"market": "BTC-5M",
"order_id": "exchange-order-123",
"side": "BUY",
"price": "0.48",
"quantity": "20"
}
Now we can search and filter logs by market, order ID, event type, or timestamp.
A simple Python logger
import logging
import json
logger = logging.getLogger("polymarket_bot")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
logger.addHandler(handler)
def log_event(event, **details):
record = {
"event": event,
**details,
}
logger.info(json.dumps(record))
log_event(
"order_submitted",
market="BTC-5M",
order_id="exchange-order-123",
side="BUY",
price="0.48",
quantity="20",
)
This is a minimal example.
For production, add consistent timestamps, exception tracebacks, session IDs, log rotation, retention, and centralized log collection.
And one critical rule: never write private keys, API secrets, or authentication tokens into logs.
7. Monitoring Bot Health
A running process isn't necessarily a healthy trading bot.
Imagine your WebSocket connection stops delivering new prices.
Python is still running.
Your CPU usage looks normal.
No exception has been thrown.
But the bot may be evaluating opportunities using stale market data.
That's why health monitoring matters.
A useful monitoring system should track four areas.
| Area | Important Metrics |
|---|---|
| Market data | WebSocket status, data age, synchronization |
| Execution | Submission latency, rejections, unknown orders |
| Portfolio | Inventory, reserved capital, exposure |
| Infrastructure | CPU, memory, database, disk, uptime |
A simplified status dashboard could look like this:
POLYMARKET BOT STATUS
----------------------------
Process: RUNNING
Database: CONNECTED
Market Data: HEALTHY
Execution: HEALTHY
Portfolio: RECONCILED
BTC: ACTIVE
ETH: ACTIVE
SOL: PAUSED
XRP: ACTIVE
Open Orders: 8
Rejections: 2
Stale Markets: 0
Risk Status: WITHIN LIMITS
These are illustrative values.
A real implementation should calculate each status from measurable conditions.
For example, market-data health could depend on the age of the last valid update rather than whether the WebSocket object still exists.
8. Crash Recovery: The Most Important Workflow
Now let's return to our original problem.
The VPS crashes while the bot has active orders.
What should happen after restart?
The answer is not simply "start trading again."
The correct sequence is:
Bot Running
|
v
Process Crashes
|
v
VPS Restarts
|
v
Python Process Starts
|
v
Load PostgreSQL Records
|
v
Connect to Polymarket
|
v
Fetch Current Exchange State
|
v
Compare Local and Exchange Data
|
v
Rebuild Portfolio State
|
v
Run Risk Checks
|
v
Resume Trading If Safe
Let's break this down.
Step 1: Load local records
Read the latest stored orders, fills, positions, balances, market configuration, and bot session information.
Step 2: Reconnect to Polymarket
Reestablish the required exchange and market-data connections.
Step 3: Fetch the actual account state
Retrieve open orders, recent executions, token balances, and available collateral using the currently supported exchange APIs.
Step 4: Compare both states
Suppose PostgreSQL reports:
YES inventory: 50
Open orders: 3
But Polymarket reports:
YES inventory: 60
Open orders: 1
We need to determine what changed.
Perhaps an order filled while the bot was offline.
Perhaps a cancellation completed.
Perhaps a database write failed before the crash.
Step 5: Reconstruct the portfolio
Update the local records and recalculate inventory, balances, open orders, and portfolio exposure.
Step 6: Validate before resuming
If discrepancies remain unresolved, keep trading paused.
Missing a trading opportunity is better than placing orders using an incorrect portfolio state.
9. Implementing a Recovery Engine in Python
Here's a simplified recovery class.
The exchange methods below are placeholders, not actual Polymarket SDK calls.
class TradingBotRecovery:
def __init__(self, database, exchange):
self.database = database
self.exchange = exchange
async def recover(self):
print("Starting recovery")
# Load persisted trading state
local_state = await self.database.load_state()
# Reconnect to exchange
await self.exchange.connect()
# Fetch current account information
exchange_state = (
await self.exchange.fetch_account_state()
)
# Compare both states
differences = self.compare_states(
local_state,
exchange_state,
)
# Reconcile discrepancies
if differences:
await self.reconcile(
differences,
exchange_state,
)
# Validate the recovered portfolio
if not self.is_state_safe(exchange_state):
print("Recovery failed: trading paused")
return False
print("Recovery completed")
return True
def compare_states(self, local, exchange):
# Compare orders, fills, and balances
return []
async def reconcile(self, differences, exchange):
# Update persistent records safely
pass
def is_state_safe(self, state):
# Validate reconciled portfolio state
return True
Notice that the methods are intentionally incomplete.
The class demonstrates the recovery sequence, not a finished implementation.
In production, the recovery engine must also handle pagination, partial fills, uncertain submissions, missed events, database failures, and unresolved discrepancies.
The final safety check must validate the reconstructed portfolio, not merely confirm that the exchange connection works.
10. Preventing Duplicate Trades With Idempotency
Here's another failure scenario.
A fill event arrives:
YES inventory: 50 → 60
The bot processes it successfully.
Then the WebSocket reconnects, and the same event arrives again.
Without duplicate protection:
YES inventory: 60 → 70
The bot now believes it owns more tokens than it actually does.
This is where idempotency comes in.
Processing the same event twice must not apply the same financial change twice.
One approach is maintaining a table of processed events.
CREATE TABLE processed_events (
id BIGSERIAL PRIMARY KEY,
exchange_event_id TEXT NOT NULL UNIQUE,
event_type TEXT NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Before applying a fill, the bot checks whether that event has already been processed.
Conceptually:
def process_fill(database, fill):
with database.transaction():
if database.event_exists(fill.event_id):
return
database.insert_fill(fill)
database.update_position(
token_id=fill.token_id,
quantity=fill.quantity,
)
database.mark_event_processed(
fill.event_id
)
This is pseudocode.
A production implementation needs atomic duplicate handling, correct buy/sell accounting, fees, partial fills, and concurrency controls.
The important idea is that the fill record, position update, and processed-event marker belong in the same database transaction.
11. Why Database Transactions Matter
Imagine processing a fill requires four operations:
- Insert the fill record.
- Update the order's filled quantity.
- Update the position.
- Recalculate reserved capital.
What happens if the application crashes after step two?
The order record may be updated, but the position could remain unchanged.
That's an inconsistent state.
Database transactions help prevent partial updates.
BEGIN;
INSERT INTO fills (
exchange_fill_id,
exchange_order_id,
token_id,
price,
quantity,
executed_at
)
VALUES (
'fill-123',
'order-456',
'yes-token-id',
0.48,
10,
NOW()
);
UPDATE orders
SET filled_quantity = filled_quantity + 10,
updated_at = NOW()
WHERE exchange_order_id = 'order-456';
UPDATE positions
SET quantity = quantity + 10,
updated_at = NOW()
WHERE token_id = 'yes-token-id';
COMMIT;
This is an illustrative SQL sequence, not a complete production transaction.
Real implementations must check whether the fill already exists, account for buy and sell direction, handle missing position rows, and protect against concurrent updates.
But the principle is straightforward.
Related financial changes should either succeed together or fail together.
12. Automatically Restarting the Bot
We also need a way to restart the Python process after a failure.
On a Linux VPS, common options include systemd, Supervisor, and Docker restart policies.
Here's a basic systemd service:
[Unit]
Description=Polymarket Market Maker Bot
After=network.target
[Service]
Type=simple
User=tradingbot
WorkingDirectory=/opt/polymarket-bot
ExecStart=/opt/polymarket-bot/venv/bin/python main.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
This configuration restarts the Python application after certain failures.
However, there's an important distinction:
Restarting a process is not the same as recovering a trading system.
Systemd can restart Python.
It cannot automatically determine whether your exchange orders, balances, and inventory are correct.
That responsibility belongs to your application.
The bot must complete recovery and reconciliation before enabling new trading activity.
13. Protecting Credentials and Trading Data
A production trading bot may use private keys, API credentials, database passwords, and signing configuration.
Never hardcode these values into source files or commit them to GitHub.
For development, protected environment variables are a reasonable starting point.
import os
database_url = os.getenv("DATABASE_URL")
api_key = os.getenv("POLYMARKET_API_KEY")
For production, use appropriate secret management and restrict access to the VPS and database.
You also need a backup strategy.
A basic PostgreSQL logical backup looks like this:
pg_dump -Fc trading_database > trading_backup.dump
Restoring it:
pg_restore -d trading_database trading_backup.dump
Backups should be stored securely, preferably outside the trading server, and tested regularly.
A backup that has never been successfully restored isn't a proven recovery plan.
14. Test Failures Before Trading With Real Capital
A production system should be tested under failure conditions, not only when everything works.
Here are some scenarios worth simulating:
| Failure | Expected Behavior |
|---|---|
| Python process crashes | Supervisor restarts it |
| VPS restarts | Recovery begins before trading |
| Database unavailable | New trading remains paused |
| WebSocket disconnects | Reconnect and rebuild market state |
| Exchange request times out | Reconcile uncertain order state |
| Duplicate fill arrives | Do not update inventory twice |
| Exchange and database disagree | Investigate and reconcile |
| Credentials become invalid | Stop execution and alert |
| Recovery cannot complete | Keep trading disabled |
One useful test is to simulate a partially filled order, terminate the process unexpectedly, restart the application, and verify that inventory is reconstructed correctly.
Then repeat with cancellations, duplicate fills, and missing exchange responses.
The objective isn't merely to prove that Python restarts.
It's to prove that the bot can recover without losing track of its actual trading state.
15. Putting Everything Together
At this point, our Polymarket market maker has grown beyond a simple strategy script.
The overall architecture now includes:
Polymarket Exchange
|
+----------+----------+
| |
v v
Market Data Execution API
| |
v v
Market State Order Manager
| |
+----------+----------+
|
v
Strategy Engine
|
v
Capital Allocator
|
v
Portfolio Risk Manager
|
v
Order Approval
|
v
Order Manager
|
+----------+----------+
| |
v v
PostgreSQL Logs & Metrics
| |
v v
Recovery Engine Monitoring
|
v
State Reconciliation
|
v
Safe Trading Resume
Each component has a clear responsibility.
The strategy engine calculates potential orders.
The risk manager decides whether they are acceptable.
The order manager handles exchange execution.
PostgreSQL preserves trading records.
Monitoring helps detect operational problems.
And the recovery engine ensures the application can rebuild a trustworthy state after failures.
This separation makes the system easier to debug, test, and extend.
Final Thoughts
One of the biggest differences between a trading script and a production trading system is how they behave when something goes wrong.
A script might work perfectly until the first unexpected restart.
A reliable trading system is designed around the possibility of failure.
In Day 14, we explored how to:
- Persist orders, fills, positions, and balances in PostgreSQL
- Use structured logging to investigate trading events
- Monitor market data and execution health
- Rebuild state after a process crash
- Reconcile internal records with Polymarket
- Prevent duplicate fill processing
- Use transactions to protect financial consistency
- Automatically restart the application without blindly resuming trading
The biggest takeaway?
A trading bot must know what happened, understand what it currently owns, and verify that information before making its next trading decision.
That's the foundation of reliable trading infrastructure.
Coming Next: Day 15
Polymarket Market Maker Bot: Performance Analytics, P&L Tracking, and Strategy Evaluation
Now that we've covered persistent state and recovery, the next challenge is measuring whether the strategy is actually performing well.
We'll explore realized and unrealized P&L, execution quality, trading fees, portfolio performance, and the metrics needed to evaluate a market-making strategy.
Follow the Project
Building a Polymarket trading bot with Python? Follow the series as we work through the architecture, strategy, risk management, and infrastructure.
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
If you've built automated trading systems before, what was the hardest production issue you encountered: exchange reconciliation, duplicate orders, or unexpected downtime?
Share your experience in the comments.


Top comments (0)