Type/to search
8
Follow
1376
Followers
Adaptive Conditional Probability Modeling: A Practical Exploration and Implementation for Binary Markets
Discussions
Created 2026-08-26 14:11:37  Updated 2026-08-26 14:31:32
 0
 2

img

I had wanted for some time to build a relatively complete model for binary markets.

At first glance, these markets look simple. Take Polymarket's 15-minute BTC Up/Down market as an example: each round has only two outcomes. If the settlement price finishes above the benchmark price, Up wins; otherwise, Down wins. There is no complex payoff curve, nor the large matrix of strikes and maturities found in traditional options.

But once you try to turn the idea into a quantitative system, it becomes surprisingly difficult to find the right entry point.

If the task is reduced to predicting whether BTC will rise or fall next, the problem quickly becomes another moving-average, momentum, breakout, or order-flow strategy. If we simply treat the Polymarket contract price as the probability, it becomes difficult to answer another important question: has the market price already incorporated the information we currently observe?

When I revisited Bayesian ideas recently, a different framing occurred to me: perhaps what a binary market really requires us to model is not direction, but conditional probability.

\[ P(Y=\mathrm{Up}\mid X_t) \]

Here, \(Y\) is the final settlement outcome, while \(X_t\) represents the information observable at time \(t\), including BTC's position relative to the benchmark price for the current round, remaining time, volatility across multiple horizons, short-term drift, the price path, and the uncertainty of the information currently available.

The question therefore changes from:

Will BTC go up?

to:

Given the conditions that have already occurred, what is the probability that BTC will ultimately finish above the benchmark price?

This may sound like a simple change in wording, but it determines the architecture of the entire system.


Why Conditional Probability Fits Binary Markets

Suppose the Up contract can currently be bought at 0.58. Temporarily ignoring trading frictions, we can interpret that as the market assigning Up a price of roughly 58%.

If our model also estimates the probability at 58%, then even if the directional conclusion is Up, there is no trading value. A trade becomes potentially interesting only when the model's conditional probability differs sufficiently from the price we can actually execute.

We therefore need two separate quantities:

\[ p_t=P(Y=\mathrm{Up}\mid X_t) \]

and

\[ q_t=\text{actual executable cost of buying the target side} \]

The second quantity, \(q_t\), cannot simply be the best Ask displayed on the page. A real order may consume multiple levels of the order book, while fees and slippage also matter.

The implementation therefore walks through Ask depth using the intended purchase quantity, calculates the full VWAP, and then computes the expected absolute profit per token:

\[ EV_t=p_t(1-f)-(VWAP_t+s)(1+f) \]

The corresponding code is:

rust
fn expected_value(prob: f64, buy_vwap: f64) -> f64 { prob * (1.0 - FEE_RATE) - (buy_vwap + SLIPPAGE) * (1.0 + FEE_RATE) }

A winning binary contract ultimately pays 1, while a losing contract pays 0. The expression above therefore represents the expected number of dollars earned per token rather than an abstract rate of return.

The current entry threshold is 0.025. In other words, after estimated fees and slippage, each token must still retain at least 2.5 cents of expected edge.

This is also the most important difference between a conditional-probability model and an ordinary directional indicator.

A directional indicator tells us whether the market currently leans bullish or bearish. A probability model tries to answer how likely a specific final outcome is under the current state. The execution layer then asks whether that probability can actually be monetized at prices available in the real order book.

Polymarket prices naturally contain a great deal of information, but there is an easy circularity trap here. If the market price is used as an input feature and the model output is then compared with that same market price to calculate EV, the model can end up in a circular argument: the market price helps generate the forecast, and the forecast is then used to claim that the market price is wrong.

For that reason, I keep the two information paths separate:

  • Binance market data and the official benchmark price generate an independent terminal probability.
  • The Polymarket order book is used only for trading prices, depth, spread, and execution cost.

Finding OpenMarket: A Research Framework Honest Enough to Be Useful

While looking for related work, I came across Gregory Young's open-source OpenMarket project and the paper OpenMarket: A Synchronized Polymarket-Binance Dataset for High-Frequency Prediction-Market Research.

OpenMarket GitHub:

https://github.com/gregyoung14/openmarket

Paper:

https://arxiv.org/abs/2607.26245

The project studies exactly the relationship between Binance BTC/USDT market data and Polymarket's 15-minute BTC binary markets.

The author built a fairly complete research framework: millisecond-level Binance trades and Polymarket order books are collected and synchronized; 43 features are constructed across time, returns, volatility, order flow, and market microstructure; and terminal probabilities are then evaluated using Logistic Regression, Platt Scaling, and walk-forward validation.

What I find especially valuable is that the author does not package the results as a story about having discovered stable arbitrage.

The paper's conclusion is much more honest: the public 43-feature model does not consistently outperform the probability implied by the Polymarket order book out of sample. After fees, spreads, and slippage are included, the model also does not produce a trading edge that can simply be declared effective.

That does not make the framework less useful. For quantitative research, a negative result with public features, data, model parameters, calibration methods, and out-of-sample results is often more valuable than a "successful strategy" that shows only an equity curve and cannot be reproduced.

At a minimum, OpenMarket answers three important questions:

  1. What data should be collected?
  2. How can short-horizon market state be transformed into a conditional probability?
  3. How should probability-fitting ability be separated from actual tradability?

I retained the data-collection part of this research framework. The 43 stage features are written to the database every five seconds, and the final label is added after settlement. These records can later be used for stage-bucketed training and out-of-sample validation.

For now, however, the fixed feature weights do not directly participate in the live trading probability.

The reason is concrete. At the 60-second mark, a 180-second return does not yet exist, and a feature called "180-second volume" actually contains only 60 seconds of data. By the tenth minute, that same feature name finally refers to a complete 180-second window.

If one uncalibrated set of weights is used from a few seconds after market open all the way to minute 14, the feature names may stay the same while their statistical meaning changes substantially.

So this implementation does not rush a complex model into the production decision path. Instead, it starts from the terminal event itself and builds a transparent probability baseline that can be inspected directly.


Starting with the Distance to the Terminal Event

Predicting the 15-minute result at minute 1 does not mean knowing the future. It means estimating a terminal probability under the current information set:

\[ P(S_T>K\mid S_t,K,\tau,\sigma_t,\mathcal{I}_t) \]

where:

  • \(K\) is the official Price-to-Beat for the current round;
  • \(S_t\) is the current BTC reference price;
  • \(\tau\) is the remaining time;
  • \(\sigma_t\) is the estimate of future volatility;
  • \(\mathcal{I}_t\) represents the information already available at time \(t\).

The most basic standardized distance is:

\[ z_t = \frac{\ln(S_t/K)+\tilde{\mu}_t\tau} {\hat{\sigma}_t\sqrt{\tau}} \]

The central Up probability is then approximated through the standard normal distribution:

\[ p_{\text{center},t}\approx\Phi(z_t) \]

The formula itself is not complicated. The difficult part is estimating volatility and drift.

If a round has been open for only 15 seconds and we extrapolate those 15 seconds of volatility over the remaining 885 seconds, the result can be seriously distorted. If the most recent seconds happen to be quiet, the denominator becomes too small. If those seconds happen to be strongly directional, the drift estimate can become too large.

The combination can easily create false certainty shortly after market open.

For that reason, volatility is not estimated only from data inside the current round.

The system builds one-second closes from the Binance real-time trade stream and combines realized volatility over 60-second, 5-minute, 15-minute, and 30-minute windows:

rust
let windows = [ (60usize, 0.15), (300usize, 0.25), (900usize, 0.30), (1800usize, 0.30), ]; for &(window, weight) in windows.iter() { if available >= window * 3 / 4 { let vol = realized_vol(&close, window); variance += weight * vol * vol; total_weight += weight; } }

This allows the model to use pre-open price history to estimate remaining volatility even when a new round has just started.

The system also imposes a conservative floor on per-second volatility so that an accidentally quiet short sample does not push the estimated probability toward an extreme.

Drift is handled even more conservatively.

The average return over the most recent 60 seconds is not simply multiplied by the entire remaining time. Instead, it is shrunk toward zero according to the number of effective observations:

\[ \tilde{\mu}_t=\hat{\mu}_t\frac{n}{n+300} \]

The total contribution of drift to the terminal distribution is then capped at 0.25 times the remaining volatility:

rust
let reliability = n as f64 / (n as f64 + 300.0); let drift_total = drift_per_sec * remaining * reliability; let drift = clamp( drift_total, -0.25 * remaining_sigma, 0.25 * remaining_sigma, );

Short-term trend is allowed to adjust the probability, but it is not allowed to turn a single trade into something that appears almost certain on its own.

During the first five minutes after market open, the system also reduces confidence in the probability's log-odds.

This is not an arbitrary rule saying that no trades are allowed during the first few minutes, nor does it force the probability to remain at 50%. It simply recognizes that the same price distance carries different information quality at different stages of the round.

As the stage data becomes more complete, the confidence coefficient gradually returns to 1.


The Central Probability Is Not Necessarily the Probability You Should Trade On

This is one of the most important steps in the entire framework.

The model's central probability tells us where the current estimate is centered. It does not imply that the estimation error is zero.

This matters especially just after market open, when volatility history is incomplete, or when the estimated probability itself is changing rapidly. If the central probability is used directly to calculate EV, the system can easily confuse uncertainty with certainty.

For the side the strategy is considering buying, it therefore uses a probability discounted by uncertainty:

\[ p_{\text{trade},t}=\max\left(0.001,\,p_{\text{center},t}-z_u u_t\right) \]

The current implementation uses \(z_u=1\).

The uncertainty term \(u_t\) has three components:

  • recent volatility of the probability sequence itself;
  • information deficiency during the early stage of the round;
  • the amount of volatility history currently available.

This is not another hard rule such as "wait for N warm-up observations before trading."

The first valid probability estimate is still calculated immediately. The difference is that less information leads to a lower tradable probability. Only if EV remains positive after this conservative discount does the candidate continue to the next stage.

One simulated log entry illustrates the distinction well.

At the 15-second mark, the central probability of Down was approximately 49.75%, while the contract Ask was 0.43. Looking only at the central probability, the system appeared to have more than six percentage points of room.

But after taking VWAP and slippage into account, the effective entry cost was approximately 0.435, while the full-position liquidation value was only 0.42. The position therefore started with an immediate mark-to-market loss of 5.36%.

After stage uncertainty was included, if the bot had a complete 30-minute volatility history, the tradable probability fell to approximately 44.81%, and fee-adjusted net EV was only 0.0043, below the 0.025 threshold.

If the bot had just started and only 15 seconds of volatility history were available, the tradable probability fell to approximately 41.96%, and net EV became negative.

The trade would therefore fail at the probability layer before reaching execution.

The point of adaptation here is not that the model continuously changes all of its parameters. The important adaptation is that the credibility of the probability changes with information quality.


FMZ Happened to Add Rust Support at the Right Time

Once the modeling framework was defined, the next question was how to implement something suitable for continuous operation.

FMZ happened to add Rust support, which made this architecture much easier to implement cleanly.

Rust is well suited to real-time data handling, fixed-dimension numerical work, and complex state management. Its compiler also catches many field, type, and ownership errors before the strategy is allowed to run.

The current framework requires only one Polymarket exchange object to be configured in FMZ.

Binance BTC/USDT data is consumed directly through the public WebSocket:

rust
const BINANCE_WS_URL: &str = "wss://stream.binance.com:9443/ws/btcusdt@trade"; let mut binance_ws = Dial(BINANCE_WS_URL);

The Polymarket BTC 15-minute market is assembled deterministically from UTC time:

rust
let start = (now_ms() / 900_000) * 900_000; let slug = format!("btc-updown-15m-{}", start / 1000);

The system validates the market and retrieves token information through the Gamma Market API.

At the same time, it subscribes to Chainlink and TWAP60 data from Polymarket RTDS, keeping four concepts separate:

  • Binance real-time trade price;
  • Chainlink reference price;
  • current 60-second TWAP;
  • the fixed official Price-to-Beat for the current round.

These prices must not be treated as interchangeable.

The real-time price describes the current state. The Price-to-Beat defines the terminal event:

\[ Y=\mathbf{1}(S_T\ge K) \]

If the benchmark \(K\) is wrong, every probability calculation downstream becomes meaningless.

For that reason, the trading path requires the official openPrice. Alternative sources may be stored for research and comparison, but a current price near settlement must never be substituted for the official opening benchmark.

Multi-horizon volatility is updated and cached once per second. Terminal probability is recalculated roughly every 200 milliseconds, while the main loop polls data and manages state at approximately 50-millisecond intervals.

The implementation deliberately does not confuse "record features every five seconds" with "make a decision only every five seconds."

Five seconds is the research snapshot frequency, not the trading-response frequency.


Passing the Probability Test Still Does Not Mean an Order Will Be Sent

Even if uncertainty-adjusted EV reaches 0.025, the system does not immediately trade the first frame that appears profitable.

When a new market opens, the order book is often still being assembled. Quotes on both sides, available depth, and market-maker inventory can change very quickly.

A single qualifying EV observation means only that a candidate opportunity exists at that moment. It does not prove that the edge persists.

The system therefore maintains a very short entry-evidence state.

The candidate direction must remain unchanged, net EV must remain above the threshold for at least 1.2 seconds, and the signal must accumulate at least five valid updates.

If the direction changes, EV disappears, or an order-book check fails, the evidence counter is immediately reset.

rust
let ready = evidence_count >= 5 && now_ms - evidence_first >= 1_200; if !ready { signal.decision = "EV confirmation pending"; return; }

The 1.2-second requirement is not a slow warm-up period. It is simply a way to distinguish persistent edge from a momentary order-book dislocation.

Immediately before submitting an order, the system reloads both the Up and Down books, checks whether their timestamps are synchronized, and validates the complementary relationship of the binary market:

\[ Ask_{\mathrm{Up}}+Ask_{\mathrm{Down}}\ge 0.98 \]

\[ Bid_{\mathrm{Up}}+Bid_{\mathrm{Down}}\le 1.02 \]

It then calculates the fee-adjusted round-trip loss of buying now and immediately selling the entire position.

This check incorporates Ask depth, Bid depth, slippage, and fees on both sides. The current maximum allowed loss is 4%.

The earlier example that would begin with an immediate 5.36% loss would therefore be rejected by the execution layer even if the probability EV barely passed.

Position size uses one-fifth Kelly:

\[ f=0.20\times\frac{p_{\text{trade}}-q_{\text{break-even}}}{1-q_{\text{break-even}}} \]

The final allocation is capped between 0.1% and 3% of account equity.

Therefore, if the simulation account has 100 USDC, the maximum capital committed to a single position is 3 USDC.

This is not a balance-reading error. It is an intentional cap on per-trade risk.

The probability model is not responsible for submitting orders directly.

It proposes a probability view that can be tested. The order book determines whether execution is available at acceptable terms. Persistent evidence, round-trip cost, and position sizing determine whether the probability view is worth expressing with real capital.


After Entry: Prediction Makes the Decision, Price Provides the Backstop

After a position has been opened, the system continuously calculates the VWAP at which the entire position could actually be sold instead of looking only at the best Bid.

Actual PnL is measured using the current executable liquidation value and the true cost basis:

\[ PnL_t=\frac{ExitValue_t-Cost}{Cost} \]

The prediction side continuously calculates the EV of the current position:

\[ EV_t^{hold}=p_t^{smooth}(1-f)-\frac{Cost}{Quantity} \]

A normal prediction-based exit is not triggered simply because the probability falls by a few percentage points.

Instead, the current holding EV must fall below -0.005 and decline by at least 0.025 relative to its prior level. This condition must then be confirmed three consecutive times for at least 600 milliseconds.

Once all conditions are met, the strategy exits with:

text
PREDICTED_EV_SHOCK_NEGATIVE

This means that if probability declines but the position's EV remains positive, the strategy continues to hold.

Conversely, even a probability change that does not look large can trigger an exit if it is sufficient to turn the original trading thesis into negative expected value.

On the price side, the system retains only one fixed stop line, determined at entry and never moved afterward:

\[ StopPrice=\max(EntryVWAP-0.50,\ 0.35) \]

The code is:

rust
let fixed_stop_price = (position.vwap - 0.50).max(0.35);

The stop is not based on a percentage of entry cost, and it does not follow the highest Bid observed during the holding period.

This avoids triggering a trailing stop simply because the order book briefly spikes and then normalizes.

The strategy uses the executable VWAP obtained by walking through the entire Bid side to determine whether the stop has actually been breached.

A fixed take-profit at 0.95 is also retained.

This stop is better understood as catastrophe protection than as the normal prediction-management mechanism.

When the entry price is below 0.85, the stop line remains 0.35. With an entry price of 0.915, the stop becomes 0.415.

That can correspond to a large percentage loss on the token, which is why normal exits still rely primarily on predicted EV turning negative.

Whether this fixed stop is appropriately calibrated still needs to be tested against real holding paths and maximum adverse excursion.


The Database Gives the Research Time Continuity

A system like this loses much of its value if it cannot persist state.

If the bot restarts and all probability history, positions, and simulated-account state disappear, the research process itself becomes discontinuous.

The system uses FMZ DBExec to persist:

  • each market round;
  • the official benchmark price;
  • stage-feature snapshots;
  • probability sequences;
  • orders;
  • positions;
  • exit reasons;
  • settlement outcomes;
  • simulated balance;
  • risk state.

After a restart, the bot can recover unsettled positions instead of pretending that already-existing risk no longer exists.

Paper trading and live trading use the same state machine.

The difference is that paper fills do not send real orders. Instead, they update the internal ledger using full VWAP, slippage, and estimated fees.

Real-time logs explicitly display simulated buys and sells, position direction, quantity, cost, executable liquidation value, actual PnL, central probability, tradable probability, predicted EV, fixed stop price, and exit reason.

The status panel continuously shows:

  • Binance, Chainlink, TWAP, and the official benchmark price;
  • Up/Down central probabilities, stage uncertainty, and tradable probabilities;
  • real Ask prices on both sides, full VWAP, and fee-adjusted net EV;
  • accumulated entry evidence and reasons why no order has been sent;
  • current position cost, executable full-position value, and actual PnL;
  • holding EV, EV shock magnitude, exit-confirmation count, and fixed stop price;
  • recent trades, simulated balance, and cumulative realized PnL.

These records are not there to make the status table look complicated.

They are needed to answer one key research question:

Did the strategy lose because the probability forecast was wrong, or because the order book, slippage, liquidity, and execution costs prevented the forecast from being monetized?

If those two categories cannot be separated, later model optimization can easily move in the wrong direction.


This Is Still Only a Practical Research Experiment

I prefer to think of the current system as an adaptive conditional-probability experimentation platform rather than a fully validated trading strategy.

"Adaptive" here does not mean changing the model after every outcome, nor does it mean continuously feeding the market price back into the forecast.

What really needs to adapt is information quality.

The same price distance should not carry the same credibility at second 30 and minute 13.

Likewise, the same 50% central probability should not produce the same tradable probability or position size when one estimate has 30 minutes of volatility history behind it and another has only 15 seconds of data.

The current implementation first trades using a transparent terminal-distance model while continuing to store the 43 stage features and the final settlement label.

Once enough data has accumulated, the dataset can be divided into stage buckets such as:

  • 0–1 minute;
  • 1–3 minutes;
  • 3–6 minutes;
  • 6–10 minutes;
  • 10–14 minutes.

Each bucket can then be evaluated out of sample using calibration curves, Brier Score, Log Loss, and actual executable trading returns.

Only if a feature set continues to provide incremental information beyond the Polymarket order book during the corresponding stage should it be allowed into the production probability model.

Otherwise, a more complex model merely adds more parameters to a simple hypothesis.

Future work can also explore:

  • conditional distributions across different market regimes;
  • jump risk caused by macroeconomic releases and breaking news;
  • First-Passage Probability;
  • regime-specific Logistic models;
  • dynamic volatility models;
  • nonlinear models.

But continuous validation matters more than adding complexity:

  • Are the probabilities calibrated?
  • Does the edge exist at prices that can actually be executed?
  • Can the expected return survive fees, spreads, slippage, and execution latency?

For a binary market, the difficult part is not simply deciding Up or Down.

The real challenge is repeatedly answering three questions:

  1. Given the current conditions, what is the probability of the event?
  2. How much confidence should we place in that probability?
  3. Can the probability difference be realized after actual trading costs?

Placed back into the trading pipeline, the structure is roughly:

text
Central Probability ↓ Stage Uncertainty ↓ Tradable Probability ↓ Real VWAP + Fees ↓ Persistent EV Evidence ↓ Position Sizing and Exit

The prediction model proposes a view.

Market prices define the trading terms.

The execution layer determines whether that view can be monetized.

The risk layer limits losses when the view is wrong.

At the current stage, the system should still be used primarily for paper trading and data collection.

A logically complete system is not the same thing as a statistically validated one.

Whether this framework can ultimately produce a stable edge can only be answered through stage-specific out-of-sample results and returns measured at genuinely executable prices.


Risk Warning: This article discusses quantitative research and engineering implementation only and does not constitute investment advice. Binary contracts can expire worthless. Liquidity deterioration, order-book jumps, model failure, and settlement-rule changes can all cause significant losses.

Strategy source code:

https://www.fmz.com/strategy/548121

Related Recommendations
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)