A funded-account evaluation is a simple game with strict rules. At one large futures prop firm, the $50,000 evaluation asks for $3,000 of profit before the account falls $2,000 below its best close, and it watches every tick. Pass, and the firm gives you a funded account; fail, and you pay for another try. The question traders argue about most is how big to trade, and it has an answer even for a trader with no edge at all.
Probability has an old answer. Lester Dubins and Leonard Savage proved in 1965 that a gambler facing a game tilted against him, who needs to reach a goal before going broke, does best by playing boldly: few, large bets rather than many small ones. Costs tilt every retail trade against the trader, so the evaluation is exactly their game. This notebook works out what the mathematics allows, then replays the firm’s own rules, tick limit and all, on three years of one-minute Nasdaq-100 futures bars, for a trader who trades all day and for one who places a single bold bracket a day.
The answer. For a trader with no edge, trading all day passes at best 10.4% of 500 attempts, at five micros, and less at any larger size. One bracket a day at the full size, 16 points wide, passes 25.8%, two and a half times as often and close to a fair coin under the same rules (28.8%). The sizes that pass best are set by the rules’ arithmetic, not by skill, so a pass says little about an edge.
The Rules
The rules are those of Topstep’s $50K Trading Combine as its help centre stated them on 3 October 2026:
- Profit target: $3,000.
- Maximum Loss Limit: $2,000 below the highest end-of-day balance. It rises with each new high close, never falls, and stops rising once it reaches the $50,000 starting balance. It is checked in real time on open profit and loss: a touch ends the attempt.
- Consistency: the best single day must be at most 55% of the total profit; if it is more, the target rises until it is not. One great day cannot pass on its own.
- Size: at most 5 E-mini Nasdaq-100 contracts or 50 micros.
- Costs: $3.78 a round turn for an E-mini and $1.22 for a micro (exchange, regulatory and commission together).
An E-mini is worth $20 a point and a micro $2, both moving in ticks of a quarter point (CME). Sizes below are counted in micros, so 50 means five E-minis; any ten micros are traded as one E-mini, which is cheaper. The bars are the regular session, 9:30 AM to 4:00 PM Eastern, from a public-domain Kaggle dataset of one-minute Nasdaq-100 futures bars; the 729 complete sessions from 27 December 2022 to 11 December 2025 are saved in the notebook’s data folder, with the shortened holiday sessions left out. The dataset itself stops partway through the evening of 11 December 2025, at a spreadsheet’s limit of 1,048,576 rows.
The Rules and Three Years of Bars
import numpy as np
import pandas as pd
# Topstep $50K Trading Combine, as its help centre stated it on 3 October 2026
START, TARGET, MLL, CONSISTENCY = 50_000, 3_000, 2_000, 0.55
MAX_MICROS = 50 # 5 E-minis or 50 micros
POINT, TICK = 2.0, 0.25 # dollars a point for one micro (an E-mini: $20)
FEE_MINI, FEE_MICRO = 3.78, 1.22 # round turn
def fees(k):
# k micros' worth, as E-minis where possible: ten micros = one cheaper E-mini
return FEE_MINI * (k // 10) + FEE_MICRO * (k % 10)
bars = pd.read_csv("data/nq_rth.csv")
DAYS = bars["session"].unique()
O, H, L, C = (bars[x].to_numpy(float).reshape(-1, 390)
for x in ("open", "high", "low", "close"))
ND = len(DAYS)
print(f"{ND} sessions, {DAYS[0]} to {DAYS[-1]}, 390 one-minute bars each")
print(f"typical one-minute range {np.median(H - L):.2f} points, "
f"typical session range {np.median(H.max(1) - L.min(1)):.2f} points")
print(f"one micro: ${POINT:.0f} a point; the $2,000 limit is "
f"{MLL / (POINT * MAX_MICROS):.0f} points at the full 50 micros")
# 729 sessions, 2022-12-27 to 2025-12-11, 390 one-minute bars each
# typical one-minute range 9.50 points, typical session range 222.25 points
# one micro: $2 a point; the $2,000 limit is 20 points at the full 50 micros
The Ceiling for a Trader With No Edge
Suppose a trader’s account moved like a fair game, with no edge and no costs. Then its expected value never changes, and that holds at the moment the attempt ends too: this is the optional stopping theorem. If the attempt ends with a gain $G$ when it passes and a loss $\ell$ when it fails, then $p\,G = (1-p)\,\ell$, so the pass rate is
With a floor that never moves, the loss is the full $2,000 and the gain the $3,000 target, so a trader with no edge passes 40% of the time, whatever size he trades. A floor that trails the running peak does worse, because the account can fail after climbing. For a continuous account with drift $\mu$ and volatility $\sigma$ per unit of time, the peak it reaches before its first drawdown of $d$ has an exponential distribution, a result of Howard Taylor (1975) and John Lehoczky (1977). Writing $\theta = 2\mu/\sigma^2$, the chance of reaching a gain $a$ before a drawdown $d$ is
and for the floor that never moves, $P_{\text{static}} = (1 – e^{\theta b})/(e^{-\theta a} – e^{\theta b}) \to b/(a+b)$.
Size enters through $\theta$. Trading $k$ times as large multiplies the drift by $k$ and the variance by $k^2$, so $\theta$ falls by a factor $k$. With an edge ($\theta > 0$) a larger size wastes it: the pass rate falls towards the no-edge value. With costs ($\theta < 0$) a larger size dilutes them, and the pass rate rises towards the no-edge value. That is bold play, in Dubins and Savage’s sense, and these formulas are its continuous form. The cell below checks both formulas against a simulation of small fair-ish steps.
Two Formulas Against a Simulation
def static_pass(theta, a=TARGET, b=MLL):
if abs(theta) < 1e-12:
return b / (a + b)
return (1 - np.exp(theta * b)) / (np.exp(-theta * a) - np.exp(theta * b))
def trailing_pass(theta, a=TARGET, d=MLL):
rate = 1 / d if abs(theta) < 1e-12 else theta / np.expm1(theta * d)
return np.exp(-a * rate)
def simulate(mu, sd, trailing, n=20_000, seed=7):
rng = np.random.default_rng(seed)
x, peak = np.zeros(n), np.zeros(n)
alive, passed = np.ones(n, bool), np.zeros(n, bool)
while alive.any():
x[alive] += mu + sd * rng.standard_normal(alive.sum())
peak = np.maximum(peak, x)
floor = (peak - MLL) if trailing else -MLL
won, lost = alive & (x >= TARGET), alive & (x <= floor)
passed |= won
alive &= ~(won | lost)
return passed.mean()
sd = 25.0 # dollars a step: small against the $2,000 limit
print("drift a step theta fixed floor: formula simulated"
" trailing: formula simulated")
for mu in (-0.5, 0.0, 0.5):
th = 2 * mu / sd**2
print(f"{mu:+11.2f} {th:+.5f} {static_pass(th):19.4f}"
f" {simulate(mu, sd, False):9.4f} {trailing_pass(th):17.4f}"
f" {simulate(mu, sd, True):9.4f}")
# drift a step theta fixed floor: formula simulated trailing: formula simulated
# -0.50 -0.00160 0.0079 0.0070 0.0067 0.0066
# +0.00 +0.00000 0.4000 0.3942 0.2231 0.2215
# +0.50 +0.00160 0.9596 0.9573 0.8155 0.8220

Trading All Day on Three Years of Nasdaq Futures
The formulas assume an account that moves smoothly and a floor that trails continuously. The real rules differ in three ways that matter: the floor trails only the end-of-day balance and stops at $50,000; the consistency rule forbids passing on one day; and trades come in lumps, which at a large size can be bigger than the whole loss limit.
So the rules are replayed on the bars. The trader has no edge by construction: he enters at the open of a bar in a direction chosen by a coin, with a stop and a target 25 points away, and enters again on the next bar after each exit, all session, flat by 3:55 PM. A target fills only when the price trades a tick through it; a stop fills a tick worse than its price; if one bar touches both, the stop is taken first. Every attempt starts on one of the first 500 sessions with its own coin, and at every size the same trades are scored, with the account checked against the floor at each trade’s worst open loss.
Brackets, Attempts and the Rules
LAST = 384 # the 3:55 PM bar: flat at its close
def brackets(S):
# an entry at every bar's open, long (+1) and short (-1):
# the exit bar, the P&L in points and the worst open loss in points
out = {}
entry = O[:, :LAST + 1]
for side in (+1, -1):
exit_bar = np.full(entry.shape, LAST)
pnl, worst = np.zeros(entry.shape), np.zeros(entry.shape)
done = np.zeros(entry.shape, bool)
for j in range(LAST + 1):
bar = np.arange(LAST + 1) + j
live = (bar <= LAST) & ~done
b = np.minimum(bar, LAST)
against = (entry - L[:, b]) if side > 0 else (H[:, b] - entry)
towards = (H[:, b] - entry) if side > 0 else (entry - L[:, b])
worst = np.where(live, np.maximum(worst, np.minimum(against, S)),
worst)
stop = live & (against >= S)
target = live & ~stop & (towards >= S + TICK)
pnl = np.where(stop, -S - TICK, np.where(target, S, pnl))
exit_bar = np.where(stop | target, b, exit_bar)
done |= stop | target
at_close = side * (C[:, LAST][:, None] - entry) # still open at 3:55
out[side] = (exit_bar, np.where(done, pnl, at_close), worst)
return out
def all_day(B, first, seed, horizon=229):
# one attempt: a coin for every entry, again on the bar after each exit
coin = np.random.default_rng(seed)
trades = []
for d in range(first, min(ND, first + horizon)):
i = 0
while i <= LAST:
exit_bar, pnl, worst = B[1 if coin.random() < 0.5 else -1]
trades.append((d - first, pnl[d, i], worst[d, i]))
i = exit_bar[d, i] + 1
return np.array(trades)
def one_a_day(B, first, seed, horizon=229):
# one attempt: one bracket a day, at the session's first bar
coin = np.random.default_rng(seed)
trades = []
for d in range(first, min(ND, first + horizon)):
exit_bar, pnl, worst = B[1 if coin.random() < 0.5 else -1]
trades.append((d - first, pnl[d, 0], worst[d, 0]))
return np.array(trades)
def attempt(trades, k):
# the rules at k micros: (1, day) passed, (0, day) failed, (-1, None) open
day = trades[:, 0].astype(int)
pl = POINT * k * trades[:, 1] - fees(k)
worst_open = -POINT * k * trades[:, 2] - fees(k)
after = START + np.cumsum(pl)
before = after - pl
n = day.max() + 1
close = np.full(n, -np.inf)
last = np.r_[np.flatnonzero(np.diff(day)), len(day) - 1]
close[day[last]] = after[last]
best_close = np.maximum.accumulate(close)
trail = np.r_[START, best_close[:-1]] - MLL
floor = np.minimum(START, np.maximum(START - MLL, trail))
breach = np.flatnonzero(before + worst_open <= floor[day])
fail_day = day[breach[0]] if breach.size else None
daily = np.bincount(day, weights=pl, minlength=n)
total, best = np.cumsum(daily), np.maximum.accumulate(daily)
ok = np.flatnonzero(total >= np.maximum(TARGET, best / CONSISTENCY))
pass_day = ok[0] if ok.size else None
if pass_day is not None and (fail_day is None or pass_day < fail_day):
return 1, int(pass_day)
return (0, int(fail_day)) if fail_day is not None else (-1, None)
B25 = brackets(25.0)
FIRST = range(500)
attempts = [all_day(B25, s, seed=s) for s in FIRST]
pts = np.concatenate([a[:, 1] for a in attempts])
per_session = np.mean([len(a) / (a[-1, 0] + 1) for a in attempts])
print(f"{len(FIRST)} attempts, {sum(len(a) for a in attempts):,} trades, "
f"{per_session:.1f} a session")
print(f"a trade averages {pts.mean():+.3f} points (sd {pts.std():.2f}); "
f"{np.mean(pts > 0):.1%} are winners")
# 500 attempts, 4,523,644 trades, 39.5 a session
# a trade averages -0.431 points (sd 24.91); 49.4% are winners
All Day, at Every Size
SIZES = [1, 2, 3, 5, 8, 10, 15, 20, 30, 40, 50]
allday = {}
print("micros passed failed median sessions to pass")
for k in SIZES:
r = [attempt(a, k) for a in attempts]
res = np.array([x[0] for x in r])
days = [x[1] + 1 for x in r if x[0] == 1]
allday[k] = res
med = np.median(days) if days else float("nan")
print(f"{k:6d} {np.mean(res == 1):6.1%} {np.mean(res == 0):6.1%} "
f"{med:10.0f}")
# micros passed failed median sessions to pass
# 1 0.6% 99.4% 60
# 2 5.6% 94.4% 18
# 3 8.8% 91.2% 9
# 5 10.4% 89.6% 6
# 8 8.4% 91.6% 5
# 10 8.2% 91.8% 4
# 15 5.4% 94.6% 4
# 20 5.4% 94.6% 4
# 30 3.4% 96.6% 4
# 40 2.8% 97.2% 4
# 50 2.4% 97.6% 4

One Bold Bracket a Day
Bold play means few bets, so the second trader places one bracket a day, at the session’s first bar, at the full 50 micros, and lets it run to its stop, its target or 3:55 PM. The only choice left is the width of the bracket, which sets the stake: at five E-minis every point is $100.
The rules do the rest of the arithmetic. Two winning days are the fewest that can pass, since each must then be half of the total, under the 55% limit. They pass only if two wins clear $3,000 after costs, which takes a bracket of at least 16 points: two 15-point wins come to $2,977, and a third win is needed. A bracket of 20 points would risk the whole $2,000 limit on the first trade. For each width the replay is set beside a fair coin, an account that moves like a Brownian motion with no edge inside the same bracket, under the same rules and costs, and beside the optional stopping ceiling for that width. The coin must move continuously: a winning trade can first go against the account, and once a losing day has brought the floor within a few points, that dip is enough to end the attempt.
One a Day, at Every Bracket
def fair_coin(S, k, rng, horizon=229):
# the same bracket and rules for an account that moves like a Brownian motion
# with no edge: inside a trade it reaches the target (a tick beyond S) before
# the stop or the loss limit, whichever is nearer, at the gambler's-ruin odds
balance, best_close, best_day, total = START, START, 0.0, 0.0
for _ in range(horizon):
floor = min(START, max(START - MLL, best_close - MLL))
room = (balance - fees(k) - floor) / (POINT * k) # points it may fall
down = min(S, room)
if rng.random() < down / (down + S + TICK):
day = POINT * k * S - fees(k)
elif room <= S:
return False # the loss limit, touched in real time
else:
day = -POINT * k * (S + TICK) - fees(k)
balance += day
total += day
best_close, best_day = max(best_close, balance), max(best_day, day)
if total >= max(TARGET, best_day / CONSISTENCY):
return True
return False
WIDTHS = list(range(8, 21))
daily, coin_rate, ceiling = {}, {}, {}
print("bracket stake win 2 wins passed fair coin ceiling")
for S in WIDTHS:
B = brackets(float(S))
res = np.array([attempt(one_a_day(B, f, seed=10_000 + f), MAX_MICROS)[0]
for f in FIRST])
daily[S] = res
win = POINT * MAX_MICROS * S - fees(MAX_MICROS)
rng = np.random.default_rng(S)
coin_rate[S] = np.mean([fair_coin(S, MAX_MICROS, rng)
for _ in range(20_000)])
n_wins = max(2, int(np.ceil(TARGET / win))) # consistency: two days at least
ceiling[S] = MLL / (n_wins * win + MLL)
stake = POINT * MAX_MICROS * S
print(f"{S:5d} pt ${stake:5.0f} ${win:7.2f} ${2 * win:8.2f}"
f" {np.mean(res == 1):6.1%} {coin_rate[S]:6.1%}"
f" {ceiling[S]:6.1%}")
print()
for S in (8, 12, 16): # the first minute, where these trades start
both = np.mean((H[:, 0] - O[:, 0] >= S + TICK) & (O[:, 0] - L[:, 0] >= S))
print(f"the first minute reaches both sides of a bracket {S} points wide "
f"on {both:.1%} of sessions")
# bracket stake win 2 wins passed fair coin ceiling
# 8 pt $ 800 $ 781.10 $ 1562.20 3.6% 20.3% 39.0%
# 9 pt $ 900 $ 881.10 $ 1762.20 7.4% 21.6% 36.2%
# 10 pt $ 1000 $ 981.10 $ 1962.20 8.8% 19.8% 33.8%
# 11 pt $ 1100 $1081.10 $ 2162.20 17.2% 25.3% 38.1%
# 12 pt $ 1200 $1181.10 $ 2362.20 16.4% 24.2% 36.1%
# 13 pt $ 1300 $1281.10 $ 2562.20 16.4% 22.9% 34.2%
# 14 pt $ 1400 $1381.10 $ 2762.20 15.8% 21.6% 32.6%
# 15 pt $ 1500 $1481.10 $ 2962.20 16.2% 20.2% 31.0%
# 16 pt $ 1600 $1581.10 $ 3162.20 25.8% 28.8% 38.7%
# 17 pt $ 1700 $1681.10 $ 3362.20 24.4% 28.7% 37.3%
# 18 pt $ 1800 $1781.10 $ 3562.20 24.4% 27.1% 36.0%
# 19 pt $ 1900 $1881.10 $ 3762.20 22.4% 24.9% 34.7%
# 20 pt $ 2000 $1981.10 $ 3962.20 22.4% 24.4% 33.5%
#
# the first minute reaches both sides of a bracket 8 points wide on 26.5% of sessions
# the first minute reaches both sides of a bracket 12 points wide on 9.7% of sessions
# the first minute reaches both sides of a bracket 16 points wide on 3.4% of sessions

What the Evaluation Rewards
The Findings
the case passes from no edge, a floor that never moves 40.0% optional stopping no edge, a floor that trails every tick 22.3% Taylor and Lehoczky all day, best size (5 micros) 10.4% 500 attempts on the bars all day, full size (50 micros) 2.4% 500 attempts on the bars one bracket a day, 16 points at 50 micros 25.8% 500 attempts on the bars a fair coin, same bracket and rules 28.8% 20,000 simulated attempts
- Bold beats busy. One bracket a day passes two and a half times as often as trading all day at its best size, because costs and bad luck grow with the number of bets. That is Dubins and Savage’s bold play.
- Trading all day, bigger is worse. Up to five micros the pass rate rises, as the formulas say. Beyond that each 25-point trade is a large lump of the $2,000 limit and the consistency rule keeps the account trading after its best days, so 50 micros passes only 2.4%.
- The rules’ arithmetic picks the winning sizes. From 11 points three winning days clear the target after costs, and from 16 points two, so the pass rate jumps at both widths. Narrower brackets fail at the open: the first minute alone reaches both sides of an 8-point bracket on 26.5% of sessions.
- The firm’s floor is gentler than the formula’s. It trails only the day’s close and stops at $50,000, which is why a bold daily bracket can beat the 22.3% of a floor that trails every tick; its own ceiling, from optional stopping, is 38.7%.
- A pass is weak evidence of skill. A fair coin passes 28.8% of the time at the right size. A trader who does have an edge should do the opposite of the bold player and trade small and often, where the formulas show an edge doing the most for the pass rate.
Sources
- L. E. Dubins and L. J. Savage, How to Gamble If You Must: Inequalities for Stochastic Processes, McGraw-Hill, New York, 1965.
- H. M. Taylor, “A stopped Brownian motion formula”, The Annals of Probability 3 (1975) 234–246.
- J. P. Lehoczky, “Formulas for stopped diffusion processes with stopping times based on the maximum”, The Annals of Probability 5 (1977) 601–607.
- D. Williams, Probability with Martingales, Cambridge University Press, Cambridge, 1991 (the optional stopping theorem).
- Topstep help centre, read 3 October 2026: “What is the Maximum Loss Limit?”, “Consistency at Topstep”, “Trading Combine® Parameters” and “TopstepX™ — Commissions and Fees” (help.topstep.com).
- CME Group, contract specifications of the E-mini and Micro E-mini Nasdaq-100 futures (cmegroup.com).
- TGT Analytics, “NQ Futures – 1min Bar 2022 2025”, Kaggle dataset tgtanalytics/nq-futures-1min-bar-2022-2025, CC0: Public Domain.
Working on a pricing model or risk system? Let’s talk.