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.

Polymarket Trading Bot

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:

  1. The bot submits an order.
  2. Polymarket accepts it.
  3. The network connection breaks.
  4. 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:

  1. Insert the fill record.
  2. Update the order's filled quantity.
  3. Update the position.
  4. 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:

GitHub logo 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.

Source:https://dev.to/benjamin_cup/building-a-production-ready-polymarket-trading-bot-postgresql-logging-crash-recovery-9hf

Related posts