preview
Machine Learning Under Constraint (Part 1): A Configurable Rule Set for Prop-Firm Position Sizing

Machine Learning Under Constraint (Part 1): A Configurable Rule Set for Prop-Firm Position Sizing

MetaTrader 5Statistics and analysis |
127 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Table of Contents

  1. Introduction
  2. Why a Constrained Optimization Problem
  3. What Part 11 Hardcoded
  4. A Configurable Rule Set (PropFirmRuleSet)
  5. Refactoring PropFirmAccountState
  6. Refactoring the Sizing Modifiers
  7. Validation: Parity Against the Original Implementation
  8. Production Problem: Three Constants Nobody Was Reading
  9. Opinion: Resisting the Urge to Configure Everything
  10. Conclusion
  11. Attached Files


Introduction

A prop-firm account is not sized like an ordinary one. Every position has to address two questions instead of one: how large should this trade be, given the model's confidence, and how large can it be, given how much of today's loss budget or the account's overall drawdown allowance is already spent. Get the second question wrong and the first one stops mattering — the account is disqualified before the edge has a chance to play out.

Part 11 of the MetaTrader 5 Machine Learning Blueprint series answered that second question with PropFirmAwareSizer, a sizer that shrinks positions automatically as the loss budget is consumed, with no manual threshold logic required. It works well. It also has a narrower problem than its name suggests: every rule it enforces — the phase profit targets, the daily loss limit and whether that limit expands with intraday profit, the overall floor, the news-window profit-credit haircut — is a constant copied from one program, FundedNext's Stellar 2-Step Challenge. The class is called PropFirmAwareSizer, not FundedNextAwareSizer, but until this article it could only size positions correctly for the firm it was written against. This article assumes familiarity with prop-firm challenge mechanics — phases, daily and overall loss limits, funded status — but not with Part 11's specific code; every place the refactor touches that code directly, this article shows the relevant before-and-after rather than assuming you have it memorized.

That gap is not obvious when reading Part 11 in isolation. It becomes obvious when a reader on a different program — a fixed, non-expanding daily limit; a different profit-credit haircut; a different phase structure — tries to reuse the code and discovers the rules are wired into class bodies instead of being passed as arguments. Generalizing the sizer at that point means editing bet_sizing/prop_firm_sizer.py directly, not writing a configuration. Tracing exactly which constants that generalization touches also turns up something Part 11's own test coverage never caught: three of its seven rule constants were declared and never read by any method at all, a finding Section 8 covers in full.

This article opens a new series, Machine Learning Under Constraint, rather than an extension of the MetaTrader 5 Machine Learning Blueprint. The intended reader is narrower: someone already committed to a funded-account program, for whom the daily loss budget is the primary sizing constraint, not an optional risk overlay. The series treats prop-firm trading as what it structurally is for that reader: a constrained optimization problem with a convex payoff, not an ordinary sizing problem with extra bookkeeping. This article lays the groundwork the rest of the series depends on. It factors Part 11's hardcoded rules into an explicit PropFirmRuleSet, refactors PropFirmAccountState and the sizing modifiers to consume it, and validates the refactor against the original hardcoded implementation over a simulated equity path. Later parts revisit each modifier in turn: the w-parameter calibration chain, the phase-progress de-risking factor, the news-window haircut, and the stop-loss ceiling under the optimal trading rule search from Part 15.


Why a Constrained Optimization Problem

The bet-sizing methods from Part 10 and the Kelly payoff multiplier from Part 11 both answer the question "how large should this position be, given the model's confidence and the payoff asymmetry." Neither answers a different question that a prop-firm account forces onto every sizing decision: how large can this position be, given how much of a loss budget is left before the account is disqualified. A fixed-fractional rule or a static half-Kelly fraction has no mechanism to shrink as that budget is consumed; it produces the same position size on the day the account is one bad trade from breach as it does on the day the account just reset.

Two structural features distinguish this from a generic risk-management overlay. First, the loss budget is path-dependent: the daily limit under a dynamic-limit program is a function of every prior intraday P&L swing this session, not a static number computed once. A sizing function that does not receive the current account state as an input cannot possibly respond correctly, no matter how well-calibrated its base signal is. Second, a challenge phase has a convex payoff. Passing converts a fixed evaluation fee into a funded account with an open-ended profit share; failing forfeits the fee and nothing else. That asymmetry is the mirror image of the asymmetry Kelly sizing already encodes for win/loss ratios, except it operates at the level of the entire account rather than a single trade, and it argues for reducing size as the target is approached rather than pressing an advantage, which is precisely the opposite of what an unconstrained Kelly fraction would recommend near a favorable edge.

Masters (1995) and López de Prado's Advances in Financial Machine Learning (AFML) discuss sizing under signal-estimation uncertainty. Neither addresses sizing under a hard, path-dependent capital constraint external to the model. That is the gap PropFirmAwareSizer fills, and it is a legitimate extension of the AFML toolkit rather than a firm-specific hack — provided the firm-specific parts are factored out from the constraint-handling logic. Part 11 built the logic. This article does the factoring.


What Part 11 Hardcoded

The original module declared its rules as private constants at the top of the file:

# bet_sizing/prop_firm_sizer.py — FundedNext Stellar 2-Step rule constants
_PHASE_TARGETS = {
    Phase.CHALLENGE_PHASE_1: 0.08,
    Phase.CHALLENGE_PHASE_2: 0.05,
    Phase.FUNDED: None,
}
_DAILY_LOSS_LIMIT_PCT = 0.05
_OVERALL_LOSS_LIMIT_PCT = 0.10
_NEWS_PROFIT_CREDIT = 0.40
_MIN_TRADING_DAYS = 5
_MAX_LEVERAGE = 100
_COMMISSION_PER_LOT = 5.0

PropFirmAccountState.daily_limit, .overall_floor, and .phase_progress read these directly. So did news_window_factor for the profit-credit haircut. Nothing was wrong with the arithmetic; every property computed exactly what the docstrings claimed. The problem is a coupling problem, not a correctness problem: the daily-loss-expansion rule, the phase targets, and the news haircut are properties of a specific program's terms of service, not properties of the sizing mathematics, and the two were living in the same module-level namespace with no boundary between them.


A Configurable Rule Set (PropFirmRuleSet)

The fix is a frozen dataclass that holds every value a prop-firm program's rules can vary, with FundedNext's Stellar 2-Step program expressed as one named instance of it rather than the implicit default:

from dataclasses import dataclass
from typing import Optional

from .phase import Phase

@dataclass(frozen=True)
class PropFirmRuleSet:
    """Constraint set for a prop-firm evaluation/funded program."""

    phase_targets: dict[Phase, Optional[float]]
    daily_loss_limit_pct: float
    daily_loss_is_dynamic: bool
    overall_loss_limit_pct: float
    min_trading_days: int
    max_leverage: int
    drawdown_basis: str
    allows_overnight_positions: bool
    news_profit_credit: float = 1.0
    news_window_minutes: float = 5.0
    commission_per_lot: float = 0.0

    def __post_init__(self) -> None:
        if not 0.0 < self.daily_loss_limit_pct <= 1.0:
            raise ValueError("daily_loss_limit_pct must be in (0, 1]")
        if not 0.0 < self.overall_loss_limit_pct <= 1.0:
            raise ValueError("overall_loss_limit_pct must be in (0, 1]")
        if not 0.0 <= self.news_profit_credit <= 1.0:
            raise ValueError("news_profit_credit must be in [0, 1]")
        if self.drawdown_basis not in ("balance", "equity"):
            raise ValueError('drawdown_basis must be "balance" or "equity"')

# drawdown_basis="equity": FundedNext's own help center confirms floating P&L
# counts against both the daily and overall loss limits in real time.
# allows_overnight_positions=True: FundedNext's CFD Stellar models permit
# overnight holding, with swap charges applied.
FUNDEDNEXT_STELLAR_2STEP = PropFirmRuleSet(
    phase_targets={
        Phase.CHALLENGE_PHASE_1: 0.08,
        Phase.CHALLENGE_PHASE_2: 0.05,
        Phase.FUNDED: None,
    },
    daily_loss_limit_pct=0.05,
    daily_loss_is_dynamic=True,
    overall_loss_limit_pct=0.10,
    min_trading_days=5,
    max_leverage=100,
    drawdown_basis="equity",
    allows_overnight_positions=True,
    news_profit_credit=0.40,
    news_window_minutes=5.0,
    commission_per_lot=5.0,
)

Two field-level decisions are worth stating explicitly rather than leaving them implicit in the code. daily_loss_is_dynamic is a boolean, not a percentage, because the dynamic-expansion behavior is a qualitatively different rule from a fixed daily cap, not a parameterization of the same rule; collapsing the two into one numeric field would have hidden a structural difference behind a cosmetic one. Phase stays in its own module (phase.py) rather than inside either rule_set.py or account_state.py, because both of those modules need it and Python does not tolerate a circular import between them.

A third pair of fields — drawdown_basis and allows_overnight_positions — captures a distinction that matters even more than the two above, because getting it wrong doesn't just misfile a value, it measures the wrong quantity entirely. phase_targets, daily_loss_limit_pct, and the rest describe what a program's limits are. drawdown_basis describes what quantity those limits are actually checked against. FundedNext's own help center is explicit that a floating loss on an open position counts against both the daily and the overall loss limit the moment it occurs, not only once the trade closes — a position down $2,000 intraday eats $2,000 of the daily allowance even if it is never closed. That is an equity-basis rule: the relevant quantity is current_equity (realized balance plus the latest floating P&L snapshot), not current_balance (realized P&L alone).

A minority of programs exclude floating P&L from drawdown checks until it is realized — drawdown_basis is how PropFirmAccountState knows which one governs its own limits, rather than assuming one universally. allows_overnight_positions is documentation of a related but separate fact: whether the program permits carrying a position past the daily reset at all. FundedNext's CFD Stellar programs do, with swap charges applied; other programs require flattening before the reset. Section 5 covers why this pair, taken together, is exactly the pair that determines what reset_daily() is and is not allowed to do with a still-open position's P&L.

One more boundary is worth making explicit, since the rest of this article relies on it without restating it every time. PropFirmRuleSet holds static facts about a program's terms of service: values that are identical for every account run under that program. PropFirmAccountState holds one specific account's mutable, path-dependent state, plus a reference to whichever rule set that account operates under. Bare functions such as news_window_factor hold neither; they are pure and stateless, and receive whatever rule-set values they need as explicit arguments from a caller that does have access to the account state. That third category is not an inconsistency between the two — it is what keeps news_window_factor unit-testable in isolation, with no PropFirmAccountState instance required to exercise it.

Before going further into PropFirmAccountState specifically, the whole chain is worth seeing at once. PropFirmRuleSet and PropFirmAccountState are both firm-constraint facts — one static, one derived from it — and both feed PropFirmAwareSizer directly. derisking_factor also reads PropFirmAccountState (specifically phase_progress), but it is drawn as a separate branch rather than folded into the constraint chain, because its thresholds are a strategy choice rather than anything PropFirmRuleSet declares — the same boundary Section 9's Opinion argues for.

Architecture overview: firm constraints versus strategy response

Architecture overview: firm constraints versus strategy response

Figure 1. Flow diagram of the PropFirmRuleSet → PropFirmAccountState → PropFirmAwareSizer chain

  • Firm constraints (blue): PropFirmRuleSet and PropFirmAccountState — facts about the program and the account's resulting state, both consumed directly by the sizer.
  • Strategy response (purple): derisking_factor reads the same PropFirmAccountState, but is drawn in a different color and on a separate branch, because its thresholds are not part of PropFirmRuleSet.
  • PropFirmAwareSizer (green) is the only node that consumes both branches, multiplying the strategy response into the budget-driven calibration rather than treating it as a firm-level input.


Refactoring PropFirmAccountState

PropFirmAccountState now takes a rule_set field alongside initial_balance and phase, and every property that previously read a module constant reads the corresponding field on self.rule_set instead. The daily_limit property is the one with behavior that actually branches on the new configuration, since it is the property that encodes the dynamic-versus-fixed distinction:

# Before
@property
def daily_limit(self) -> float:
    base = self.initial_balance * _DAILY_LOSS_LIMIT_PCT
    return base + max(0.0, self.intraday_realized_pnl)

# After
@property
def daily_limit(self) -> float:
    base = self.initial_balance * self.rule_set.daily_loss_limit_pct
    if self.rule_set.daily_loss_is_dynamic:
        return base + max(0.0, self.intraday_realized_pnl)
    return base

overall_floor and phase_progress follow the same pattern: the arithmetic is unchanged, only the source of the constant moves from module scope to self.rule_set. One addition goes beyond a pure refactor. min_trading_days was declared in the original module but never read by any method; it is now consumed by a new meets_min_trading_days property. This does not change any sizing behavior — nothing in PropFirmAwareSizer reads it — but it means the field is no longer silently unused, which matters for the reason covered in the Production Problem section below. Nothing in this subpackage promotes an account between phases automatically: PropFirmAccountState.phase is set once by the caller and stays fixed for the life of the instance, since a phase advancing from CHALLENGE_PHASE_1 to CHALLENGE_PHASE_2 or FUNDED is a business event the calling code observes — the challenge platform confirms a pass — not something the sizing math can infer from P&L alone.

current_balance is realized equity only, tracked as a running total: banked_balance (cumulative realized equity as of the start of today) plus today's realized P&L net of fees. It never includes floating P&L, so multi-day profit and multi-call fee accumulation both persist correctly through reset_daily() boundaries without ever double-counting a position that survives one — reset_daily() banks current_balance, never the floating P&L snapshot, precisely because that snapshot gets re-read fresh on the next call rather than stored. current_equity is a property, not a stored field: current_balance plus whatever unrealized_pnl was passed to the most recent update() call. Which of the two actually governs daily_limit, daily_loss_used, overall_remaining, and phase_progress depends on rule_set.drawdown_basis:

@property
def daily_loss_used(self) -> float:
    """Computed fresh from today's net P&L, not accumulated call-by-call,
    so a recovery within the same day is reflected immediately."""
    net = self.intraday_realized_pnl - self.intraday_fees
    if self.rule_set.drawdown_basis == "equity":
        net += self.unrealized_pnl
    return max(0.0, -net)

FundedNext's own help center walks through the exact case this formula has to reproduce: a $2,000 realized profit on a closed trade, alongside a $6,000 floating loss on a still-open one, nets to a $4,000 daily loss — not two separately-tracked numbers, one figure that gains and losses are checked against together. The credit-back that expands daily_limit when daily_loss_is_dynamic is True stays realized-only even under an equity basis, matching FundedNext's own description of the limit expanding from profit already locked in; crediting a floating gain toward more room, on top of already letting a floating loss eat into that room, would double the account's exposure to a position that reverses intraday. The attached test suite reproduces both of FundedNext's published worked examples exactly, including the exact-breach case, as permanent regression tests rather than one-off manual checks.

The practical effect of the one if self.rule_set.daily_loss_is_dynamic branch above is easier to see against real numbers than to reason about in the abstract. Figure 2 drives both configurations through an identical simulated intraday P&L path and compares the daily budget each one computes, and the risk_budget_pct that budget ultimately produces for the sizer.

Dynamic versus fixed daily loss budget and the resulting sizing input

Dynamic versus fixed daily loss budget and the resulting sizing input

Figure 2. Two-panel illustration of the daily-limit generalization

  • Left panel: the daily loss budget under FUNDEDNEXT_STELLAR_2STEP (dynamic, expands with intraday profit) against a fixed-limit rule set with the same base rate, over an identical simulated intraday P&L path that draws down roughly $1,800 before recovering into a $1,200 intraday profit. daily_loss_used is identical under both configurations, since loss tracking does not depend on the rule set.
  • Right panel: the resulting risk_budget_pct, the quantity that actually reaches the sizer. The two configurations diverge as soon as the account turns profitable intraday, because the dynamic rule credits that profit toward the daily budget and the fixed rule does not.

This scenario keeps the drawdown shallow enough ($1,800 against a $10,000 overall cushion at 10% of a $100,000 balance) that the daily constraint remains binding throughout for both configurations. It demonstrates that the refactored PropFirmAccountState reproduces the correct FundedNext-style expansion when configured for it, and produces a materially different, correct result for a fixed-limit program when configured for that instead. It does not demonstrate behavior near the overall floor, where overall_remaining — which does not depend on daily_loss_is_dynamic — eventually becomes the binding constraint for both configurations and the two lines in the right panel would converge. A deeper drawdown scenario for that regime is a natural addition to Part 2, once the w-parameter calibration chain that actually consumes risk_budget_pct is back in scope.


Refactoring the Sizing Modifiers

news_window_factor previously read _NEWS_PROFIT_CREDIT as a hardcoded 0.40 inside the function body. It now takes news_profit_credit as an explicit parameter, and PropFirmAwareSizer.size() sources both that value and news_window_minutes from state.rule_set at call time rather than from its own constructor arguments:

def news_window_factor(
    current_time: datetime,
    news_times: list[datetime],
    news_profit_credit: float,
    window_minutes: float = 5.0,
    phase: Phase = Phase.CHALLENGE_PHASE_1,
) -> float:
    if phase != Phase.FUNDED or not news_times or news_profit_credit >= 1.0:
        return 1.0
    window = pd.Timedelta(minutes=window_minutes)
    ct = pd.Timestamp(current_time).tz_localize(None)
    for nt in news_times:
        if abs(ct - pd.Timestamp(nt).tz_localize(None)) <= window:
            return news_profit_credit
    return 1.0

This removes news_window_minutes as a PropFirmAwareSizer constructor argument — a deliberate public-API change from Part 11's version. Having the window width configurable both on the sizer and, separately, hardcoded inside the credit-haircut constant was two sources of truth for what should be one fact about the program's rules. derisking_factor is conspicuously absent from this section; it did not change, and Section 9 explains why it should not.


Validation: Parity Against the Original Implementation

A refactor that changes the source of every constant without changing any arithmetic makes a specific, checkable claim: for the one configuration that existed before this change, nothing about the sizer's behavior should be different. That claim is falsifiable, so the test suite checks it directly rather than only checking the new code in isolation. A bare reimplementation of the original PropFirmAccountState — the hardcoded constants, copied verbatim, with no dependency on PropFirmRuleSet — runs alongside the refactored version through a twenty-day, ten-bar-per-day randomized P&L simulation. The properties that are correctly scoped to a single day regardless of call count — daily_limit, daily_remaining, daily_loss_used, overall_floor — are asserted equal to the reference at every single step, since neither implementation's handling of them changed. current_balance and the properties derived from it (phase_profit_pct, overall_remaining, phase_progress) are cumulative across the entire simulated path rather than reset each day, so those are checked directly against the correct running-total arithmetic — initial_balance plus every realized gain or loss across all twenty days — rather than against a bare reference that a single-call comparison would not meaningfully exercise:

rng = np.random.default_rng(42)
orig = _OriginalAccountState(initial_balance=100_000.0, phase=Phase.CHALLENGE_PHASE_1)
new = PropFirmAccountState(
    initial_balance=100_000.0, phase=Phase.CHALLENGE_PHASE_1, rule_set=FUNDEDNEXT_STELLAR_2STEP
)

total_pnl = total_fees = 0.0
for day in range(20):
    for _bar in range(10):
        pnl = float(rng.normal(50, 300))
        fees = float(abs(rng.normal(2, 1)))
        orig.update(pnl, 0.0, fees)
        new.update(pnl, 0.0, fees)
        total_pnl += pnl
        total_fees += fees
        # assert day-scoped properties equal orig; balance/progress checked below
    orig.reset_daily()
    new.reset_daily()

assert new.current_balance == 100_000.0 + total_pnl - total_fees

Alongside this check, the full suite reproduces both of FundedNext's own published worked examples for the daily loss limit — including the exact-breach case — as permanent regression tests, confirms a balance-basis rule set genuinely excludes floating P&L where an equity-basis one does not, verifies PropFirmRuleSet validation, checks a synthetic fixed-daily-limit rule set to confirm the generalization actually generalizes and not just reproduces the one case it was built from, covers the unchanged derisking_factor and kelly_payoff_multiplier thresholds, and runs an end-to-end PropFirmAwareSizer.size() pass on dummy events and probabilities, including the budget-exhausted branch. The complete script is in the attached archive. This kind of test does not prove the refactored code is correct in any absolute sense — it proves the refactor did not silently change the sizer-facing behavior the previous article's readers were already relying on, while holding the cumulative balance tracking and the balance-versus-equity distinction to a standard verified against the program's own published numbers, not just internal consistency.


Production Problem: Three Constants Nobody Was Reading

The most useful thing this refactor produced was not the abstraction itself. It was what tracing every read of the original module constants forced into the open — a fact that reading the module top-to-bottom, the way a reviewer normally would, does not surface: min_trading_days, max_leverage, and commission_per_lot were declared in Part 11's module and never consumed by any method in it. They looked like configuration. They were closer to documentation that happened to be typed as code, and no amount of careful reading catches that; only forcing every constant to prove it has a reader does.

commission_per_lot is the more interesting case of the three, because Part 14 had already established the correct way to obtain a number like it: measure it from the broker, don't guess it. That article's TransactionCostModel treats commission_per_lot as a per-side rate obtained from an MQL5 collector script sampling the broker's own spread and swap history, with the round-trip doubling applied explicitly and documented so it cannot silently double-count. The prop-firm sizer's _COMMISSION_PER_LOT = 5.0 has no such provenance and no docstring stating whether it is per-side or round-trip. It was a plausible-looking number sitting in a module that never once multiplied it against a lot size, right next to _DAILY_LOSS_LIMIT_PCT, which the same module used on every single call.

This refactor does not wire commission_per_lot into an actual P&L calculation; that remains out of scope for Part 1 and belongs with whichever future part actually needs it as a sizing input. What it does is make the omission visible rather than silent, and it points at the correct fix: when commission_per_lot does become load-bearing in this subpackage, it should be populated from a TransactionCostModel.summary() call per Part 14, not hand-typed as a constant a second time. A constant with no consumer is easy to miss during a routine read-through; a constant with no consumer and no measurement provenance is a mistake waiting for the day something finally reads it.


Opinion: Resisting the Urge to Configure Everything

derisking_factor reduces position size once phase progress passes 80% of the target, reaching a floor at 100% — 0.30 or 0.10, depending on whether the rule set's minimum trading-days requirement is already satisfied; the mechanics of that second floor belong to a later part, not here. Every other prop-firm-flavored number in the sizer moved into PropFirmRuleSet during this refactor. Those thresholds did not, and the temptation to fold them in anyway — while the dataclass was already open, while every other magic number was getting a home — was real enough to be worth naming explicitly rather than just declining silently.

The reason to resist it is that the de-risking curve is not part of any prop firm's terms. It reflects this strategy's choice for protecting a convex payoff and therefore should not be encoded in the rule set. A different strategy author trading the exact same FundedNext account might reasonably choose a 90%/50% curve, or no de-risking at all, without changing a single fact about FundedNext's rules. Putting that choice inside PropFirmRuleSet would have made two independent decisions — what the firm requires, and how this strategy chooses to respond to its own progress — appear to be one decision, discoverable only by reading the field instead of the strategy logic. The dataclass boundary is doing real work here: it separates the constraint from the response to the constraint, and that separation matters more than the minor convenience of having every tunable number in one object.


Conclusion

Part 11 built a prop-firm-aware sizer that worked for one firm. This article turned "one firm" into a parameter without changing the sizer-facing arithmetic that made the sizer work in the first place, and put the account state's own cumulative equity tracking — across both trading days and multiple fee-bearing calls within a day — under the same direct verification. Key takeaways:

  • Separate the constraint from the response to the constraint. PropFirmRuleSet holds facts about the firm's program; derisking_factor's thresholds stay outside it because they are a strategy choice, not a firm rule.
  • A dynamic-versus-fixed daily limit is a structural difference, not a parameterization. Model it as a boolean, not a number that happens to be zero for one case.
  • Cumulative state needs a cumulative accumulator, not a recomputation. current_balance is tracked via banked_balance plus running totals for the current day's P&L and fees, so it is correct regardless of how many days or how many calls within a day have elapsed.
  • Balance and equity are not interchangeable defaults. FundedNext's own rules check floating P&L in real time; a program that doesn't is the exception, not the rule, and drawdown_basis exists so the account state never has to assume which one it's looking at.
  • An unused constant is a mistake waiting for a reader. min_trading_days, max_leverage, and commission_per_lot sat unconsumed in Part 11's module; generalizing the rules was what finally forced them into the light.

Part 2 returns to the w-parameter calibration chain — WParamCalibrator, and the sigmoid-scale modifier it drives — now that it reads risk_budget_pct from a rule-set-aware account state instead of a FundedNext-only one.

The refactor has a cost worth stating plainly rather than glossing over. PropFirmRuleSet is now a dependency every part of this series has to version alongside the strategy itself. A hardcoded module constant cannot silently drift out of sync with a firm's actual current terms of service, because there is no external configuration for it to drift from; a PropFirmRuleSet instance can, if a program changes its rules and the instance in a live strategy is never updated to match. That is a new failure mode this abstraction introduces, not one it removes. It is a trade worth making for a series that has to support more than one program — but it is a trade, not a pure win, and the next part that reads a PropFirmRuleSet value should treat it as an input that can be stale, not a constant that is definitionally correct.

Attached Files

 1. phase.py afml.prop_firm Phase enumeration, split out to avoid a circular import.
 2. rule_set.py afml.prop_firm PropFirmRuleSet dataclass and the FUNDEDNEXT_STELLAR_2STEP preset.
 3. account_state.py afml.prop_firm PropFirmAccountState, refactored to consume a PropFirmRuleSet, with current_balance tracked as a running total across days and calls, and daily/overall limits respecting drawdown_basis.
 4. sizing.py afml.prop_firm WParamCalibrator, kelly_payoff_multiplier, derisking_factor, news_window_factor, PropFirmAwareSizer, make_sizer, and the deduplicated prop_firm_sl_ceiling.
 5. test_prop_firm.py afml.prop_firm Test suite: rule-set validation, day-scoped and cumulative account-state verification, generalization to a non-FundedNext rule set, modifier thresholds, and an end-to-end sizer smoke test.


Further Reading

  • López de Prado, M. (2018). Advances in Financial Machine Learning. John Wiley & Sons.
  • Masters, T. (1995). Neural, Novel & Hybrid Algorithms for Time Series Prediction. John Wiley & Sons.
Attached files |
MQL5.zip (31.8 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Market Simulation: Position View (X) Market Simulation: Position View (X)
We need a way to handle the graphical objects we create. The approach presented in the previous article works very well for certain scenarios. In this case, we will need something more complex, given the specific nature of the problem at hand. Therefore, we will not attempt to replace the ZOrder management mechanisms already present in MetaTrader 5, nor, of course, will we check which object is in the foreground or covered by another object. We are going to do something completely different. Here, I will show you what changes need to be made to the code in order to use part of what MetaTrader 5 already does for us.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
From Basic to Intermediate: Queues, Lists, and Trees (II) From Basic to Intermediate: Queues, Lists, and Trees (II)
This is an article that you, dear reader, should study carefully. That is due to the nature of the material presented here. Although we have tried to present the material as simply and informatively as possible, the information provided here can certainly seem quite complex to those who are just beginning to learn programming. Nevertheless, this is no reason to lose heart or ignore what is explained here, as this article will establish a link between two completely different, though closely related, topics.