edgekit.challenge
Prop-firm challenge simulation. A prop challenge is a path problem, not an average-return problem, so the only honest answer is Monte-Carlo over resampled paths. Both entry points take daily_pnl_dollars (P&L after sizing, dollars per day) so the breach limits are absolute dollars.
What's inside. The ChallengeRules dataclass and three presets (FTMO_1STEP, CFT_2PHASE, BRIGHTFUNDED), plus two Monte-Carlo entry points: simulate (the pass rate) and days_to_pass (the distribution of days to clear all phases). Both use a block-bootstrap (not i.i.d.) so losing streaks stay intact and the breach probability is honest.
ChallengeRules (frozen dataclass)#
A prop-firm evaluation's hard constraints, all in account dollars.
ChallengeRules(account: float, phase_targets: list[float], daily_loss: float,
max_loss: float, min_days: int = 4)| Field | Type | Meaning |
|---|---|---|
account | float | Account size in dollars. |
phase_targets | list[float] | Profit target per phase in dollars ([10_000] = single phase; [8_000, 5_000] = two phases). |
daily_loss | float | Positive daily-loss limit in dollars. |
max_loss | float | Positive overall max-loss limit in dollars. |
min_days | int (default 4) | Minimum active days before a phase may be marked passed. |
Presets#
All three are $100k accounts with dollar limits, encoding each firm's published rules.
| Preset | phase_targets | daily_loss | max_loss | min_days |
|---|---|---|---|---|
FTMO_1STEP | [10_000] | 5_000 | 10_000 | 4 |
CFT_2PHASE | [8_000, 5_000] | 5_000 | 10_000 | 4 |
BRIGHTFUNDED | [8_000, 5_000] | 5_000 | 10_000 | 3 |
from edgekit import challenge
challenge.FTMO_1STEP # ChallengeRules(account=100_000, phase_targets=[10_000], daily_loss=5_000, max_loss=10_000, min_days=4)
challenge.CFT_2PHASE # two phases: [8_000, 5_000]
challenge.BRIGHTFUNDED # two phases, min_days=3
# a custom rule set
rules = challenge.ChallengeRules(account=50_000, phase_targets=[4_000], daily_loss=2_500, max_loss=5_000)simulate#
Block-bootstrap pass rate for a sized daily-P&L stream through the rules — the fraction of n Monte-Carlo paths that clear every phase without breaching the daily-loss or max-loss limits.
simulate(daily_pnl_dollars, rules: ChallengeRules, n: int = 12000,
rng=None, block: int = 5, phase_days: int = 400) -> float| Param | Type | Default | Meaning |
|---|---|---|---|
daily_pnl_dollars | pd.Series | — | Sized P&L per day, in dollars (breach limits are absolute). |
rules | ChallengeRules | — | The evaluation's constraints (or a preset). |
n | int | 12000 | Number of Monte-Carlo paths. |
rng | Generator | None | None | Seeded generator for reproducibility. |
block | int | 5 | Block length for the bootstrap (keeps losing streaks intact). |
phase_days | int | 400 | Max days allowed per phase before the path is abandoned. |
Returns a float pass rate in [0, 1].
import numpy as np, pandas as pd
from edgekit import challenge
# sized daily P&L (dollars/day) — e.g. from sizing.size_to_dd(...)["sized"]
idx = pd.date_range("2020-01-01", periods=400, freq="D")
daily_pnl = pd.Series(np.random.default_rng(0).normal(700.0, 200.0, 400), index=idx)
rate = challenge.simulate(daily_pnl, challenge.FTMO_1STEP, n=12000)
print(f"pass rate = {rate:.1%}")days_to_pass#
Distribution of days-to-pass (all phases) over the paths that pass — how long a successful attempt typically takes.
days_to_pass(daily_pnl_dollars, rules: ChallengeRules, n: int = 12000,
rng=None, block: int = 5, phase_days: int = 400) -> dictReturns a dict:
median— median days-to-pass (NaN if nothing passes).p25,p75— 25th / 75th percentile days.mean— mean days-to-pass.pass_rate— fraction of paths that passed.n_pass— count of passing paths.
d = challenge.days_to_pass(daily_pnl, challenge.CFT_2PHASE)
print(d["median"], d["p25"], d["p75"], d["pass_rate"])See also#
- edgekit.sizing — produce the sized dollar stream these functions consume.
- edgekit.portfolio — build the combined book you then size and simulate.
- edgekit.report — turn the pass rate into a Challenge / Live / Realistic report suite.