dexorynlabs

Activity

Stars and mentions · Last 30 days, Oct 7 – Nov 5

GitHub stars0
Mentions5
01234Oct 7Oct 21Nov 5
UTC days · repositories dexorynlabs owns0 repositories

Top repositories

dexorynlabs’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside dexorynlabs’s account where dexorynlabs opened or reviewed pull requests in the past 365 days.

Who noticed dexorynlabs

Everyone who starred or forked dexorynlabs’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

5posts
Nov 60 posts
  1. DEV · · @dexoryn

    How to Calculate Expected Value for a Polymarket Trading Bot in Python A Polymarket trading bot can identify a promising outcome and still make a poor entry. The problem is that predicting what will happen and determining whether a trade is worth taking are two different tasks. A bot might estimate that an outcome has a 65% chance of winning. But if the price is already $0.63, the estimated advantage is small before accounting for trading costs and uncertainty. This is where expected value becomes useful. In this tutorial, we will build a small Python component that calculates the estimated expected value of a Polymarket outcome token, applies a minimum threshold, and rejects trades that do not meet the strategy's requirements. The example uses hypothetical inputs. It does not connect to a live market, submit orders, or guarantee profitable results. 1. Understand the expected value calculation For a binary outcome token held until resolution, a winning share pays $1 and a losing share pays $0. Let: - (q) be your estimated probability that the outcome wins. - (p) be the purchase price per share. The simplified expected profit per share before costs is: [ EV=q(1-p)+(1-q)(-p) ] This simplifies to: [ EV=q-p ] Suppose your model estimates a 65% probability of winning. If the token costs $0.45: [ EV=0.65-0.45=$0.20 ] If the token costs $0.62: [ EV=0.65-0.62=$0.03 ] The estimated expected profit falls from $0.20 to $0.03 per share before fees and other costs. This calculation assumes the probability estimate is accurate enough to be useful and that the position is held until resolution. Selling before resolution requires a different model that accounts for the exit price. Expected value is an estimate, not a guaranteed return. 2. Implement the calculation in Python Use "Decimal" for financial values when you want explicit decimal arithmetic rather than binary floating-point calculations. from decimal import Decimal def expected_value_per_share( probability: Decimal, price: Decimal, cost_per_share: Decimal = Decimal("0"), ) -> Decimal: if not Decimal("0") <= probability <= Decimal("1"): raise ValueError("Probability must be between 0 and 1") if not Decimal("0") < price < Decimal("1"): raise ValueError("Price must be between 0 and 1") if cost_per_share < Decimal("0"): raise ValueError("Cost cannot be negative") return probability - price - cost_per_share probability = Decimal("0.65") price = Decimal("0.45") cost = Decimal("0.01") ev = expected_value_per_share( probability=probability, price=price, cost_per_share=cost, ) print(f"Estimated EV per share: ${ev}") Expected output: Estimated EV per share: $0.19 The example subtracts an assumed cost of $0.01 per share. This is only a placeholder. Actual costs depend on the applicable market fee schedule and execution conditions. The function deliberately keeps probability estimation separate from expected-value calculation. That makes it easier to replace the hypothetical probability with an estimate from a model, research process, or calibrated forecasting system. 3. Reject trades with insufficient expected value Calculating expected value is only useful if the bot uses the result to make a decision. A strategy can define a minimum expected-value threshold. If a proposed trade falls below it, the bot rejects the trade rather than submitting an order. from decimal import Decimal def evaluate_trade( probability: Decimal, price: Decimal, cost_per_share: Decimal, minimum_ev: Decimal, ) -> dict: ev = expected_value_per_share( probability=probability, price=price, cost_per_share=cost_per_share, ) if ev < minimum_ev: return { "approved": False, "reason": "EXPECTED_VALUE_TOO_LOW", "expected_value": str(ev), } return { "approved": True, "reason": "APPROVED", "expected_value": str(ev), } result = evaluate_trade( probability=Decimal("0.65"), price=Decimal("0.62"), cost_per_share=Decimal("0.01"), minimum_ev=Decimal("0.05"), ) print(result) The result is: { "approved": False, "reason": "EXPECTED_VALUE_TOO_LOW", "expected_value": "0.02" } The trade is rejected because its estimated expected value is $0.02 per share, below the configured minimum of $0.05. The threshold is an example, not a universally correct value. It should be selected and evaluated based on the strategy's assumptions, uncertainty, execution costs, and risk requirements. A positive expected value alone does not mean a trade is worth taking. 4. Use the executable price, not an assumed price One of the most important implementation details is choosing the correct price input. Polymarket provides market data through its APIs. The CLOB API exposes order-book information and price-related endpoints, while the Gamma API provides market metadata and discovery information. The "Polymarket Institute's data guide" (institute.polymarket.com) includes examples of retrieving CLOB prices and historical prices. For a potential buy order, the bot should evaluate the price available to buyers, not blindly use a last-traded price or a midpoint. For larger orders, even the best ask may not represent the average price of the entire order. The bot may need to inspect available ask levels and estimate the cost of filling the intended quantity. Consider this simplified order book: Ask price| Available shares $0.50| 20 $0.52| 30 $0.55| 50 A purchase of 20 shares might be available at $0.50. A purchase of 50 shares would require buying across at least two price levels if those quantities remain available. The estimated average entry price therefore depends on the size of the order. For a live implementation, the process should be: 1. Retrieve the relevant outcome token's current order book. 2. Validate that the response is complete and sufficiently fresh. 3. Estimate the average execution price for the intended quantity. 4. Calculate expected value using that estimate and applicable costs. 5. Reject the trade if the resulting expected value falls below the strategy's threshold. Market data can change between retrieval and order submission. The execution layer should enforce suitable price limits and handle cases where the market moves before the order is filled. 5. Add unit tests Expected-value logic is small enough that it should be tested independently before being integrated into a trading bot. Using "pytest", create a file called "test_expected_value.py": from decimal import Decimal import pytest from trading_logic import expected_value_per_share def test_positive_expected_value(): result = expected_value_per_share( Decimal("0.65"), Decimal("0.45"), Decimal("0.01"), ) assert result == Decimal("0.19") def test_zero_expected_value(): result = expected_value_per_share( Decimal("0.50"), Decimal("0.50"), ) assert result == Decimal("0.00") def test_invalid_probability(): with pytest.raises(ValueError): expected_value_per_share( Decimal("1.10"), Decimal("0.50"), ) def test_invalid_price(): with pytest.raises(ValueError): expected_value_per_share( Decimal("0.60"), Decimal("1.00"), ) Place the calculation function in "trading_logic.py", then run: pip install pytest pytest -q These tests verify the arithmetic and basic input validation. They do not establish that a probability model is accurate or that a live trading system will execute orders as expected. 6. What this calculation does not tell you Expected value is useful, but it is only one part of a trading decision. First, the probability estimate may be wrong. A model that consistently overestimates an outcome's chances can report positive expected value while making poor decisions. Second, costs may be more complicated than a fixed amount per share. The actual implementation must account for the applicable fee schedule and the economics of the order. Third, expected value does not measure the uncertainty of the estimate. Two opportunities can have the same calculated expected value but very different levels of model uncertainty. Fourth, it does not account for portfolio concentration. Several individually attractive positions may depend on the same underlying event and create excessive combined exposure. A production trading bot should combine expected-value evaluation with data-quality checks, executable-price validation, position limits, order-state management, and monitoring. It should also record rejected trades and the reasons for rejection. This makes it possible to assess whether the rules are too permissive, too restrictive, or based on assumptions that do not hold in live conditions. Conclusion Expected value gives a Polymarket trading bot a clear mathematical basis for evaluating a proposed entry. Implementing it as an independent Python component makes the logic easier to test, audit, and improve. The calculation is straightforward. The difficult work lies in producing reliable probability estimates, using realistic execution prices, accounting for costs, and enforcing the result consistently throughout the trading system. At Dexoryn Labs, we work on Polymarket trading automation, including existing bot projects and custom development. You can explore our "open-source projects on GitHub" (github.com/dexorynlabs) or visit "dexoryn.xyz" (dexoryn.xyz) to learn more. Build the decision logic first, test its assumptions, and only then consider connecting it to live execution.

    4 reactions
  2. DEV · · @dexoryn

    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" (help.polymarket.com) and the current developer documentation for the specific endpoints and SDK version you use. 2. 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. 3. 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. 4. 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") 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. 5. 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) 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" 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" 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" 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. 6. 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. 7. 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. 8. 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" (github.com/dexorynlabs) or visit "dexoryn.xyz" (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.

    3 reactions
  3. How to Test a Polymarket Trading Bot in Python Before Going Live A Polymarket trading bot can run without crashing and still make serious mistakes. It might submit duplicate orders, calculate the wrong position size, trade using stale prices, or assume an order was filled when it was not. Testing only whether the script starts successfully is not enough. A reliable trading bot needs tests for its decision logic, risk controls, execution behaviour, and recovery from failures. You can test most of these components without placing real orders or risking funds. This tutorial shows how to structure a basic Python testing workflow using a simple trading decision, a risk check, a simulated execution layer, and automated tests. 1. Separate trading logic from order execution One of the most useful design decisions is to separate the component that decides what to do from the component that communicates with the trading API. A simple architecture looks like this: - Market data: Supplies prices, order-book information, and market identifiers. - Strategy: Evaluates whether a trading signal meets the strategy's conditions. - Risk manager: Checks position limits, available capital, and permitted prices. - Execution layer: Submits, tracks, and reconciles orders. - Test suite: Verifies the behaviour of each component independently. This separation makes testing easier because you can evaluate the strategy without connecting a wallet or sending an order. It also helps identify which component failed when a test does not behave as expected. 2. Write a small, testable risk-control function Start with a function that decides whether a proposed order is allowed. For this example, the bot accepts an order only when its price, size, and total cost meet predefined limits. These are illustrative rules, not a complete trading strategy. risk.py from decimal import Decimal def validate_order( price: Decimal, shares: Decimal, available_cash: Decimal, max_order_cost: Decimal, ) -> bool: if price <= Decimal("0") or price >= Decimal("1"): return False if shares <= Decimal("0"): return False order_cost = price * shares if order_cost > available_cash: return False if order_cost > max_order_cost: return False return True The price restriction reflects the conventional probability-token range used in this simplified example. A production implementation must also validate the actual market's supported price increments, order-size constraints, applicable fees, and current API requirements. Notice that the function does not place an order. It only evaluates whether the proposed order satisfies these particular checks. That makes it deterministic and straightforward to test. 3. Add automated unit tests with pytest Install the testing library: pip install pytest Create a file named "test_risk.py": from decimal import Decimal from risk import validate_order def test_accepts_order_within_limits(): assert validate_order( price=Decimal("0.40"), shares=Decimal("10"), available_cash=Decimal("20"), max_order_cost=Decimal("10"), ) def test_rejects_order_exceeding_available_cash(): assert not validate_order( price=Decimal("0.50"), shares=Decimal("50"), available_cash=Decimal("20"), max_order_cost=Decimal("100"), ) def test_rejects_order_exceeding_order_limit(): assert not validate_order( price=Decimal("0.50"), shares=Decimal("30"), available_cash=Decimal("100"), max_order_cost=Decimal("10"), ) def test_rejects_zero_shares(): assert not validate_order( price=Decimal("0.40"), shares=Decimal("0"), available_cash=Decimal("100"), max_order_cost=Decimal("10"), ) def test_rejects_invalid_price(): assert not validate_order( price=Decimal("1.00"), shares=Decimal("10"), available_cash=Decimal("100"), max_order_cost=Decimal("10"), ) Run the tests: python -m pytest -v These tests check normal behaviour and several invalid inputs. They do not prove that the entire bot is safe, but they establish that the specific risk rules behave as intended. Add tests whenever a new rule is introduced. If you change how order size is calculated, for example, test both the expected allocation and the conditions that should cause the order to be rejected. 4. Simulate execution instead of sending real orders Unit tests cannot establish how the bot behaves when an execution request times out, an order is partially filled, or the market price changes. For those scenarios, use a simulated execution layer. A minimal interface might look like this: class SimulatedBroker: def init(self): self.orders = [] def submit(self, token_id, side, price, shares): order = { "token_id": token_id, "side": side, "price": price, "shares": shares, "status": "SIMULATED", } self.orders.append(order) return order Your strategy can call this interface in tests instead of the real exchange client. The simulator records the order request but does not sign, submit, or settle a live trade. This lets you inspect what the strategy intended to do. However, a simulated broker is not a realistic market simulator by itself. It does not model order-book depth, queue position, latency, fees, partial fills, or adverse price movement. Those behaviours must be added explicitly if you want the simulation to represent them. 5. Test failures, not just successful trades Many important bugs appear when something unexpected happens. A useful test suite should include the following scenarios. Insufficient balance: The strategy detects an opportunity but does not submit an order that exceeds available capital. Stale market data: The bot refuses to use a price snapshot older than its configured freshness limit. Duplicate signal: Processing the same source event twice does not create two independent orders. Partial fill: The system records the filled quantity separately from the remaining quantity. Rejected order: The bot records the rejection and does not mark the position as successfully opened. Network timeout: The bot treats the result as unknown until it can reconcile the order state, rather than blindly submitting a duplicate. Restart recovery: The bot reloads persistent state and reconciles open orders and positions before resuming execution. These tests should verify state transitions as well as return values. For example, after a timeout, the order should enter an unresolved state and trigger reconciliation. It should not automatically become a failed order simply because the response was missing. 6. Test against historical data carefully Historical data can help evaluate whether a strategy's rules would have generated signals under past market conditions. But a historical simulation is only as realistic as its assumptions. A test that assumes every order fills at the price recorded in a historical dataset may ignore spread, order-book depth, execution delays, and the fact that the desired quantity might not have been available. For a more useful evaluation, document: - Which price or order-book data the simulation uses. - How orders are assumed to fill. - How fees and execution costs are handled. - What happens when data is missing. - Whether the strategy uses only information that would have been available at the decision time. Avoid using future information to make past trading decisions. That creates look-ahead bias and can make an ineffective strategy appear successful. Keep historical strategy evaluation separate from software correctness testing. A strategy can pass every unit test and still have no profitable trading edge. 7. Use a staged testing process Before connecting a bot to live execution, progress through distinct stages. Stage 1: Unit testing Test calculations, validation rules, signal processing, and state transitions using controlled inputs. Stage 2: Integration testing Test how the data, strategy, risk, persistence, and execution components work together. Mock external services where appropriate. Stage 3: Historical simulation Evaluate the strategy against historical data using explicit assumptions about fills, fees, and data availability. Stage 4: Dry-run monitoring Connect to the relevant market-data feeds and record what the bot would do without submitting real orders. Compare intended decisions with current market conditions. Stage 5: Controlled live testing Only after the preceding stages have been reviewed should live execution be considered. Use a dedicated wallet with limited funds, conservative exposure limits, protected credentials, and a tested emergency stop. Passing one stage does not automatically guarantee success at the next. In particular, dry-run execution cannot fully reproduce real order matching or settlement. 8. Define what passing actually means A test suite is more useful when its acceptance criteria are explicit. For example, a team might require that: - Invalid orders are rejected before submission. - Repeated source events are deduplicated. - Unknown order outcomes trigger reconciliation. - Stale market data blocks new trading decisions. - Partial fills are represented accurately. - Restart recovery does not blindly duplicate previous orders. - Secrets never appear in logs. - Risk limits remain enforced during errors and reconnects. These are testable engineering requirements. They are more meaningful than simply declaring that a bot is ready because it ran for an hour without crashing. The appropriate criteria will depend on the bot's architecture and intended use. Conclusion Testing a Polymarket trading bot is not just about checking whether its strategy produces signals. It is about verifying that the entire system behaves correctly when inputs are invalid, data becomes stale, orders fail, and the application restarts. Start with deterministic unit tests, isolate external execution behind a testable interface, simulate failure conditions, and evaluate historical performance with realistic assumptions. Then progress to dry-run monitoring before considering live execution. Dexoryn Labs maintains a public "Polymarket copy-trading bot repository" (dexorynlabs/polymarket-copy-trading-bot) that developers can inspect when evaluating an existing implementation. Teams that need different testing, monitoring, execution, or risk-control workflows may require custom development. The goal is not to prove that a bot can never fail. It is to identify predictable failures before they become expensive, and to make unexpected failures easier to detect and recover from. This article is for educational purposes only and is not financial advice. Automated trading involves risk, and software tests cannot guarantee profitability or prevent every production failure.

  4. How to Build a Reliable Polymarket WebSocket Monitor in Python A Polymarket trading bot needs timely market data to make informed execution decisions. But connecting to a WebSocket and receiving messages is only the beginning. Connections can drop, messages can be delayed, and a bot can accidentally keep using an old order-book snapshot after reconnecting. A reliable monitor must do three things well: consume market updates, detect when its data is no longer trustworthy, and rebuild its state after a connection failure. This tutorial builds a small Python monitor around Polymarket's public market WebSocket. It focuses on top-of-book data, connection recovery, and stale-data protection rather than order execution. 1. Connect to the Polymarket market WebSocket Polymarket provides a public market channel for real-time order-book and trade updates. The documented endpoint is: "wss://ws-subscriptions-clob.polymarket.com/ws/market" The market channel subscribes using asset IDs, also called token IDs. Each outcome has its own token ID, so you need the appropriate ID for the outcome you want to monitor. The official "Polymarket WebSocket documentation" (github.com/Polymarket/agent-skills/blob/main/webs…) describes the subscription format and event types. Install the Python WebSocket library: pip install websockets The example below uses the raw WebSocket protocol so the connection lifecycle is visible rather than hidden inside a larger framework. 2. Understand the messages you receive Three message types are particularly useful for a basic monitor. "book" provides an order-book snapshot with bid and ask levels. Use it to initialize or rebuild local market state. "price_change" reports changes to price levels. Its "price_changes" array can contain updates for different assets and levels. "last_trade_price" reports a trade that has occurred. A last-traded price is not necessarily the price at which your next order can execute. For monitoring the best bid and ask, the "price_change" payload includes "best_bid" and "best_ask" fields. The code should still distinguish these updates from a complete order-book snapshot. That distinction matters after reconnecting: receiving one price update does not necessarily mean your local view of the entire book has been rebuilt. 3. Build a monitor with stale-data protection The monitor below tracks one outcome token. It starts in an unready state, subscribes to the market, sends the required application-level heartbeat, and marks the data ready only after receiving a fresh book snapshot. If the connection drops, the monitor retries with exponential backoff. Every new connection begins unready, preventing the application from treating an old snapshot as current. Replace "YOUR_OUTCOME_TOKEN_ID" with a valid token ID. import asyncio import json import time import websockets WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market" ASSET_ID = "YOUR_OUTCOME_TOKEN_ID" class MarketMonitor: def init(self): self.best_bid = None self.best_ask = None self.book_ready = False self.last_message_at = None def reset(self): self.best_bid = None self.best_ask = None self.book_ready = False self.last_message_at = None def handle(self, message): event_type = message.get("event_type") self.last_message_at = time.monotonic() if event_type == "book": if message.get("asset_id") != ASSET_ID: return bids = [ float(level["price"]) for level in message.get("bids", []) if float(level["size"]) > 0 ] asks = [ float(level["price"]) for level in message.get("asks", []) if float(level["size"]) > 0 ] self.best_bid = max(bids) if bids else None self.best_ask = min(asks) if asks else None self.book_ready = True print("Fresh book snapshot received.") elif event_type == "price_change": for change in message.get("price_changes", []): if change.get("asset_id") != ASSET_ID: continue # Do not trust incremental updates until a fresh # snapshot has initialized this connection's state. if not self.book_ready: continue if "best_bid" in change: self.best_bid = float(change["best_bid"]) if "best_ask" in change: self.best_ask = float(change["best_ask"]) if self.book_ready: print( f"Bid: {self.best_bid} | " f"Ask: {self.best_ask} | " f"Spread: {self.spread()}" ) def spread(self): if self.best_bid is None or self.best_ask is None: return None return round(self.best_ask - self.best_bid, 6) async def heartbeat(ws): while True: await asyncio.sleep(10) await ws.send("PING") async def run_monitor(): monitor = MarketMonitor() backoff = 1 while True: monitor.reset() try: async with websockets.connect( WS_URL, open_timeout=10, close_timeout=5, ping_interval=None, ) as ws: await ws.send(json.dumps({ "type": "market", "assets_ids": [ASSET_ID], "custom_feature_enabled": True, })) print("Connected and subscribed.") backoff = 1 heartbeat_task = asyncio.create_task(heartbeat(ws)) try: async for raw in ws: if raw == "PONG": continue message = json.loads(raw) if isinstance(message, dict): monitor.handle(message) finally: heartbeat_task.cancel() await asyncio.gather( heartbeat_task, return_exceptions=True, ) except ( OSError, asyncio.TimeoutError, websockets.exceptions.WebSocketException, json.JSONDecodeError, ) as error: print(f"Connection or message error: {error}") monitor.reset() print(f"Reconnecting in {backoff} seconds...") await asyncio.sleep(backoff) backoff = min(backoff * 2, 30) if name == "main": asyncio.run(run_monitor()) This is a teaching example, not a complete trading data service. It uses the best-bid and best-ask fields for top-of-book monitoring; it does not maintain a complete local order book after every incremental change. A production implementation should apply price-level updates correctly, validate messages, track data timestamps, and test the current protocol against the official documentation. 4. Why the monitor resets its state Imagine the connection drops while the best ask is $0.48. During the outage, the market moves to $0.53. If the bot reconnects but keeps using its old $0.48 value, a strategy might believe an attractive entry is still available when it is not. Resetting the state avoids presenting that old value as current. The monitor waits for a new book snapshot before declaring its top-of-book view ready. The same principle should apply to a trading bot's decision-making layer: - Do not submit new trades when market data is stale. - Do not assume a successful reconnect means the local book is synchronized. - Rebuild state from a fresh snapshot before trusting incremental updates. - Reconcile open orders and positions separately from public market data. A market-data connection does not tell you whether your own order was accepted or filled. Account and order state require their own reliable tracking process. 5. Detect stale data explicitly A connected socket does not guarantee that the data your strategy needs is fresh. A production monitor should track the time of the last valid market update and define a maximum acceptable age. If no qualifying update arrives within that period, it should mark the feed stale and prevent dependent trading decisions. The threshold should reflect the application. A dashboard may tolerate a longer delay than a strategy attempting to react to small price changes. It is also important to distinguish a quiet market from a broken connection. A lack of price changes does not necessarily mean the feed is unhealthy. Heartbeat responses, connection state, server timestamps, and application-level freshness checks should be considered together. Polymarket's documented market and user channels require sending "PING" every ten seconds, with the server responding "PONG". The example disables the WebSocket library's own ping frames because it sends the protocol's application-level heartbeat instead. 6. What to improve before using this in a trading bot A reliable production implementation needs additional safeguards. Full order-book maintenance. A "book" event supplies a snapshot. Subsequent "price_change" events can add, update, or remove price levels. A local book must apply these changes correctly, including updates where size becomes zero. Snapshot and update consistency. If the application loses messages or reconnects, it should rebuild the book rather than assuming its previous state remains valid. Structured logging. Record connection attempts, reconnects, stale-feed incidents, malformed messages, and snapshot recovery. Avoid logging private credentials. Backpressure. If updates arrive faster than the application can process them, an unbounded queue can increase memory usage and delay decisions. Measure processing lag and choose a queue policy appropriate to the strategy. Execution safeguards. Before placing an order, validate the age of the market data, price limits, available balance, and portfolio exposure. A fresh feed alone does not make a trade safe. Testing. Simulate disconnects, malformed messages, empty books, missing fields, and delayed updates. Verify that the bot refuses to act while its market state is unready or stale. Conclusion A Polymarket WebSocket monitor is more than a connection that prints prices. It is a stateful component that must know when its data is valid, when it has become stale, and when it needs to rebuild its view of the market. Starting with snapshot recovery, heartbeat handling, freshness checks, and bounded reconnect delays gives developers a stronger foundation for market-data systems. Dexoryn Labs maintains a public "Polymarket copy-trading bot repository" (dexorynlabs/polymarket-copy-trading-bot) for developers evaluating existing automation. Teams with more specific data-processing, monitoring, or execution requirements may need a custom implementation. Reliable market data does not guarantee profitable trading. It helps prevent a bot from making decisions based on information it should no longer trust. This article is for educational purposes only. Automated trading involves financial risk. Test in a non-trading environment before connecting market-data logic to live order execution.

  5. How to Prevent Duplicate Orders in a Polymarket Trading Bot with Python A Polymarket trading bot can receive the same event more than once, restart after a crash, or lose its connection while an order is being submitted. If the application treats every repeated signal as a new instruction, it can place duplicate orders and expose more capital than intended. The solution is not simply to add a delay between trades. A reliable bot needs idempotent event processing, persistent state, and order reconciliation. This tutorial explains how to design those safeguards in Python, using a copy-trading bot as the example. The same principles apply to other automated trading systems. Why duplicate orders happen Consider a bot that monitors a target wallet and copies its trades. The bot detects a buy event and submits a corresponding order. Several failure scenarios can produce an unintended second order. Repeated events: A reconnect or polling cycle causes the same source activity to be processed again. Application restarts: The process crashes after detecting a trade but before recording that the event has been handled. Network timeouts: The order request reaches the trading API, but the response never reaches the bot. The application cannot tell whether the order was accepted. Concurrent workers: Two workers process the same event before either records it as complete. Partial failures: The bot saves a trade as processed before submitting its order, then crashes. On restart, it may skip a trade that was never submitted. These are different failure modes. A single in-memory set of processed events will not solve all of them. 1. Give every source event a stable identity The first safeguard is to distinguish a genuinely new trade from a repeated notification about an existing trade. Ideally, the source provides a stable event identifier. Depending on the data source, this might be a transaction hash combined with a log index, or another unique activity identifier. Do not use only the wallet address, market ID, and trade direction as the deduplication key. A trader can legitimately buy the same outcome multiple times. Here is a simple Python example: def event_key(event: dict) -> str: tx_hash = event.get("transaction_hash") log_index = event.get("log_index") if tx_hash is None or log_index is None: raise ValueError("Missing stable event identity") return f"{tx_hash.lower()}:{log_index}" This example assumes the source exposes both fields and that the combination uniquely identifies the relevant event. Verify the actual schema before using it. If your data provider exposes a different stable identifier, use that instead. If no reliable identifier exists, a composite fingerprint may be necessary, but it must be designed around the source's semantics and can accidentally merge legitimate trades. The principle is simple: deduplicate events by identity, not by similarity. 2. Store processed events persistently An in-memory set disappears when the process restarts. A persistent database allows the bot to remember which source events it has already accepted for processing. SQLite is a practical starting point for a single-process Python bot. import sqlite3 db = sqlite3.connect("bot_state.db") db.execute(""" CREATE TABLE IF NOT EXISTS source_events ( event_key TEXT PRIMARY KEY, status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """) db.commit() The primary key prevents the database from storing the same event identity twice. You can attempt to register an event with an insert that ignores an existing key: def register_event(event_id: str) -> bool: cursor = db.execute( """ INSERT OR IGNORE INTO source_events (event_key, status) VALUES (?, ?) """, (event_id, "received"), ) db.commit() return cursor.rowcount == 1 If the function returns "True", the event was newly registered. If it returns "False", an event with that identity already exists. This is more reliable than checking a set and inserting later, because the database enforces uniqueness. In a multi-worker system, the claim operation and associated state transitions still need appropriate transaction and concurrency handling. Most importantly, registering an event does not mean its order has been executed. That distinction leads to the next safeguard. 3. Track order states explicitly A robust bot should represent the lifecycle of an event and its associated order. A simplified state model might include: - "received": the source event has been registered. - "validated": the event passed market, price, balance, and risk checks. - "submitting": the bot is attempting to send the order. - "submitted": the API acknowledged the order. - "filled": the order was confirmed filled. - "rejected": the order was rejected. - "unknown": the request outcome cannot yet be established. These states should not be treated as interchangeable. For example, a submitted order might remain open, fill partially, or be cancelled. A rejected order is different from an order whose response timed out. A useful database design separates the source event from the order attempt. One source event may produce no order because it fails validation, while a valid event may require more than one order attempt under a carefully defined recovery policy. The system should record the source event identity, intended market and outcome, requested size, order identifier when available, current state, timestamps, and any error details. That record makes recovery possible without blindly repeating the original action. 4. Treat timeouts as unknown outcomes This is one of the most important rules in automated execution. Suppose the bot sends an order request. The server accepts it, but the network connection drops before the response arrives. The bot sees a timeout. It does not know whether the order was accepted. If it immediately submits the same order again, the account could end up with two orders. A safer recovery process is: 1. Mark the attempt as "unknown". 2. Do not blindly resubmit it. 3. Query the trading API for the order or relevant activity using the identifiers available. 4. Reconcile the response with locally stored state. 5. Retry only when the original request is known not to have produced an order and the retry remains valid. The exact reconciliation procedure depends on the current API and the information it exposes. Read the current "Polymarket developer documentation" (docs.polymarket.com) before implementing it. There is an important limitation here: a local database transaction cannot atomically commit both a database update and a remote trading order. Without a server-supported idempotency mechanism, the application cannot guarantee exactly-once execution across every network failure. The practical goal is to make duplicate execution unlikely, detect ambiguous outcomes, and recover conservatively. 5. Separate event detection from order execution Do not place the entire trading workflow inside a WebSocket message handler. If event detection, database writes, risk checks, network requests, and order submission all happen in one callback, a slow API response can interfere with processing incoming events. A cleaner architecture separates the responsibilities: Market or wallet activity | v Event normalizer | v Persistent event registry | v Validation and risk | v Order submission | v Order reconciliation | v Persistent state update The event registry prevents the same source activity from being independently accepted multiple times. The execution component handles order submission. Reconciliation checks whether the external system agrees with the bot's local records. A queue can connect the components, but the queue alone is not a deduplication mechanism. Events may be delivered more than once, workers may crash, and messages may be retried. The persistent event identity and state machine remain essential. 6. Test the failure cases, not only successful trades A bot that works during a normal test run may still fail when requests time out or the process restarts. At minimum, test these scenarios: Duplicate event: Deliver the same source event twice. Confirm that it creates no more than one active processing record. Crash before submission: Restart the application after registering an event but before sending an order. Confirm that recovery resumes safely. Timeout after submission: Simulate a request whose result is unknown. Confirm that the bot reconciles before considering another submission. Concurrent processing: Allow two workers to receive the same event simultaneously. Confirm that database uniqueness and transaction handling prevent duplicate processing. Partial fill: Confirm that the bot tracks filled and remaining quantities rather than treating any fill as a fully completed order. Stale state: Restart the bot with existing open orders and positions. Confirm that it reconciles external state before resuming new execution. These tests should use mocked API responses or a dry-run environment. They should not require risking live funds. 7. Add observability to make failures diagnosable When an order is rejected or an event is skipped, the logs should explain why. Record the source event identifier, state transition, validation outcome, order identifier, requested size, response status, and reconciliation result. Never log private keys, API secrets, or other credentials. Useful operational metrics include: - Duplicate source events detected. - Orders submitted and rejected. - Requests with unknown outcomes. - Orders awaiting reconciliation. - Partial fills and unresolved order states. - Time spent between event detection and order confirmation. These measurements help distinguish a data-feed problem from an execution problem or a state-management bug. Conclusion Preventing duplicate orders in a Polymarket trading bot requires more than checking whether a trade was seen before. Use stable event identities, persistent uniqueness constraints, explicit order states, conservative timeout recovery, and reconciliation with the external trading system. Then test the system under duplicate delivery, restarts, concurrency, and ambiguous network outcomes. For developers building more advanced automation, these safeguards should be part of the trading architecture from the beginning. Dexoryn Labs maintains a public "Polymarket copy-trading bot repository" (dexorynlabs/polymarket-copy-trading-bot) that developers can inspect when evaluating an existing implementation. Teams with different execution, monitoring, or risk requirements may also need custom development. Reliable execution does not guarantee profitable trading. It helps ensure the bot behaves more predictably when software and network failures occur. This article is for educational purposes only. Automated trading involves financial risk. Test in dry-run mode before considering live execution, protect credentials, and use only funds you can afford to lose.

    1 comment

Achievements

Awards earned by the repositories dexorynlabs owns. Select an award to embed its badge.

Gold
01st place finishes
Silver
12nd place finishes
Bronze
03rd place finishes
Awarded repositories
1Owned by dexorynlabs now

Repositories

Ordered by gold, then silver, then bronze.

  1. polymarket-trading-bot-pythonBest: 2nd by forks, 159 forks010

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. polymarket-trading-bot-python

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.