Position Management: Scaling Into Winners With A Falling-Risk Pyramid - MQL5 Articles

23 min read
RoboForex

Follow MQL5.community on social mediaWe publish the best technical materials from experts – free from advertising and irrelevant contentLearn more

MetaTrader 5 / Trading systems

Table of Contents

  1. Introduction
  2. Architecture Overview
  3. The Five Integration Points
  4. Engine Additions
  5. The Bridge Class
  6. Wiring a Complete Expert Advisor
  7. Conclusion
  8. Attached Files

Introduction

Part 13 of this series, "Implementing Bet Sizing in MQL5," presented an MQL5 implementation of four bet-sizing methods from the book Advances in Financial Machine Learning (AFML). Specifically, BetSizeProbability generates a concurrency-corrected, discrete position signal ranging from [-1, 1].

BetSizeDynamic translates forecast-price divergence into a bet size using a calibrated sigmoid or power curve. BetSizeBudget normalizes the long-short imbalance of active directional signals into a capacity fraction.

BetSizeReserve infers the empirical distribution of concurrent imbalance via the EF3M mixture cumulative distribution function (CDF). Each of these methods returns a BetSizeResult struct.

In the Expert Advisor (EA) from Part 13, the signed bet_size field is mapped to a single position, with its lot size calculated as InpMaxLots × |bet_size|.

However, this mapping exhibits a structural limitation: it links bet magnitude to a single-layer position. 55 initiate trades of different sizes, but both are executed as a single market order with a fixed stop loss.

The sizing information only determines the capital commitment; it does not dictate how the trade is structured, how risk evolves as the position becomes profitable, or when to add to a winning trade. These considerations fall under position management, not merely sizing.

An article by Tola Moses Hector, "Position Management: Safe Pyramiding with a Unified Stop in MQL5," introduces CPyramidEngine. This self-contained MQL5 class allows a trade to be layered through progressively decreasing lot sizes, employing a single unified stop that advances following each additional entry.

The engine is mathematically designed to ensure that total account risk diminishes with each add-on. It can be integrated into any Expert Advisor with just six modifications to existing code.

These two systems—the bet-sizing module and the pyramid engine—are complementary. The bet-sizing module determines how much to risk, while the pyramid engine dictates how to structure that risk across multiple layers.

This article focuses on building the adapter layer, named CPyramidBridge, which connects these two systems. CPyramidBridge acts as a wrapper class, sitting between the bet-sizing stack and the pyramid engine, and integrates them through five distinct points.

Each integration point replaces a hardcoded parameter in CPyramidEngine with a dynamic output from the sizing module.

Before proceeding, note the prerequisites. mq5`).

mqh, which implements CPyramidBridge. mq5, showcases all five integration points in action.

All three new files are included in the attached archive.

Figure 1 illustrates the concept of pyramiding. Progressively smaller lot sizes entered at higher price levels create a triangular shape. As the unified stop advances after each add-on, the total dollar risk decreases at each stage, even as the overall position size grows.

<figure> <img src=" width="700" height="400"/> <figcaption> Figure 1. 2-panel illustration of the pyramiding concept </figcaption> </figure>
  • Panel (a): Three horizontal bars representing entries at their respective price levels: Initial (1.00 lot at E1), Add-on 1 (0.60 lot at E2, +60 pips), and Add-on 2 (0.30 lot at E3, +120 pips). The dashed line connecting the bar tips outlines the pyramid: a broad base at the lowest price, narrowing to an apex at the highest. The dotted horizontal line signifies the unified stop level for each stage, and the red-shaded zone indicates the Stage 1 risk band.
  • Panel (b): Total risk in pip-lots at each stage (blue/green/orange bars, left axis) alongside the total open lots (grey dashed line, right axis). Risk diminishes from 50 → 40 → 15 pip-lots while the position expands from 1.00 → 1.60 → 1.90 lots. This reduction occurs because the advancing unified stop moves previously entered positions to break-even or into guaranteed profit, leaving only the newest, smallest add-on with open risk.

Architecture Overview

The five-file bet-sizing stack developed in Part 13 ("Implementing Bet Sizing in MQL5") establishes a clear dependency hierarchy. mqhforms the base, offering the normal CDF, its inverse, the sweep-line concurrency counter, and theBetSizeResult` struct.

mqh depend solely on this foundational layer. mqh relies on both of these, and the EA resides at the apex.

mqh` implements the pyramid management class.

mqhdoes not alter either of the existing stacks. Instead, it includes both, contains aCPyramidEngine` instance as a private member, and exposes a public interface that replaces direct engine calls from the EA.

The EA interacts exclusively with the bridge; the bridge, in turn, communicates with both the sizing functions and the engine. The integration points are configured within the bridge, ensuring that no sizing logic is present in the EA and no engine-specific knowledge is required by the sizing functions.

Figure 2 depicts the complete dependency graph.

<figure> <img src=" width="700" height="400"/> <figcaption> Figure 2. 3-layer dependency graph showing the BetSizing stack, CPyramidEngine, and the CPyramidBridge adapter </figcaption> </figure>
  • Left column: The Part 13 BetSizing stack, from BetSizingUtils.mqh up to BetSizing.mqh.
  • Right column: PyramidUtils.mqh, the original PyramidEngine.mqh, and the seven new public methods introduced by PyramidEngine_additions.mqh.
  • Center: BetSizingPyramidBridge.mqh, which links the two stacks. The EA only calls methods from the bridge. The five numbered labels in the callout box correspond to the five integration points detailed in the subsequent section.
FileLocationRole in this article
BetSizingUtils.mqhMQL5\Include\BetSizing\Unchanged from Part 13: Implementing Bet Sizing in MQL5"
EF3M.mqhMQL5\Include\BetSizing\Unchanged from Part 13: Implementing Bet Sizing in MQL5"
Ch10Snippets.mqhMQL5\Include\BetSizing\Unchanged from Part 13: Implementing Bet Sizing in MQL5"
BetSizing.mqhMQL5\Include\BetSizing\Unchanged from Part 13: Implementing Bet Sizing in MQL5"
PyramidUtils.mqhMQL5\Include\Pyramid\Unchanged from pyramiding article
PyramidEngine.mqhMQL5\Include\Pyramid\Unchanged; receives seven new public methods
PyramidEngine_additions.mqhMQL5\Include\Pyramid\New. Paste the contents into CPyramidEngine's public section
BetSizingPyramidBridge.mqhMQL5\Include\Pyramid\New. The bridge adapter class
BetSizingPyramidEA.mq5MQL5\Experts\New. Demonstration EA wiring all five points

The Five Integration Points

The five integration points address specific design choices in the original CPyramidEngine that were hardcoded during initialization and lacked any mechanism for the sizing module to influence them. Each point maps a particular output from the sizing stack to a specific parameter or decision within the engine.

Point 1 — Probability-Calibrated Lot Sizing

In the Part 13 EA, lot_initial is a fixed input. Consequently, a signal with 0.55 confidence and one with 0.85 confidence would both open an initial position of the same size. The solution is to calculate lot_initial at the entry point using the output from the probability method:

cpp
1//--- Integration Point 1: base lot from BetSizeProbability
2double base_lot = m_cfg.max_lots * MathAbs(prob_bet_size);

Add-on lots must adhere to the strictly decreasing constraint validated by CPyramidEngine::Init. Expressing them as fixed ratios of base_lot maintains this constraint across the entire range of prob_bet_size outputs:

cpp
1//--- Compute proportional add-on lots, floor to broker step
2double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
3out_initial = NormalizeDouble(MathFloor(base_lot / step) * step, 2);
4out_addon1 = NormalizeDouble(MathFloor(base_lot * m_cfg.addon1_ratio / step) * step, 2);
5out_addon2 = NormalizeDouble(MathFloor(base_lot * m_cfg.addon2_ratio / step) * step, 2);

Flooring, rather than rounding, is intentional. Rounding could advance a lot to the next broker step, potentially causing the constraint to fail after clipping to SYMBOL_VOLUME_MIN.

Flooring guarantees that the resulting lots are always smaller than their unrounded values, ensuring the constraint survives clipping. If the computed lots collapse to the same minimum after clipping (which occurs when prob_bet_size is small and the instrument has a coarse lot step), ComputeProportionalLots returns false, skipping the entry.

This is the correct behavior: a low-confidence signal that cannot be expressed as a valid pyramid structure should not open a trade.

The engine's lot fields are not set at Init time for each trade; instead, they are updated immediately before each OpenInitial call via the new UpdateLots method. This approach is safe because UpdateLots will not write if a pyramid is already active.

Point 2 — Budget Gate on Entry

The sole entry gate in the original CPyramidEngine is IsActive: if no pyramid is running, any signal that satisfies the entry logic can initiate one. This approach disregards the concurrent occupancy of the signal book.

BetSizeBudget calculates the normalized long-short imbalance fraction. Its complement—1 - |c_t|—represents the available headroom.

The bridge checks this headroom before invoking TryOpenInitial. SeedBudgetMaxima is a prerequisite here.

cpp
1//--- Integration Point 2: budget gate
2BetSizeResult r = BetSizeBudget(open_t, close_t, sides, now);
3double headroom = 1.0 - MathAbs(r.c_t);
4if(headroom < m_cfg.budget_min_bet)
5  {
6   PrintFormat("Budget gate blocked | c_t=%.3f headroom=%.3f", r.c_t, headroom);
7   return(false);
8  }

The budget_min_bet threshold is a configuration parameter within SBridgeConfig. 10, for example, prevents entry when the signal book is 90% occupied.

00 completely disables the gate. The optimal value depends on the strategy's average holding period and the expected maximum number of concurrent signals.

Point 3 — Dynamic Add-On Trigger

The original engine triggers add-ons when the position has moved a fixed number of pips in its favor. However, fixed pip triggers do not account for the model's forecast.

BetSizeDynamic maps the divergence between the current price and the model's forecast price through a calibrated sigmoid or power curve, yielding a value in [-1, 1]. As the trade becomes profitable and the price approaches the forecast, the dynamic bet size increases.

The bridge monitors the dynamic bet size at the entry bar and compares it against two thresholds on every tick.

cpp
1//--- Integration Point 3: dynamic add-on trigger
2bool a1_crossed = (!m_engine.IsAddon1Open) &&
3                  (current_dyn_bet >= m_cfg.dynamic_level_1);
4bool a2_crossed = (m_engine.IsAddon1Open) && (!m_engine.IsAddon2Open) &&
5                  (current_dyn_bet >= m_cfg.dynamic_level_2);
6
7if(a1_crossed)
8   m_engine.SetAddonTriggerPips(0.1, m_engine.GetAddon2TriggerPips);
9if(a2_crossed)
10   m_engine.SetAddonTriggerPips(m_engine.GetAddon1TriggerPips, 0.1);

SetAddonTriggerPips can only reduce triggers, preventing a crossed threshold from reverting if the dynamic bet size briefly dips. The original pip-based triggers remain as a fallback: if use_dynamic_trigger in SBridgeConfig is false, the bridge takes no action, and the engine's original pip logic operates without change.

Point 4 — Reserve Sizing as Adaptive Trail Multiplier

BetSizeReserve maps the raw concurrent imbalance using the CDF of the EF3M mixture, which is fitted to historical imbalance data. When the reserve bet size contracts, it indicates that the imbalance is moving back towards the center of its historical distribution, implying weaker empirical support for holding the position. The bridge calculates the pyramid's current allocation ratio (total open lots divided by max_lots) and compares it against the reserve bet size on each new bar. If the reserve falls below the allocation, the trailing stop is tightened by reserve_tighten_mul; if the reserve recovers, the base trailing stop is restored:

cpp
1//--- Integration Point 4: reserve-adaptive trail
2double total_lots = m_live_lot_initial + m_live_lot_addon1 + m_live_lot_addon2;
3double alloc_ratio = (m_cfg.max_lots > 0.0)
4                    ? total_lots / m_cfg.max_lots
5                    : 0.0;
6
7if(reserve_bet < alloc_ratio)
8  {
9   double tight_pips = m_base_trail_pips * m_cfg.reserve_tighten_mul;
10   double tight_step = m_base_trail_step * m_cfg.reserve_tighten_mul;
11   m_engine.SetTrailParams(tight_pips, tight_step);
12  }
13else
14   m_engine.SetTrailParams(m_base_trail_pips, m_base_trail_step);

This point necessitates the EF3M warm-up described in Part 13: "Implementing Bet Sizing in MQL5": FitM2N must be called within OnInit using the historical c_t series before the reserve method can be effectively employed here. A minimum of 500 bets is recommended for historical data; with fewer observations, the mixture fit becomes unstable, leading to an oscillatory adaptive trail.

Point 5 — Signal Array Synchronization on Pyramid Close

When CPyramidEngine detects that the initial position has been closed, it invokes ResetState and deactivates itself. However, the sizing module's signal arrays in the EA are unaware of this closure.

The close_t entry of the most recent open signal still points to the original triple-barrier t1 timestamp, causing the signal to remain registered as active in the concurrency counter. Consequently, every subsequent call to BetSizeProbability or BetSizeBudget overcounts active signals by one.

cpp
1//--- Integration Point 5: detect pyramid close and sync arrays
2bool was_active = g_bridge.IsActive;
3g_bridge.HandleTransaction(trans);
4if(g_bridge.WasJustClosed(was_active))
5   SyncSignalArraysOnClose;

SyncSignalArraysOnClose iterates backward through g_close_t[] and updates the timestamp of the most recently closed signal to TimeCurrent. For the concurrency counter, which operates with event times at bar granularity, this approximation is precise.

Engine Additions

Implementing the five integration points requires seven new public methods for CPyramidEngine. These additions do not modify existing logic but rather provide public read and write access to member variables that were already present but not exposed. Paste the contents of PyramidEngine_additions.mqh into the public section of CPyramidEngine in PyramidEngine.mqh, immediately after the existing GetUnifiedStop declaration.

cpp
1//+------------------------------------------------------------------+
2//| UpdateLots: update lot sizes before the next TryOpenInitial call |
3//| Returns false when the pyramid is active; no writes occur |
4//+------------------------------------------------------------------+
5bool UpdateLots(double lot_initial, double lot_addon1, double lot_addon2)
6  {
7   if(m_state.active)
8     {
9      Print("UpdateLots called while pyramid active — ignored.");
10      return(false);
11     }
12   if(lot_addon1 >= lot_initial || lot_addon2 >= lot_addon1)
13     {
14      Print("UpdateLots: decreasing constraint violated.");
15      return(false);
16     }
17   m_lot_initial = lot_initial;
18   m_lot_addon1 = lot_addon1;
19   m_lot_addon2 = lot_addon2;
20   return(true);
21  }
22
23//+------------------------------------------------------------------+
24//| SetAddonTriggerPips: advance add-on pip triggers; never retracts |
25//| A trigger can only decrease; passing a larger value is ignored |
26//+------------------------------------------------------------------+
27void SetAddonTriggerPips(double trig1, double trig2)
28  {
29   if(trig1 < m_addon1_trigger_pips) m_addon1_trigger_pips = trig1;
30   if(trig2 < m_addon2_trigger_pips) m_addon2_trigger_pips = trig2;
31  }
32
33//+------------------------------------------------------------------+
34//| Read-only accessors used by CPyramidBridge for dynamic triggers |
35//+------------------------------------------------------------------+
36double GetAddon1TriggerPips(void) { return(m_addon1_trigger_pips); }
37double GetAddon2TriggerPips(void) { return(m_addon2_trigger_pips); }
38bool IsAddon1Open(void) { return(m_state.addon1_open); }
39bool IsAddon2Open(void) { return(m_state.addon2_open); }
40
41//+------------------------------------------------------------------+
42//| SetTrailParams: update trailing stop parameters at runtime |
43//| Used by the reserve-adaptive trail (Integration Point 4) |
44//+------------------------------------------------------------------+
45void SetTrailParams(double trail_pips, double trail_step)
46  {
47   m_trail_pips = trail_pips;
48   m_trail_step_pips = trail_step;
49  }

The SetAddonTriggerPips function employs a strictly non-increasing write: it only accepts a new trigger value if that value is smaller than the currently stored one. UpdateLots first checks m_state.active and returns false without writing if a pyramid is currently open; calling it mid-trade would result in a silent state inconsistency.

The Bridge Class

The complete BetSizingPyramidBridge.mqh file is available in the attached archive. The SBridgeConfig struct consolidates all bridge configuration parameters, which are then passed to Init. This approach streamlines the EA's input section: the bridge configuration is assembled into a single struct in OnInit and passed once, rather than scattering a dozen parameters across multiple function calls.

cpp
1struct SBridgeConfig
2  {
3   //--- Point 1 — lot sizing
4   double max_lots; // Maximum lots at full confidence
5   double addon1_ratio; // lot_addon1 = lot_initial * ratio
6   double addon2_ratio; // lot_addon2 = lot_initial * ratio
7
8   //--- Point 2 — budget gate
9   bool use_budget_gate;
10   double budget_min_bet; // Skip entry when headroom < this value
11
12   //--- Point 3 — dynamic add-on trigger
13   bool use_dynamic_trigger;
14   double dynamic_level_1; // |BetSizeDynamic| threshold for add-on 1
15   double dynamic_level_2; // |BetSizeDynamic| threshold for add-on 2
16
17   //--- Point 4 — reserve adaptive trail
18   bool use_reserve_trail;
19   double reserve_tighten_mul; // Trail multiplier when reserve < alloc_ratio
20
21   double min_lot; // Hard floor for lot_initial
22  };

CPyramidBridge holds one private instance of CPyramidEngine. It also separately stores the base trailing stop parameters, allowing the reserve trailing stop logic to always restore them after tightening. The live lot values are saved after each TryOpenInitial call, enabling the allocation ratio calculation in Point 4 to run without querying broker positions. The public interface exposes five integration methods, along with pass-throughs to the engine for operations that the EA must call directly:

cpp
1bool Init(const SBridgeConfig &cfg, double trail_pips, double trail_step);
2bool InitEngine(int magic, int slip,
3                 double lot_i, double lot_a1, double lot_a2,
4                 double trig1, double trig2,
5                 double stop1, double stop2,
6                 bool trail, double trail_pips, double trail_step);
7
8//--- Points 1 and 3: open initial position with calibrated lots
9bool TryOpenInitial(long direction, double price, double sl,
10                     double prob_bet_size, double dynamic_bet_size,
11                     string comment = "Pyramid Entry");
12
13//--- Point 2: budget gate check before entry
14bool IsBudgetClearForEntry(const datetime &open_t[], const datetime &close_t[],
15                            const int &sides[], datetime now);
16
17//--- Point 3: update dynamic trigger — call every tick when active
18void UpdateDynamicTrigger(double current_dyn_bet);
19
20//--- Point 4: reserve-adaptive trail — call once per bar when active
21void AdaptTrailToReserve(double reserve_bet);
22
23//--- Point 5: did the pyramid just close?
24bool WasJustClosed(bool was_active_before);
25
26//--- Engine pass-throughs
27void Manage(void);
28void HandleTransaction(const MqlTradeTransaction &trans);
29void RecoverState(void);
30bool IsActive(void);
31double GetUnifiedStop(void);

The distinction between Init and InitEngine is intentional. The bridge's configuration is independent of the engine's; therefore, they can be modified in separate development cycles without affecting each other. The lot values passed to InitEngine are merely placeholders; they are overwritten by UpdateLots during each TryOpenInitial call.

Wiring a Complete Expert Advisor

Figure 3 illustrates the outcome of the five integration points on synthetic data over 30 bars: panel (a) displays the BetSizeProbability output, with threshold lines indicating when add-on 1 (|bet_size| > 0.40) and add-on 2 (|bet_size| > 0.65) would be justified; panel (b) shows the corresponding pyramid lot layers. This structure, abstractly presented in Figure 1, is now dynamically sized by the classifier's confidence.

<figure> <img src=" width="700" height="400"/> <figcaption> Figure 3. 2-panel illustration of BetSizeProbability output driving proportional pyramid lot allocation </figcaption> </figure>
  • Panel (a): Discretized bet_size from BetSizeProbability across 30 synthetic bars. The orange dotted line indicates the add-on 2 threshold (0.65); the green dotted line indicates the add-on 1 threshold (0.40).
  • Panel (b): Stacked pyramid lot layers derived from the signal above. Blue bars represent lot_initial. Green bars show lot_addon1 (0.60 × lot_initial). Orange bars show lot_addon2 (0.30 × lot_initial), appearing only when |bet_size| > 0.65.

The complete demonstration EA, BetSizingPyramidEA.mq5, integrates all five points. The OnTick sequence is as follows:

cpp
1//+------------------------------------------------------------------+
2//| OnTick: entry point called on every market tick |
3//| Runs dynamic trigger and Manage on every tick; reserve trail |
4//| and entry evaluation run on new bars only |
5//+------------------------------------------------------------------+
6void OnTick(void)
7  {
8   datetime current_bar = iTime(_Symbol, PERIOD_H1, 0);
9   bool is_new_bar = (current_bar != g_last_bar);
10   if(is_new_bar)
11      g_last_bar = current_bar;
12
13   //--- Point 3: update dynamic trigger on every tick
14   if(g_bridge.IsActive)
15     {
16      double dyn_bet = MathAbs(BetSizeDynamic(
17                                g_current_pos, InpMaxLots * 100,
18                                SymbolInfoDouble(_Symbol, SYMBOL_BID),
19                                GetForecastPrice,
20                                InpCalDiv * _Point,
21                                InpCalBetSize, InpDynFunc).bet_size);
22      g_bridge.UpdateDynamicTrigger(dyn_bet);
23     }
24
25   //--- Manage pyramid on every tick
26   bool was_active = g_bridge.IsActive;
27   g_bridge.Manage;
28
29   //--- Point 5: sync arrays on pyramid close
30   if(g_bridge.WasJustClosed(was_active))
31      SyncSignalArraysOnClose;
32
33   if(is_new_bar)
34     {
35      //--- Point 4: adapt trail once per bar
36      if(g_bridge.IsActive && InpUseReserveTrail)
37        {
38         BetSizeResult rv = BetSizeReserve(g_open_t, g_close_t, g_sides,
39                                         TimeCurrent, g_reserve_params);
40         g_bridge.AdaptTrailToReserve(MathAbs(rv.bet_size));
41        }
42      if(!g_bridge.IsActive)
43         CheckForEntry;
44     }
45  }

The sequence of calls reflects the differing time scales of the five points. The dynamic trigger check and Manage execute on every tick because add-on triggers must respond to price movements within a bar. The reserve trail and entry signal, however, run only on new bars: the trail adjusts at bar granularity, and the entry signal is evaluated on completed bars to avoid signal noise during a bar's formation.

CheckForEntry first calls IsBudgetClearForEntry and immediately returns if the gate is blocked. It then evaluates its signal, calls BetSizeProbability to obtain the confidence-weighted bet_size, rejects entries where the probability method's direction conflicts with the signal's direction, and finally calls TryOpenInitial with both the probability and dynamic bet sizes:

cpp
1//+------------------------------------------------------------------+
2//| CheckForEntry: evaluate signal and submit via the bridge |
3//| Applies the budget gate (Point 2), computes the probability and |
4//| dynamic bet sizes, then calls TryOpenInitial |
5//+------------------------------------------------------------------+
6void CheckForEntry(void)
7  {
8   datetime now = TimeCurrent;
9
10   //--- Point 2: budget gate
11   if(!g_bridge.IsBudgetClearForEntry(g_open_t, g_close_t, g_sides, now))
12      return;
13
14   // ... evaluate signal, determine direction, price, sl ...
15
16   //--- Point 1 source: probability bet size
17   BetSizeResult prob_r = BetSizeProbability(
18                           g_open_t, g_close_t, g_prob, g_pred,
19                           2, InpStepSize, InpAvgActive, now);
20   if(direction == POSITION_TYPE_BUY && prob_r.bet_size <= 0.0) return;
21   if(direction == POSITION_TYPE_SELL && prob_r.bet_size >= 0.0) return;
22
23   //--- Point 3 source: dynamic bet size at entry bar
24   double dyn_bet = MathAbs(BetSizeDynamic(
25                             0, InpMaxLots * 100, price, GetForecastPrice,
26                             InpCalDiv * _Point,
27                             InpCalBetSize, InpDynFunc).bet_size);
28
29   if(g_bridge.TryOpenInitial(direction, price, sl,
30                             prob_r.bet_size, dyn_bet, "Pyramid Entry"))
31      AppendNewSignal(now, direction, MathAbs(prob_r.bet_size));
32  }

Two calibration decisions are essential before deployment. 0, and addon2_ratio must be less than addon1_ratio.

For instruments with coarse lot steps, it is crucial to verify that the calculated lots remain valid after being clipped to SYMBOL_VOLUME_MIN before going live. Second, dynamic_level_1 and dynamic_level_2 must be calibrated against the strategy's historical BetSizeDynamic output.

This involves running the bet-sizing stack over warm-up data and analyzing the distribution of |bet_size| values on profitable bars; the median and 75th percentile of this distribution are reasonable starting points for the two thresholds.

Conclusion

The Part 13 bet-sizing module from "Implementing Bet Sizing in MQL5" and the pyramiding engine from "Position Management: Safe Pyramiding with a Unified Stop in MQL5" address distinct facets of the same overarching problem. The sizing module determines the appropriate capital allocation for a given signal, considering classifier confidence, label concurrency, and the historical distribution of past positions.

Conversely, the pyramid engine dictates how to structure that capital across multiple layers, offering a mathematically verifiable property of risk reduction. This property, as depicted in Figure 1, ensures that each additional entry is smaller than the preceding one, the unified stop advances, and the total dollar risk diminishes at every stage.

Although these two systems were not initially designed for synergy, connecting them requires a lean adapter layer that translates sizing outputs into engine parameters.

CPyramidBridge implements this adapter across five integration points. The probability method drives the initial lot sizing.

The budget method controls entry, blocking new trades when the signal book reaches capacity. The dynamic method advances add-on triggers when price divergence supports adding to a position.

The reserve method tightens the trailing stop when the empirical distribution of imbalance contracts below the current allocation. Finally, a synchronization check ensures signal arrays remain consistent when the pyramid closes.

Each point is independently configurable through the SBridgeConfig struct; disabling any flag reverts that aspect to the engine's original behavior.

In Part 16 of the Blueprint series, the sizing layer will be integrated with the CPCV backtesting framework within the MetaTrader 5 Strategy Tester. The probability estimates feeding into BetSizeProbability are prone to systematic bias from class imbalance and calibration errors; their characterization and correction will be explored in Part 12.

Attached Files

#FileLocationDepends OnDescription
1BetSizingUtils.mqhMQL5\Include\BetSizing\Unchanged from Part 13: "Implementing Bet Sizing in MQL5". Includes NormCDF, NormICDF, NormPDF, RawMoments, SweepLineActiveCounts, BetSizeResult, Clamp, MathSign.
2EF3M.mqhMQL5\Include\BetSizing\BetSizingUtils.mqhUnchanged from Part 13: "Implementing Bet Sizing in MQL5". Includes M2NParams, DeriveComponentParams, FitM2N, MixtureCDF, ReserveBetSize.
3Ch10Snippets.mqhMQL5\Include\BetSizing\BetSizingUtils.mqhUnchanged from Part 13: "Implementing Bet Sizing in MQL5". Includes GetSignal, AvgActiveSignals, DiscreteSignal, SigmoidBetSize, PowerBetSize, GetW, LimitPrice.
4BetSizing.mqhMQL5\Include\BetSizing\Ch10Snippets.mqh, EF3M.mqhUnchanged from Part 13: "Implementing Bet Sizing in MQL5". Includes BetSizeProbability, BetSizeDynamic, BetSizeBudget, BetSizeReserve, SeedBudgetMaxima.
5PyramidUtils.mqhMQL5\Include\Pyramid\Unchanged from pyramiding article. Contains pip-value helpers and broker-level stop validation.
6PyramidEngine.mqhMQL5\Include\Pyramid\PyramidUtils.mqhThe original CPyramidEngine restructured to receive seven new public methods: UpdateLots, SetAddonTriggerPips, GetAddon1TriggerPips, GetAddon2TriggerPips, IsAddon1Open, IsAddon2Open, SetTrailParams. Paste into CPyramidEngine's public section.
7BetSizingPyramidBridge.mqhMQL5\Include\Pyramid\BetSizing.mqh, PyramidEngine.mqhThe SBridgeConfig struct and CPyramidBridge class, implementing all five integration points.
8BetSizingPyramidEA.mq5MQL5\Experts\BetSizingPyramidBridge.mqhA demonstration EA that connects an EMA crossover signal to the bridge. All five integration points are active. Replace CheckForEntry with any classifier output.

References

  • López de Prado, M. (2018). Advances in Financial Machine Learning. John Wiley & Sons. Chapter 10.
  • López de Prado, M. and Foreman, M. (2014). A mixture of two Gaussians approach to mathematical portfolio oversight: The EF3M algorithm. Quantitative Finance, 14(5), 913–930.
  • Hector, T. M. (2026). Position Management: Safe Pyramiding with a Unified Stop in MQL5.

Attached files |

Download ZIP

MQL5.zip (33.87 KB)

Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.

This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.

Patrick Murimi Njoroge

  • Kenya
  • 6527
  • Beyond the Clock (Part 2): Building Runs Bars in MQL5
  • Meta-Labeling the Classics (Part 1): Filtering and Sizing RSI Trades
  • Feature Engineering for ML (Part 4): Implementing Time Features in MQL5
  • MetaTrader 5 Machine Learning Blueprint (Part 16): Nested CV for Unbiased Evaluation
  • Feature Engineering for ML (Part 3): Session-Aware Time Features for Forex Machine Learning
  • MetaTrader 5 Machine Learning Blueprint (Part 15): How to Calibrate Profit-Taking and Stop-Loss Targets from Synthetic Data

Go to discussion

Custom Debugging and Profiling Tools for MQL5 Development (Part II): Profiling EAs and Testing Trading Logic

We build a compact profiler that records calls, min/max/average times, and slow-call counts to CSV, and a simple test runner that writes deterministic pass/fail reports. The article explains where to place measurements in an EA, how to sample ticks, and how to keep pure calculations testable. Running the script first and the profiling EA second provides repeatable evidence for regression analysis.

MQL5 Wizard Techniques you should know (Part 92): Using B-Tree Indexing and a Bayesian NN in a Custom Signal Class

In this article we present yet another custom MQL5 Signal Class that we are labelling ‘CSignalBTreeBayesian’. We are marrying the algorithm of a balanced tree with a neural network that is built on Bayesian principles to formulate yet another custom signal testable independently or with other signals thanks to the MQL5 Wizard.

Formulating Dynamic Multi-Pair EA (Part 9): Market Microstructure Execution Noise Filtering

This article presents a multi-symbol execution filter that scores real-time market quality before any trade is allowed. It measures spread behavior, tick velocity, quote gaps, micro-volatility, and a slippage estimate, then classifies the state to block degraded conditions. Once noise settles, a liquidity sweep continuation model evaluates structure shifts so entries occur only when execution is mechanically stable.

MQL5 Bootstrap (I): Reusable Functions for Working with Positions and Orders

This article presents a compact MQL5 utility layer for routine trade operations. It includes position existence checkers, position counters, bulk close helpers, and functions to retrieve the most recent or oldest position by symbol, magic, or type.

A simple SMA crossover Expert Advisor demonstrates integration. The result is cleaner EAs, fewer inconsistencies across projects, and faster maintenance.

This website uses cookies. Learn more about our Cookies Policy.

You are missing trading opportunities:

  • Free trading apps
  • Over 8,000 signals for copying
  • Economic news for exploring financial markets

RegistrationLog in

latin characters without spaces

a password will be sent to this email

An error occurred

  • Log in With Google

You agree to website policy and terms of use

If you do not have an account, please register

Allow the use of cookies to log in to the website.

Please enable the necessary setting in your browser, otherwise you will not be able to log in.

Forgot your login/password?

  • Log in With Google
RoboForex