How to win (a little) and lose (a lot) at European roulette.

In this notebook I implement a Monte Carlo simulation applied to European roulette.

European roulette has 37 slots: the numbers from 0 to 36. We consider a fixed bet on red.

At each spin, the economic result can be represented by a random variable X.

If red comes up, the player wins one unit:

X = +1

If red does not come up, the player loses one unit:

X = -1

In European roulette there are 18 red numbers, 18 black numbers, and zero. The probability of winning by betting on red is therefore:

P(X = +1) = \frac{18}{37}

The probability of losing is:

P(X = -1) = \frac{19}{37}

The value 19 includes the 18 black numbers plus zero.

The theoretical average gain of one spin is:

E[X] = (+1)\frac{18}{37} + (-1)\frac{19}{37}

therefore:

E[X] = \frac{18}{37} - \frac{19}{37}

that is:

E[X] = -\frac{1}{37}

Numerically:

-\frac{1}{37} \approx -0.027

The expected value is negative. If many spins are repeated, the average of the simulated results tends toward this value.

The Monte Carlo simulation consists of generating many random spins and computing the average gain obtained.

If X_i is the result of spin number i, after N spins the total gain is:

S_N = \sum_{i=1}^{N} X_i

The average gain per spin is:

\overline{X}_N = \frac{S_N}{N}

For a large number of spins, we expect:

\overline{X}_N \approx E[X]

In this case:

\overline{X}_N \approx -\frac{1}{37}

By simulating many independent players, each with the same number of spins, we obtain a distribution of final gains.

In the final part I compare this distribution with a Gaussian distribution generated using the Box-Muller transform.

roulette_wheel.png

Implementing Box-Muller

import numpy as np

def box_muller(n, seed=None):
  rng=np.random.default_rng(seed)
  u1=rng.random(n)
  u2=rng.random(n)
  r=np.sqrt(-2.0*np.log(u1))
  theta=2.0*np.pi*u2
  return r*np.cos(theta)

Roulette parameters

p_win = 18 / 37
p_loss = 19 / 37

expected_value = -1 / 37

print("Win probability =", p_win)
print("Loss probability =", p_loss)
print("Theoretical expected value =", expected_value)
Win probability = 0.4864864864864865
Loss probability = 0.5135135135135135
Theoretical expected value = -0.02702702702702703

Monte Carlo for European roulette

def monte_carlo_roulette(n_spins, seed=None):
    rng = np.random.default_rng(seed)

    results = rng.choice(
        [1, -1],
        size=n_spins,
        p=[p_win, p_loss]
    )

    cumulative_gain = np.cumsum(results)
    running_mean = cumulative_gain / np.arange(1, n_spins + 1)

    return results, cumulative_gain, running_mean

Simulating n spins of roulette

n_spins = 200

results, cumulative_gain, running_mean = monte_carlo_roulette(
    n_spins,
    seed=1
)


expected_final_gain = n_spins * expected_value
difference = cumulative_gain[-1] - expected_final_gain

print("Final gain =", cumulative_gain[-1])
print("Simulated average gain =", running_mean[-1])
print("Theoretical expected value =", expected_value)
print("Theoretical average final gain =", expected_final_gain)
print("Difference =", difference)
Final gain = -4
Simulated average gain = -0.02
Theoretical expected value = -0.02702702702702703
Theoretical average final gain = -5.405405405405405
Difference = 1.4054054054054053

Evolution of the simulated average gain

import numpy as np
import matplotlib.pyplot as plt

plt.figure(figsize=(9, 5))

plt.plot(running_mean, label="Simulated average")
plt.axhline(expected_value, linestyle="--", label="Theoretical expected value")

plt.xlabel("Number of spins")
plt.ylabel("Average gain per spin")
plt.title("Monte Carlo convergence of the average gain")
plt.grid(True)
plt.legend()
plt.show()
plot

Gaussian approximation of final gains

Consider many independent players.

Each player makes the same number of spins N, always betting on red.

The final gain of a single player is:

S_N = \sum_{i=1}^{N} X_i

where each X_i can be either +1 or -1.

For a single spin we have:

E[X] = -\frac{1}{37}

Since X only takes the values +1 and -1, we have:

X^2 = 1

therefore:

E[X^2] = 1

The variance of a single spin is:

Var(X) = E[X^2] - (E[X])^2

that is:

Var(X) = 1 - \left(-\frac{1}{37}\right)^2

For the sum of N independent spins:

E[S_N] = N E[X]

and:

\sigma_{S_N} = \sqrt{N Var(X)}

For a large number of spins, the distribution of final gains tends to have an approximately Gaussian shape.

We then use the Box-Muller transform to generate standard Gaussian numbers and convert them into Gaussian gains with the same theoretical mean and theoretical standard deviation as the roulette gains.

n_players = 10000
n_spins = 1000

rng = np.random.default_rng(2)

outcomes_many = rng.choice(
    [1, -1],
    size=(n_players, n_spins),
    p=[18/37, 19/37]
)

final_gains = np.sum(outcomes_many, axis=1)

expected_value = -1 / 37

var_single = 1 - expected_value**2

mu_total = n_spins * expected_value
sigma_total = np.sqrt(n_spins * var_single)

print("Theoretical total mean =", mu_total)
print("Theoretical total standard deviation =", sigma_total)
print("Simulated average =", np.mean(final_gains))
print("Simulated standard deviation =", np.std(final_gains))
Theoretical total mean = -27.027027027027028
Theoretical total standard deviation = 31.611224902083126
Simulated average = -27.384
Simulated standard deviation = 31.153332791211923
z = box_muller(n_players, seed=3)

gaussian_gains = mu_total + sigma_total * z
plt.figure(figsize=(9, 5))

plt.hist(
    final_gains,
    bins=50,
    density=True,
    alpha=0.6,
    label="Monte Carlo roulette"
)

plt.hist(
    gaussian_gains,
    bins=50,
    density=True,
    alpha=0.5,
    label="Box-Muller Gaussian"
)

plt.axvline(mu_total, linestyle="--", label="Theoretical mean")

plt.xlabel("Final gain")
plt.ylabel("Density")
plt.title("Final gains and Gaussian approximation")
plt.grid(True)
plt.legend()
plt.show()
plot

The histogram of final gains obtained with the Monte Carlo simulation has a shape similar to a Gaussian distribution.

The Gaussian distribution was generated with the Box-Muller transform and then rescaled using the theoretical mean and theoretical standard deviation of the total gain.

The similarity between the two histograms comes from the fact that the final gain is the sum of many independent random variables.

Roulette remains unfavorable to the player: the distribution is centered on a negative average value.

I WANT TO WIN!!!

Now we invent a clever strategy to win at roulette.

Strategy with stop-win and stop-loss

Consider a playing session with a fixed initial capital.

The player always bets one unit on red. After each spin, the capital is updated according to the result:

C_{k+1} = C_k + X_k

where C_k is the capital after k spins and X_k is the result of the spin.

For a bet on red in European roulette:

X_k = \begin{cases} +1 & \text{with probability } \frac{18}{37} \\ -1 & \text{with probability } \frac{19}{37} \end{cases}

Define three parameters of the session:

C_0 = 100

initial capital,

C_{\text{win}} = 110

exit threshold in profit,

C_{\text{loss}} = 80

exit threshold in loss.

The session ends when one of the following conditions occurs:

C_k \ge C_{\text{win}}

or:

C_k \le C_{\text{loss}}

or when the maximum number of spins is reached.

This strategy changes how the risk is distributed. Wins are locked when the capital reaches the upper threshold, while losses are limited by the lower threshold.

The expected value of a single bet remains:

E[X] = -\frac{1}{37}

For this reason, if a session contains many spins on average, the expected average gain remains linked to the total amount wagered.

Simulation of a single session

We simulate one complete session.

At each step, the result of one spin is generated. The capital is updated and stored in an array.

The resulting array represents the capital during the session:

C_0, C_1, C_2, \dots, C_m

where m is the actual number of spins played before the session stops.

def roulette_session(
    initial_capital=100,
    base_bet=1,
    stop_win=110,
    stop_loss=80,
    max_spins=1000,
    seed=None
):
    rng = np.random.default_rng(seed)

    capital = initial_capital
    capital_history = [capital]

    for _ in range(max_spins):
        result = rng.choice(
            [1, -1],
            p=[18/37, 19/37]
        )

        capital += base_bet * result
        capital_history.append(capital)

        if capital >= stop_win:
            break

        if capital <= stop_loss:
            break

    return np.array(capital_history)
capital_history = roulette_session(seed=1)

initial_capital = 100
stop_win = 110
stop_loss = 80

print("Final capital =", capital_history[-1])
print("Number of spins =", len(capital_history) - 1)
print("Profit =", capital_history[-1] - initial_capital)
Final capital = 110
Number of spins = 400
Profit = 10
plt.figure(figsize=(9, 5))

plt.plot(capital_history, label="Capital")

plt.axhline(initial_capital, linestyle="--", label="Initial capital")
plt.axhline(stop_win, linestyle="--", label="Stop-win")
plt.axhline(stop_loss, linestyle="--", label="Stop-loss")

plt.xlabel("Number of spins")
plt.ylabel("Capital")
plt.title("Roulette session with stop-win and stop-loss")
plt.grid(True)
plt.legend()
plt.show()
plot

The figure shows the capital during a single session.

The session ends when the capital reaches the upper threshold, the lower threshold, or the maximum number of spins. It looks like a strategy that might at least be winning, but a single session is not enough to evaluate the strategy because the result depends on the particular random sequence generated.

We put it under pressure using Monte Carlo and simulate many sessions.

n_sessions = 2000

final_capitals = np.zeros(n_sessions)
n_spins_used = np.zeros(n_sessions)

for i in range(n_sessions):
    history = roulette_session(seed=i)

    final_capitals[i] = history[-1]
    n_spins_used[i] = len(history) - 1

profits = final_capitals - initial_capital

print("Number of sessions =", n_sessions)
print("Average profit =", np.mean(profits))
print("Median profit =", np.median(profits))
print("Probability of closing in profit =", np.mean(profits > 0))
print("Probability of closing in loss =", np.mean(profits < 0))
print("Average number of spins =", np.mean(n_spins_used))
Number of sessions = 2000
Average profit = -6.072
Median profit = -20.0
Probability of closing in profit = 0.464
Probability of closing in loss = 0.536
Average number of spins = 200.492

Interpretation of the results

The simulation was repeated over many independent sessions.

The results obtained are:

  • negative average profit;
  • median profit equal to the loss threshold;
  • probability of closing in loss greater than the probability of closing in profit.

In the simulated case, the player’s average profit is about:

-6.072

This means that, under the same conditions, the Casino’s average profit is about:

+6.072

per session.

The stop-win / stop-loss strategy can produce many sessions that look favorable, because some of them end with a small profit. However, when the session ends in loss, the loss is larger than the gain fixed by the stop-win.

In this example:

C_{\text{win}} - C_0 = 110 - 100 = 10

while:

C_{\text{loss}} - C_0 = 80 - 100 = -20

A winning session therefore gives a gain of 10 units, while a losing session gives a loss of 20 units.

The average result remains negative because European roulette gives the bet on red the expected value:

E[X] = -\frac{1}{37}

Each unit wagered loses about 2.7% on average.

The average amount lost by the player is therefore linked to the average number of spins played:

E[\text{profit}] \approx -\frac{\text{average number of spins}}{37}

In this case the average number of spins is about:

200.492

so the expected loss is about:

-\frac{200.492}{37} \approx -5.42

The simulated value, about -6.07, is compatible with this estimate, considering the statistical fluctuations due to the finite number of simulated sessions.

Over many players and many sessions, the fluctuations tend to compensate for each other. The total profit of the Casino grows with the total volume wagered.

I WANT TO BEAT THE CASINO!

Martingale strategy

We now study a more aggressive strategy: the martingale.

The player always bets on red. The initial bet is one unit.

After a win, the bet returns to the initial value. After a loss, the next bet is doubled.

If B_k is the bet at spin k, the rule is:

B_{k+1} = B_0

after a win, while:

B_{k+1} = 2B_k

after a loss.

The idea of the strategy is to recover all previous losses at the first useful win.

For example, after three consecutive losses:

-1 -2 -4 = -7

the next bet is:

8

If this spin wins, the balance of the sequence becomes:

-1 -2 -4 + 8 = +1

The strategy produces many sessions closed with a small profit. The risk is concentrated in long loss sequences, where the bet grows very quickly.

In the simulation we introduce two realistic constraints:

  • limited available capital;
  • maximum bet allowed at the table.

When the player can no longer cover the required bet, the session ends.

def martingale_session(
    initial_capital=255,
    base_bet=1,
    target_profit=1,
    table_limit=128,
    max_spins=1000,
    seed=None
):
    rng = np.random.default_rng(seed)

    capital = initial_capital
    bet = base_bet

    capital_history = [capital]
    bet_history = []

    for _ in range(max_spins):

        # If the required bet exceeds the capital or the table limit, the session ends
        if bet > capital:
            break

        if bet > table_limit:
            break

        result = rng.choice(
            [1, -1],
            p=[18/37, 19/37]
        )

        bet_history.append(bet)

        if result == 1:
            capital += bet
            bet = base_bet
        else:
            capital -= bet
            bet = 2 * bet

        capital_history.append(capital)

        # Target: chiudere appena si è guadagnata una unità
        if capital >= initial_capital + target_profit:
            break

    return np.array(capital_history), np.array(bet_history)
capital_history, bet_history = martingale_session(seed=1)

print("Final capital =", capital_history[-1])
print("Number of spins =", len(capital_history) - 1)
print("Profit =", capital_history[-1] - 255)
Final capital = 256
Number of spins = 3
Profit = 1
plt.figure(figsize=(9, 5))

plt.plot(capital_history)

plt.axhline(255, linestyle="-", color="red", label="Initial capital")
plt.axhline(256, linestyle="--", color="green", label="Target")

plt.xlabel("Number of spins")
plt.ylabel("Capital")
plt.title("Session with martingale strategy")
plt.grid(True)
plt.legend()
plt.show()
plot
n_sessions = 10000

final_capitals = np.zeros(n_sessions)
n_spins_used = np.zeros(n_sessions)

initial_capital = 255

for i in range(n_sessions):
    history, bets = martingale_session(seed=i)

    final_capitals[i] = history[-1]
    n_spins_used[i] = len(history) - 1

profits = final_capitals - initial_capital

print("Number of sessions =", n_sessions)
print("Average profit =", np.mean(profits))
print("Median profit =", np.median(profits))
print("Probability of closing in profit =", np.mean(profits > 0))
print("Probability of closing in loss =", np.mean(profits < 0))
print("Average number of spins =", np.mean(n_spins_used))
print("Maximum loss =", np.min(profits))
Number of sessions = 10000
Average profit = -0.3056
Median profit = 1.0
Probability of closing in profit = 0.9949
Probability of closing in loss = 0.0051
Average number of spins = 2.0484
Maximum loss = -255.0
wins = np.sum(profits > 0)
losses = np.sum(profits < 0)

labels = ["Winning sessions\nprofit +1", "Losing sessions\nprofit -255"]
counts = [wins, losses]

plt.figure(figsize=(7, 5))

plt.bar(labels, counts)

plt.ylabel("Number of sessions")
plt.title("Session outcomes with martingale strategy")
plt.grid(axis="y")

for i, value in enumerate(counts):
    percentage = 100 * value / len(profits)
    plt.text(
        i,
        value,
        f"{value}\n{percentage:.2f}%",
        ha="center",
        va="bottom"
    )

plt.show()
plot
average_profit = np.mean(profits)

print("Winning sessions =", wins)
print("Losing sessions =", losses)
print("Win probability =", wins / len(profits))
print("Loss probability =", losses / len(profits))
print("Average profit =", average_profit)
Winning sessions = 9949
Losing sessions = 51
Win probability = 0.9949
Loss probability = 0.0051
Average profit = -0.3056

The martingale produces many winning sessions, but each winning session earns only one unit.

Losing sessions are much rarer, but when they occur they produce a very large loss.

The graph therefore shows two main outcomes:

  • many small wins;
  • few catastrophic losses.

The average profit depends on the balance between these two effects. Even if winning sessions are much more frequent, rare losses can compensate for and exceed the accumulated wins.

D’Alembert strategy

We now examine a less aggressive strategy than the martingale: the D’Alembert system.

The player always bets on red.

The initial bet is one unit.

After a loss, the bet is increased by one unit:

B_{k+1} = B_k + 1

After a win, the bet is decreased by one unit:

B_{k+1} = B_k - 1

The bet cannot fall below the base bet.

Compared with the martingale, the growth of the bet is linear and not exponential.

The strategy tries to recover losses gradually, without producing very rapid doublings. Capital is limited, the maximum table bet cannot be exceeded, and there is a maximum number of spins.

def dalembert_session(
    initial_capital=100,
    base_bet=1,
    table_limit=50,
    max_spins=1000,
    seed=None
):
    rng = np.random.default_rng(seed)

    capital = initial_capital
    bet = base_bet

    capital_history = [capital]
    bet_history = []

    for _ in range(max_spins):

        if bet > capital:
            break

        if bet > table_limit:
            break

        result = rng.choice(
            [1, -1],
            p=[18/37, 19/37]
        )

        bet_history.append(bet)

        if result == 1:
            capital += bet
            bet = max(base_bet, bet - base_bet)
        else:
            capital -= bet
            bet = bet + base_bet

        capital_history.append(capital)

        if capital <= 0:
            break

    return np.array(capital_history), np.array(bet_history)
capital_history, bet_history = dalembert_session(seed=1)

print("Final capital =", capital_history[-1])
print("Number of spins =", len(capital_history) - 1)
print("Profit =", capital_history[-1] - 100)
print("Maximum bet used =", np.max(bet_history))
Final capital = 10
Number of spins = 70
Profit = -90
Maximum bet used = 15
plt.figure(figsize=(9, 5))

plt.plot(capital_history)

plt.axhline(100, linestyle="--", label="Initial capital")

plt.xlabel("Number of spins")
plt.ylabel("Capital")
plt.title("Session with D'Alembert strategy")
plt.grid(True)
plt.legend()
plt.show()
plot
n_sessions = 1000

initial_capital = 100

final_capitals = np.zeros(n_sessions)
n_spins_used = np.zeros(n_sessions)
max_bets_used = np.zeros(n_sessions)

for i in range(n_sessions):
    history, bets = dalembert_session(seed=i)

    final_capitals[i] = history[-1]
    n_spins_used[i] = len(history) - 1

    if len(bets) > 0:
        max_bets_used[i] = np.max(bets)

profits = final_capitals - initial_capital

print("Number of sessions =", n_sessions)
print("Average profit =", np.mean(profits))
print("Median profit =", np.median(profits))
print("Probability of closing in profit =", np.mean(profits > 0))
print("Probability of closing in loss =", np.mean(profits < 0))
print("Average number of spins =", np.mean(n_spins_used))
print("Average maximum bet =", np.mean(max_bets_used))
print("Maximum loss =", np.min(profits))
print("Maximum gain =", np.max(profits))
Number of sessions = 1000
Average profit = -65.597
Median profit = -89.0
Probability of closing in profit = 0.053
Probability of closing in loss = 0.946
Average number of spins = 259.557
Average maximum bet = 18.995
Maximum loss = -100.0
Maximum gain = 536.0
wins = np.sum(profits > 0)
losses = np.sum(profits < 0)
evens = np.sum(profits == 0)

labels = ["Positive", "Negative", "Break-even"]
counts = [wins, losses, evens]

plt.figure(figsize=(7, 5))

plt.bar(labels, counts)

plt.ylabel("Number of sessions")
plt.title("Session outcomes with D'Alembert strategy")
plt.grid(axis="y")

for i, value in enumerate(counts):
    percentage = 100 * value / len(profits)
    plt.text(
        i,
        value,
        f"{value}\n{percentage:.2f}%",
        ha="center",
        va="bottom"
    )

plt.show()
plot
plt.figure(figsize=(9, 5))

plt.hist(profits, bins=40, edgecolor="black")

plt.axvline(0, linestyle="-", color="orange", label="Break-even")
plt.axvline(np.mean(profits), linestyle=":", color="green", label="Average profit")
plt.axvline(np.median(profits), linestyle="--", label="Median profit")

plt.xlabel("Final profit")
plt.ylabel("Number of sessions")
plt.title("Profit distribution with D'Alembert strategy")
plt.grid(True)
plt.legend()

plt.show()
plot

The graphs show the behavior of the D’Alembert strategy over many independent sessions.

The strategy changes the bet progressively: after a loss it increases it by one unit, after a win it decreases it by one unit.

Compared with the martingale, the growth of the bet is slower. This reduces the risk of a sudden very large loss, but it does not remove the statistical disadvantage of European roulette.

The profit distribution shows that the results can vary a lot from one session to another. The position of the average profit allows us to evaluate the overall behavior of the strategy over many repetitions.

And now an “esoteric” strategy: Fibonacci

We now study a strategy based on the Fibonacci sequence. Why? Because someone decided that using esoteric number sequences could be a good idea, potentially useful for winning at roulette. This is how it works.

The player always bets on red.

The sequence of bets is built using the Fibonacci numbers:

1, 1, 2, 3, 5, 8, 13, 21, \dots

After a loss, the player moves forward by one position in the sequence.

After a win, the player moves back by two positions.

The rule can be described as:

i_{k+1} = i_k + 1

after a loss, while:

i_{k+1} = \max(0, i_k - 2)

after a win.

The bet at spin k is:

B_k = B_0 F_{i_k}

where B_0 is the base bet and F_{i_k} is the Fibonacci number corresponding to the current index.

Compared with the martingale, the bet grows less rapidly. The strategy tries to recover losses more gradually.

def fibonacci_sequence(n):
    fib = [1, 1]

    for _ in range(2, n):
        fib.append(fib[-1] + fib[-2])

    return np.array(fib)
def fibonacci_session(
    initial_capital=100,
    base_bet=1,
    table_limit=50,
    max_spins=1000,
    seed=None
):
    rng = np.random.default_rng(seed)

    fib = fibonacci_sequence(30)

    capital = initial_capital
    fib_index = 0

    capital_history = [capital]
    bet_history = []
    index_history = []

    for _ in range(max_spins):

        bet = base_bet * fib[fib_index]

        if bet > capital:
            break

        if bet > table_limit:
            break

        result = rng.choice(
            [1, -1],
            p=[18/37, 19/37]
        )

        bet_history.append(bet)
        index_history.append(fib_index)

        if result == 1:
            capital += bet
            fib_index = max(0, fib_index - 2)
        else:
            capital -= bet
            fib_index += 1

            if fib_index >= len(fib):
                break

        capital_history.append(capital)

        if capital <= 0:
            break

    return (
        np.array(capital_history),
        np.array(bet_history),
        np.array(index_history)
    )
initial_capital = 100

capital_history, bet_history, index_history = fibonacci_session(seed=1)

print("Final capital =", capital_history[-1])
print("Number of spins =", len(capital_history) - 1)
print("Profit =", capital_history[-1] - initial_capital)

if len(bet_history) > 0:
    print("Maximum bet used =", np.max(bet_history))
Final capital = 38
Number of spins = 140
Profit = -62
Maximum bet used = 34
plt.figure(figsize=(9, 5))

plt.plot(capital_history)

plt.axhline(initial_capital, linestyle="--", color="orange", label="Initial capital")

plt.xlabel("Number of spins")
plt.ylabel("Capital")
plt.title("Session with Fibonacci strategy")
plt.grid(True)
plt.legend()
plt.show()
plot
n_sessions = 1000

final_capitals = np.zeros(n_sessions)
n_spins_used = np.zeros(n_sessions)
max_bets_used = np.zeros(n_sessions)

for i in range(n_sessions):
    history, bets, indexes = fibonacci_session(seed=i)

    final_capitals[i] = history[-1]
    n_spins_used[i] = len(history) - 1

    if len(bets) > 0:
        max_bets_used[i] = np.max(bets)

profits = final_capitals - initial_capital

print("Number of sessions =", n_sessions)
print("Average profit =", np.mean(profits))
print("Median profit =", np.median(profits))
print("Probability of closing in profit =", np.mean(profits > 0))
print("Probability of closing in loss =", np.mean(profits < 0))
print("Probability of closing at break-even =", np.mean(profits == 0))
print("Average number of spins =", np.mean(n_spins_used))
print("Average maximum bet =", np.mean(max_bets_used))
print("Maximum loss =", np.min(profits))
print("Maximum gain =", np.max(profits))
Number of sessions = 1000
Average profit = -24.309
Median profit = -44.5
Probability of closing in profit = 0.242
Probability of closing in loss = 0.753
Probability of closing at break-even = 0.005
Average number of spins = 323.368
Average maximum bet = 33.883
Maximum loss = -88.0
Maximum gain = 237.0
wins = np.sum(profits > 0)
losses = np.sum(profits < 0)
evens = np.sum(profits == 0)

labels = ["Positive", "Negative", "Break-even"]
counts = [wins, losses, evens]

plt.figure(figsize=(7, 5))

plt.bar(labels, counts)

plt.ylabel("Number of sessions")
plt.title("Session outcomes with Fibonacci strategy")
plt.grid(axis="y")

for i, value in enumerate(counts):
    percentage = 100 * value / len(profits)
    plt.text(
        i,
        value,
        f"{value}\n{percentage:.2f}%",
        ha="center",
        va="bottom"
    )

plt.show()
plot

Conclusions

The simulations show that European roulette is unfavorable to the player already at the level of a single bet.

When betting on red, the result of one spin is:

X = +1

with probability:

\frac{18}{37}

or:

X = -1

with probability:

\frac{19}{37}

The expected value is:

E[X] = (+1)\frac{18}{37} + (-1)\frac{19}{37}

therefore:

E[X] = -\frac{1}{37}

Each unit wagered loses on average about:

\frac{1}{37} \approx 0.027

that is, about 2.7%.

The strategies analyzed do not change this basic probability. They only change how wins and losses are distributed across individual sessions.

The stop-win / stop-loss strategy can produce many sessions closed in profit, but negative sessions have larger losses. The martingale further increases the probability of closing a session with a small profit, but concentrates the risk into a few very large losses. The D’Alembert strategy makes the growth of the bet slower, but the average profit remains conditioned by the statistical disadvantage of roulette.

The central point is that the expected gain of the player depends on the total amount wagered:

E[\text{profit}] \approx -\frac{\text{total amount wagered}}{37}

For the Casino the sign is reversed:

E[\text{Casino profit}] \approx +\frac{\text{total amount wagered}}{37}

In a single session the player can win or lose because of randomness. Over many independent sessions, the fluctuations tend to compensate for each other and the average value converges toward the theoretical expected value.

The Monte Carlo simulation makes this behavior visible: a strategy may look effective after observing only a few sessions, but when it is repeated many times it shows its real average value.

The Casino’s advantage does not come from all players using the same strategy. It comes from the negative expected value of every bet. Different strategies produce different distributions of results, but they do not turn an unfavorable game into a favorable one.

In conclusion, European roulette can be won in a single session, but it cannot be made favorable in the long run using only a betting-management strategy.