DEV Community

Cover image for Arbitrage Betting Is a Systems Problem, Not Just a Formula
E. Mitev
E. Mitev

Posted on

Arbitrage Betting Is a Systems Problem, Not Just a Formula

Arbitrage betting is often introduced with one attractive promise: cover every outcome and lock in a return.

The formula is real. The word lock is where things get complicated.

A calculation can prove that a set of quoted prices forms a theoretical arbitrage. It cannot prove that those prices are still available, that the markets settle identically, that every stake will be accepted, or that an exchange order will fill completely.

That makes arbitrage an interesting systems problem. The arithmetic is deterministic, but the inputs arrive from changing external systems and execution happens through multiple independent venues.

This article develops the calculation, implements it in JavaScript, and then examines the validation and state-management work required to turn detected prices into a fully covered position.

This is an educational discussion of market mathematics and software design, not financial or betting advice. Laws, taxes, age requirements, and operator rules vary by jurisdiction.

The Mathematical Condition

Assume a market has mutually exclusive outcomes with decimal odds:

O₁, O₂, ... Oₙ
Enter fullscreen mode Exit fullscreen mode

Calculate the reciprocal sum:

S = 1/O₁ + 1/O₂ + ... + 1/Oₙ
Enter fullscreen mode Exit fullscreen mode

Interpret the result as follows:

  • S < 1: theoretical arbitrage before costs and execution constraints
  • S = 1: theoretical break-even before costs
  • S > 1: no full-coverage arbitrage at those prices

The theoretical gross return on the deployed capital is:

ROI = (1/S - 1) × 100%
Enter fullscreen mode Exit fullscreen mode

This is not always the same number as the simple gap between the reciprocal sum and 100%. A tool should label the two quantities clearly.

A Two-Outcome Example

Suppose the best available prices are:

  • Outcome A: 2.10
  • Outcome B: 2.05

The reciprocal sum is:

1/2.10 + 1/2.05
= 0.47619 + 0.48780
= 0.96399
Enter fullscreen mode Exit fullscreen mode

Because 0.96399 < 1, these quotes form a theoretical arbitrage.

The gross theoretical ROI is approximately:

(1/0.96399 - 1) × 100% ≈ 3.735%
Enter fullscreen mode Exit fullscreen mode

Equal stakes would not produce equal payouts because the odds differ.

For a total stake T, allocate each position using:

Stakeᵢ = T × (1/Oᵢ) / S
Enter fullscreen mode Exit fullscreen mode

With 1,000 units of total capital, the unrounded allocation is approximately:

Outcome Odds Stake Gross payout
A 2.10 493.98 1,037.36
B 2.05 506.02 1,037.34

The small payout difference comes from rounding.

Real execution may introduce larger differences through stake increments, limits, partial fills, and accepted-price changes.

Implementing the Calculator in JavaScript

The core calculation is short:

function calculateArbitrage(odds, totalStake) {
  if (!Array.isArray(odds) || odds.length < 2) {
    throw new Error("Provide at least two outcomes");
  }

  if (odds.some((price) => !Number.isFinite(price) || price <= 1)) {
    throw new Error(
      "Decimal odds must be finite numbers greater than 1"
    );
  }

  if (!Number.isFinite(totalStake) || totalStake <= 0) {
    throw new Error("Total stake must be a positive number");
  }

  const reciprocalSum = odds.reduce(
    (sum, price) => sum + 1 / price,
    0
  );

  const positions = odds.map((price, index) => {
    const stake = totalStake * (1 / price) / reciprocalSum;
    const payout = stake * price;

    return {
      outcome: index + 1,
      odds: price,
      stake,
      payout,
      profit: payout - totalStake,
    };
  });

  return {
    isTheoreticalArbitrage: reciprocalSum < 1,
    reciprocalSum,
    reciprocalPercentage: reciprocalSum * 100,
    theoreticalRoi: (1 / reciprocalSum - 1) * 100,
    positions,
  };
}

console.log(calculateArbitrage([2.10, 2.05], 1000));
Enter fullscreen mode Exit fullscreen mode

This function answers a narrow question:

What position follows from these inputs?

It does not establish that the inputs describe executable contracts.

That boundary should remain explicit in the software.

Detection Is Not Execution

A useful arbitrage system distinguishes at least three states:

  1. Detected: Observed prices produce a reciprocal sum below one.
  2. Executable: The event, market, rules, current prices, limits, balances, and liquidity have been verified.
  3. Completed: Every leg is accepted or matched at the intended size, and final exposure has been recalculated from confirmed values.

The first state can be identified from a data feed.

The second requires validation against live venue state.

The third requires acknowledgements from every venue.

A scanner result should therefore be treated as a candidate, not as a completed transaction.

Market Identity Is Part of the Data Model

Two price records are compatible only if they refer to the same contract. Matching names is not enough.

A robust market key may need fields such as:

const marketIdentity = {
  sport: "football",
  competition: "example-league",
  eventId: "home-v-away-2026-09-23",
  scheduledStart: "2026-09-23T19:00:00Z",
  period: "regulation-time",
  marketType: "match-result",
  line: null,
  participant: null,
  includesOvertime: false,
  settlementVersion: "operator-rules-2026-09",
};
Enter fullscreen mode Exit fullscreen mode

The exact schema depends on the sport and market.

The important point is that period, line, participation conditions, overtime, void treatment, and other settlement rules are not peripheral metadata. They define the payoff states assumed by the formula.

If two legs can settle differently after the same real-world event, they are not clean complements.

Quotes Need Timestamps and Provenance

Every quote should carry enough information to answer:

  • Where did the price come from?
  • When was it observed?
  • Is it pre-match or live?
  • How much is available at that price?
  • Has the source suspended or changed the market?

For example:

const quote = {
  venue: "example-operator",
  marketKey: "event:123:regulation:moneyline:away",
  decimalOdds: 2.10,
  observedAt: "2026-09-23T18:42:10.120Z",
  availableSize: 250,
  status: "open",
};
Enter fullscreen mode Exit fullscreen mode

The system can then apply a maximum quote age or require manual verification before execution.

This does not make stale prices impossible, but it makes freshness a visible control rather than an unstated assumption.

Model the Execution as a State Machine

Execution involves transitions that can fail independently.

A position might move through states like:

DETECTED
  → VALIDATED
  → LEG_1_SUBMITTED
  → LEG_1_CONFIRMED
  → LEG_2_SUBMITTED
  → FULLY_COVERED
  → SETTLED
Enter fullscreen mode Exit fullscreen mode

Failure states matter just as much:

PRICE_CHANGED
LIMITED
REJECTED
PARTIALLY_FILLED
SUSPENDED
MARKET_MISMATCH
Enter fullscreen mode Exit fullscreen mode

Thinking in states prevents a dangerous simplification: treating a submitted request as a confirmed position.

For each accepted or matched leg, store the actual values:

  • Requested stake
  • Accepted or matched stake
  • Requested price
  • Accepted or weighted-average matched price
  • Confirmation timestamp
  • Unmatched remainder
  • Required liability for lay positions

The final exposure calculation must use these confirmed values, not the original scanner snapshot.

Rounding Must Be Audited

The calculator above returns fractional stakes.

Venues usually accept only specific increments, so production code needs a rounding policy.

Rounding each stake independently can create uneven payouts. A safer workflow is:

  1. Calculate ideal stakes at full precision.
  2. Generate valid nearby stakes using each venue’s increment.
  3. Calculate the net result under every outcome.
  4. Select an allocation only if its worst-case result remains acceptable.

The key metric after rounding is not average profit. It is the minimum net outcome across all covered states.

function auditOutcomes(positions, totalCommitted) {
  return positions.map(({ outcome, stake, odds }) => ({
    winningOutcome: outcome,
    netResult: stake * odds - totalCommitted,
  }));
}
Enter fullscreen mode Exit fullscreen mode

In a real implementation, subtract applicable commission, fees, taxes, and currency costs from the relevant cash flows before evaluating the minimum.

Exchanges Add Partial-Fill Complexity

An exchange quote combines price and available size.

If 100 units are requested but only 20 match at the intended price, the remaining 80 units are not covered.

Multiple fills also require a weighted execution price:

Weighted price =
Σ(fill stake × fill odds) / Σ(fill stake)
Enter fullscreen mode Exit fullscreen mode

For lay orders, exposure cannot be tracked by stake alone.

Liability for each fill is:

Liability = (lay odds - 1) × lay stake
Enter fullscreen mode Exit fullscreen mode

If fills occur at several prices, calculate liability for each fill and aggregate the actual market outcomes.

An order that remains unmatched also needs a cancellation or recalculation rule. Leaving it open is itself a risk decision.

A Practical Validation Pipeline

Before the first leg:

  1. Confirm that every record refers to the same event.
  2. Match the exact market, period, line, and selection.
  3. Compare settlement, void, and participation rules.
  4. Verify each price at the venue.
  5. Check balances, limits, maximum payout, liquidity, and liability.
  6. Recalculate using current prices and known costs.
  7. Decide the execution order.
  8. Define responses to movement, rejection, or partial matching.

After each leg:

  1. Record the accepted price and stake.
  2. Verify the event, selection, market, and line again.
  3. For exchange orders, record matched size, weighted price, and unmatched size.
  4. Recalculate the remaining position before continuing if any value differs from the plan.

After all legs:

  1. Calculate the net result under every possible covered outcome.
  2. Use confirmed stakes and prices.
  3. Include relevant commission and known costs.
  4. Flag any negative or unexpectedly uneven outcome for immediate review.

Metrics Beyond Realized Profit

Realized profit alone provides weak feedback about system quality.

A useful execution log can also track:

  • Detected-to-executed conversion rate
  • Displayed versus realized ROI
  • Average executable stake
  • Opportunities lost to price movement
  • Rejected or limited legs
  • Partial-fill frequency
  • Settlement and void issues
  • Errors by type
  • Capital turnover and idle balances
  • Net return after known costs

These metrics help locate the real bottleneck.

If many candidates fail live verification, better freshness filters may matter more than faster interaction.

If stake limits dominate, headline opportunity count may be less useful than executable capacity.

The Engineering Lesson

The arbitrage formula is the smallest component of a reliable workflow.

The harder work is maintaining contract identity, quote freshness, venue-specific constraints, execution state, and outcome-level exposure while external systems change independently.

That leads to a useful design principle:

Separate discovery data from confirmed execution data, and never let a theoretical calculation silently become an operational claim.

GoldEdge approaches arbitrage as a data and verification workflow: surface candidate price relationships first, then verify the live market before treating them as executable.

Whatever tooling you use, the final calculation should always reflect the position that was actually accepted or matched.

What would you model next: rounding optimization, exchange commission, or a full execution state machine?

Top comments (0)