DEV Community

Cover image for How to Build a Trade-Rejection Layer for a Polymarket Bot in Python
Dexoryn
Dexoryn

Posted on

How to Build a Trade-Rejection Layer for a Polymarket Bot in Python

A Polymarket trading bot needs more than a strategy that identifies potential opportunities. It needs a reliable way to reject trades that fail its execution or risk requirements.

Without this layer, a bot may submit orders using stale market data, accept an unattractive price, exceed its exposure limits, or act on a signal that no longer makes sense.

In this tutorial, we'll build a small Python trade-validation layer that evaluates a proposed trade before it reaches the execution component.

The implementation is deliberately independent of any specific Polymarket SDK. That makes it easier to test the decision logic without connecting a wallet, signing orders, or risking real funds.

  1. Separate signal generation from trade validation

A common architectural mistake is allowing strategy logic to submit orders directly.

A cleaner design separates the process into distinct stages:

  1. Signal generation: Identifies a possible trade.
  2. Market validation: Checks data quality and market eligibility.
  3. Trade validation: Evaluates expected edge, execution price, and risk limits.
  4. Execution: Submits an approved order through the appropriate trading client.
  5. Monitoring: Tracks order status, fills, errors, and resulting exposure.

The strategy should not be able to bypass the validation layer simply because it produces a strong signal.

This separation also makes the system easier to test. You can evaluate whether a trade should be accepted without making an API request or interacting with a wallet.

Polymarket provides APIs for market data and trading. Before integrating the execution stage, consult the "official API overview" (https://help.polymarket.com/en/articles/13364254-does-polymarket-have-an-api) and the current developer documentation for the specific endpoints and SDK version you use.

  1. Define a trade proposal

Start by representing a proposed trade as structured data.

from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class TradeProposal:
token_id: str
side: str
estimated_probability: Decimal
execution_price: Decimal
expected_cost_per_share: Decimal
data_age_seconds: Decimal
proposed_exposure: Decimal
current_exposure: Decimal

Each field serves a specific purpose.

  • "token_id" identifies the outcome token.
  • "side" identifies whether the proposal is to buy or sell.
  • "estimated_probability" represents the strategy's estimated probability for the relevant outcome.
  • "execution_price" is the estimated executable price, not necessarily the last traded price.
  • "expected_cost_per_share" represents estimated costs expressed per share.
  • "data_age_seconds" measures the age of the market data used for the decision.
  • "proposed_exposure" and "current_exposure" represent the relevant risk allocation.

This is a simplified data model. A production system should define exposure units precisely, distinguish order notional from maximum loss where appropriate, and validate the token and market identifiers against trusted market metadata.

It should also distinguish between buy and sell calculations. The example below focuses on buying an outcome token.

  1. Create explicit rejection reasons

Returning only "True" or "False" tells you whether a trade was rejected, but not why.

Use a structured result instead.

from dataclasses import dataclass

@dataclass(frozen=True)
class ValidationResult:
approved: bool
reason: str

The "reason" field makes decisions observable. It can later be recorded in logs, test reports, and performance analysis.

For example, a bot might reject a proposal because its data is stale, its estimated edge is too small, or its exposure limit would be exceeded.

  1. Implement the validation function

The following implementation uses explicit thresholds and returns the first rejection reason encountered.

from decimal import Decimal

def validate_trade(
trade: TradeProposal,
*,
minimum_edge: Decimal,
maximum_data_age: Decimal,
maximum_exposure: Decimal,
maximum_price: Decimal,
) -> ValidationResult:

if not trade.token_id:
    return ValidationResult(False, "MISSING_TOKEN_ID")

if trade.side != "BUY":
    return ValidationResult(False, "UNSUPPORTED_SIDE")

if not (
    Decimal("0") < trade.estimated_probability
    < Decimal("1")
):
    return ValidationResult(False, "INVALID_PROBABILITY")

if not (
    Decimal("0") < trade.execution_price
    < Decimal("1")
):
    return ValidationResult(False, "INVALID_PRICE")

if trade.data_age_seconds < Decimal("0"):
    return ValidationResult(False, "INVALID_DATA_AGE")

if trade.data_age_seconds > maximum_data_age:
    return ValidationResult(False, "STALE_DATA")

if trade.execution_price > maximum_price:
    return ValidationResult(False, "PRICE_LIMIT_EXCEEDED")

edge = (
    trade.estimated_probability
    - trade.execution_price
    - trade.expected_cost_per_share
)

if edge < minimum_edge:
    return ValidationResult(False, "EDGE_TOO_LOW")

resulting_exposure = (
    trade.current_exposure
    + trade.proposed_exposure
)

if resulting_exposure > maximum_exposure:
    return ValidationResult(False, "EXPOSURE_LIMIT")

if trade.proposed_exposure <= Decimal("0"):
    return ValidationResult(False, "INVALID_EXPOSURE")

return ValidationResult(True, "APPROVED")
Enter fullscreen mode Exit fullscreen mode

The edge calculation is intentionally simplified.

For a binary outcome token purchased at price (p), a probability estimate (q) gives a basic expected value per share of approximately (q-p), assuming a $1 payout if the outcome wins and $0 otherwise.

The code subtracts an estimated per-share cost as well. Real calculations must account for the applicable fee schedule, price constraints, execution uncertainty, and the precise strategy being evaluated. Selling positions requires different logic.

Also, the maximum price and minimum edge are separate controls. A trade can satisfy one and fail the other.

The thresholds should come from a documented strategy configuration and testing process, not arbitrary values chosen to make the bot trade more often.

  1. Test both accepted and rejected trades

The validation layer should be tested before it is connected to a live execution client.

Here is a basic pytest example.

from decimal import Decimal

def make_trade(**overrides):
values = {
"token_id": "example-token",
"side": "BUY",
"estimated_probability": Decimal("0.60"),
"execution_price": Decimal("0.50"),
"expected_cost_per_share": Decimal("0.01"),
"data_age_seconds": Decimal("1"),
"proposed_exposure": Decimal("20"),
"current_exposure": Decimal("30"),
}

values.update(overrides)
return TradeProposal(**values)
Enter fullscreen mode Exit fullscreen mode

def test_approves_valid_trade():
result = validate_trade(
make_trade(),
minimum_edge=Decimal("0.05"),
maximum_data_age=Decimal("5"),
maximum_exposure=Decimal("100"),
maximum_price=Decimal("0.55"),
)

assert result.approved is True
assert result.reason == "APPROVED"
Enter fullscreen mode Exit fullscreen mode

def test_rejects_stale_data():
result = validate_trade(
make_trade(data_age_seconds=Decimal("10")),
minimum_edge=Decimal("0.05"),
maximum_data_age=Decimal("5"),
maximum_exposure=Decimal("100"),
maximum_price=Decimal("0.55"),
)

assert result.approved is False
assert result.reason == "STALE_DATA"
Enter fullscreen mode Exit fullscreen mode

def test_rejects_excessive_exposure():
result = validate_trade(
make_trade(
proposed_exposure=Decimal("80"),
current_exposure=Decimal("30"),
),
minimum_edge=Decimal("0.05"),
maximum_data_age=Decimal("5"),
maximum_exposure=Decimal("100"),
maximum_price=Decimal("0.55"),
)

assert result.approved is False
assert result.reason == "EXPOSURE_LIMIT"
Enter fullscreen mode Exit fullscreen mode

Save the implementation and tests in separate Python files, import the relevant classes and functions, and run:

pytest -q

The expected behavior is straightforward: the valid proposal is approved, the stale proposal is rejected, and the proposal that exceeds the exposure limit is rejected.

These are unit tests, not evidence that a live trading strategy is profitable or that an execution integration works correctly.

  1. Add failure scenarios before integrating execution

The first set of tests covers only a few conditions. A more complete suite should test:

  • Missing or invalid token identifiers.
  • Probabilities outside the permitted range.
  • Invalid prices and negative data ages.
  • Prices above the strategy's maximum.
  • Expected edge exactly at the minimum threshold.
  • Exposure exactly at the limit.
  • Non-positive proposed exposure.
  • Unexpected API responses and missing order-book fields.
  • Market data becoming stale between validation and submission.
  • Duplicate signals and uncertain order-submission outcomes.

One particularly important case is a price change between validation and execution.

Suppose a proposal passes validation at an estimated price of $0.50, but the available price moves to $0.57 before the order is submitted. The original approval should not automatically authorize the more expensive trade.

The execution component should enforce an appropriate price limit, and the system should revalidate when necessary. The exact mechanism depends on the supported order types and current API behavior.

Likewise, if an order request times out, do not immediately assume the order failed and submit another one. The first request may have reached the service even though the response was lost. Reconcile the order state before retrying.

  1. Record rejected trades for later analysis

A validation layer becomes more useful when its decisions can be examined.

A rejection record might contain:

rejection_record = {
"token_id": "example-token",
"reason": "EDGE_TOO_LOW",
"estimated_probability": "0.60",
"execution_price": "0.56",
"data_age_seconds": "1",
}

In a real application, add a timestamp, strategy version, market identifier, relevant threshold values, and a unique proposal identifier.

Avoid storing private keys, API secrets, or other sensitive credentials in logs.

Over time, analyze rejection counts by reason. If most signals fail because of execution price, the strategy may be generating signals too slowly or relying on unrealistic price assumptions. If stale data causes repeated rejections, the market-data pipeline may need attention.

Do not judge rejected trades solely by whether the market later moved in the predicted direction. A hypothetical trade is not the same as a confirmed fill at the expected price.

  1. What this example does not cover

This implementation is a starting point, not a complete production risk engine.

It does not retrieve live Polymarket order books, calculate market-specific fees, estimate fills across multiple price levels, manage open orders, track partial fills, or maintain portfolio exposure from confirmed account state.

Those responsibilities should be implemented and tested in separate components.

For a production system, the validation layer should consume trusted, current market data and a consistent view of account exposure. The execution layer should enforce order constraints, reconcile uncertain outcomes, and record order-state transitions.

The architecture matters as much as the individual checks. A perfect validation function cannot protect a bot if another code path can submit orders without using it.

Conclusion

A trade-rejection layer turns risk rules into explicit, testable software behavior.

By validating data freshness, executable price, estimated edge, and exposure before order submission, developers can make a trading bot easier to inspect and maintain. Structured rejection reasons also make it easier to identify weaknesses in the strategy and infrastructure.

At Dexoryn Labs, we work on Polymarket automation through existing bot projects and custom development. Explore our "open-source Polymarket projects on GitHub" (https://github.com/dexorynlabs) or visit "dexoryn.xyz" (https://dexoryn.xyz/) if you are evaluating a bot or planning a custom trading workflow.

The goal is not to approve every signal. It is to make sure every approved trade has passed the rules the system was designed to enforce.

Top comments (0)