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

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

MetaTrader 5Trading systems |
1 363 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

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 translated the four AFML bet-sizing methods into MQL5. BetSizeProbability produces a concurrency-corrected, discretized position signal in [−1, 1]. BetSizeDynamic maps forecast-price divergence to a bet size through a calibrated sigmoid or power curve. BetSizeBudget normalizes the long-short imbalance of active directional signals into a capacity fraction. BetSizeReserve reads the empirical distribution of concurrent imbalance via the EF3M mixture CDF. Each method returns a BetSizeResult struct, and the EA in Part 13 maps the signed bet_size field to a single position whose lot size is InpMaxLots × |bet_size|.

That mapping has one structural gap: it connects bet magnitude to a single-layer position. A bet_size of 0.90 and a bet_size of 0.55 open trades of different sizes, but both are executed as a single market order at a fixed stop. The sizing information shapes how much capital is committed; it does not shape how the trade is built, how risk evolves as the position moves in favor, or when to add to a winning trade. Those questions belong to position management, not to sizing.

An article by Tola Moses Hector, Position Management: Safe Pyramiding with a Unified Stop in MQL5, presents CPyramidEngine. It is a self-contained MQL5 class that layers a trade through strictly decreasing lot sizes and uses a single unified stop that advances after each addition. The engine is mathematically constructed so that total account risk decreases with every add-on. It is designed to plug into any Expert Advisor with six changes to existing code.

The two systems are complementary. The bet-sizing module answers how much to risk. The pyramid engine answers how to structure that risk across layers. This article builds the adapter layer that connects them. The result is CPyramidBridge, a wrapper class that sits between the bet-sizing stack and the pyramid engine and wires them through five integration points. Each point replaces a hardcoded parameter in CPyramidEngine with a live output from the sizing module.

Before we begin, note the prerequisites. The code in this article depends on all five files from Part 13 (BetSizingUtils.mqh, EF3M.mqh, Ch10Snippets.mqh, BetSizing.mqh, BetSizingEA.mq5) and on the three files from the pyramiding article (PyramidUtils.mqh, PyramidEngine.mqh, PyramidEA.mq5). This article adds two new files: PyramidEngine_additions.mqh, which extends the engine with seven new public methods, and BetSizingPyramidBridge.mqh, which implements CPyramidBridge. A wired demonstration EA, BetSizingPyramidEA.mq5, shows all five integration points active at once. All three new files are in the attached archive.

Figure 1 illustrates pyramiding. Decreasing lot sizes entered at higher price levels form a triangular outline. Because the unified stop advances after each add-on, total dollar risk falls at each stage even as the total position size increases.

Why it is called pyramiding

Figure 1. 2-panel illustration of the pyramiding concept

  • Panel (a): Three horizontal bars drawn at their entry price levels — Initial (1.00 lot at E1), Add-on 1 (0.60 lot at E2, +60 pips), Add-on 2 (0.30 lot at E3, +120 pips). The dashed line connecting the bar tips traces the pyramid outline: a wide base at the lowest price, narrowing to an apex at the highest. The unified stop level for each stage is shown as a dotted horizontal line; the red-shaded zone illustrates the Stage 1 risk band.
  • Panel (b): Total risk in pip-lots at each stage (blue/green/orange bars, left axis) alongside total open lots (grey dashed line, right axis). Risk falls from 50 → 40 → 15 pip-lots while the position grows from 1.00 → 1.60 → 1.90 lots. The reduction occurs because the advancing unified stop moves previously entered positions to break-even or into locked profit, so only the newest, smallest add-on carries open risk.


Architecture Overview

The five-file bet-sizing stack from Part 13 forms a clean dependency hierarchy. BetSizingUtils.mqh is the foundation, providing the normal CDF, its inverse, the sweep-line concurrency counter, and the BetSizeResult struct. EF3M.mqh and Ch10Snippets.mqh each depend only on that layer. BetSizing.mqh depends on both, and the EA sits at the top. CPyramidEngine is an independent two-file stack: PyramidUtils.mqh provides instrument-agnostic pip-value computation and broker-level stop validation; PyramidEngine.mqh implements the pyramid management class.

BetSizingPyramidBridge.mqh does not modify either existing stack. It includes both stacks, owns a CPyramidEngine instance as a private member, and exposes a public interface that replaces direct engine calls in the EA. The EA talks exclusively to the bridge; the bridge talks to both the sizing functions and the engine. The integration points are wired inside the bridge so that no sizing logic appears in the EA and no engine knowledge is required from the sizing functions.

Figure 2 shows the full dependency graph.

Bet Sizing and CPyramidEngine Bridge Architecture

Figure 2. 3-layer dependency graph showing the BetSizing stack, CPyramidEngine, and the CPyramidBridge adapter

  • Left column: the Part 13 BetSizing stack from BetSizingUtils.mqh up through BetSizing.mqh.
  • Right column: PyramidUtils.mqh, the original PyramidEngine.mqh, and the seven new public methods added by PyramidEngine_additions.mqh.
  • Center: BetSizingPyramidBridge.mqh, which connects the two stacks. The EA calls only the bridge. The five numbered labels in the callout box correspond to the five integration points developed in the following section.
File Place in Role in this article
BetSizingUtils.mqh MQL5\Include\BetSizing\ Unchanged from Part 13
EF3M.mqh MQL5\Include\BetSizing\ Unchanged from Part 13
Ch10Snippets.mqh MQL5\Include\BetSizing\ Unchanged from Part 13
BetSizing.mqh MQL5\Include\BetSizing\ Unchanged from Part 13
PyramidUtils.mqh MQL5\Include\Pyramid\ Unchanged from pyramiding article
PyramidEngine.mqh MQL5\Include\Pyramid\ Unchanged; receives seven new public methods
PyramidEngine_additions.mqh MQL5\Include\Pyramid\ New. Paste the contents into CPyramidEngine's public section
BetSizingPyramidBridge.mqh MQL5\Include\Pyramid\ New. The bridge adapter class
BetSizingPyramidEA.mq5 MQL5\Experts\ New. Demonstration EA wiring all five points


The Five Integration Points

The five integration points address five separate design choices in the original CPyramidEngine that were hardcoded at initialization and had no mechanism for the sizing module to influence. Each point maps a specific output of the sizing stack to a specific parameter or decision in the engine.

Point 1 — Probability-Calibrated Lot Sizing

In the Part 13 EA, lot_initial is a fixed input. A 0.55-confidence signal and a 0.85-confidence signal open the same-sized initial position. The fix is to compute lot_initial at entry time from the probability method's output:

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

Add-on lots must maintain the strictly decreasing constraint that CPyramidEngine::Init() validates. Expressing them as fixed ratios of base_lot preserves the constraint across the full range of prob_bet_size outputs:

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

Flooring rather than rounding is intentional. Rounding can advance a lot to the next broker step, which can cause the constraint to fail after clipping to SYMBOL_VOLUME_MIN. Flooring guarantees the resulting lots are always smaller than the unrounded value; the constraint survives clipping. When 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 and the entry is skipped. This is correct behavior: a low-confidence signal that cannot be expressed as a valid pyramid structure should not open a trade at all.

The engine's lot fields are not set at Init() time for each trade; they are updated immediately before each OpenInitial() call via the new UpdateLots() method. This is safe because UpdateLots() refuses to write when a pyramid is already active.

Point 2 — Budget Gate on Entry

The only entry gate in the original CPyramidEngine is IsActive(): if no pyramid is running, any signal that passes the entry logic can open one. This ignores the concurrent occupancy of the signal book. BetSizeBudget computes the normalized long-short imbalance fraction. Its complement — 1 − |c_t| — is the available headroom. The bridge checks this before calling TryOpenInitial(). SeedBudgetMaxima() is required. Without it, the running maxima start at 1, so the earliest bars produce oversized budget fractions:

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

The threshold budget_min_bet is a configuration parameter in SBridgeConfig. A value of 0.10 blocks entry when the book is 90% occupied. A value of 0.00 disables the gate entirely. The correct value depends on the average holding period and the expected maximum concurrent signal count for the strategy.

Point 3 — Dynamic Add-On Trigger

The original engine fires add-ons when the position has moved a fixed number of pips in favor. Fixed pip triggers have no awareness of 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, returning a value in [−1, 1]. As the trade moves in favor and the price approaches the forecast, the dynamic bet size rises. The bridge tracks the dynamic bet size at the entry bar and compares it against two thresholds on every tick. When the first threshold is crossed, it advances the engine's internal add-on 1 pip trigger to 0.1 pip — a value so small that the position's existing P&L will always satisfy it — and the engine fires the add-on on the next Manage() call:

//--- Integration Point 3: dynamic add-on trigger
bool a1_crossed = (!m_engine.IsAddon1Open()) &&
                  (current_dyn_bet >= m_cfg.dynamic_level_1);
bool a2_crossed = (m_engine.IsAddon1Open()) && (!m_engine.IsAddon2Open()) &&
                  (current_dyn_bet >= m_cfg.dynamic_level_2);

if(a1_crossed)
   m_engine.SetAddonTriggerPips(0.1, m_engine.GetAddon2TriggerPips());
if(a2_crossed)
   m_engine.SetAddonTriggerPips(m_engine.GetAddon1TriggerPips(), 0.1);

SetAddonTriggerPips() can only reduce triggers. This prevents a crossed threshold from being undone if the dynamic bet size briefly dips afterward. The original pip-based triggers remain as fallbacks: if use_dynamic_trigger is false in SBridgeConfig, the bridge does nothing and the engine's original pip logic operates unchanged.

Point 4 — Reserve Sizing as Adaptive Trail Multiplier

BetSizeReserve maps the raw concurrent imbalance through the CDF of the EF3M mixture fitted to historical imbalance data. When the reserve bet size contracts, the imbalance is moving back toward the center of the historical distribution; the empirical support for holding is weaker. The bridge computes the pyramid's current allocation ratio as total open lots divided by max_lots and compares it against the reserve bet size on each new bar. When the reserve drops below the allocation, the trail is tightened by reserve_tighten_mul; when the reserve recovers, the base trail is restored:

//--- Integration Point 4: reserve-adaptive trail
double total_lots  = m_live_lot_initial + m_live_lot_addon1 + m_live_lot_addon2;
double alloc_ratio = (m_cfg.max_lots > 0.0)
                    ? total_lots / m_cfg.max_lots
                    : 0.0;

if(reserve_bet < alloc_ratio)
  {
   double tight_pips = m_base_trail_pips * m_cfg.reserve_tighten_mul;
   double tight_step = m_base_trail_step * m_cfg.reserve_tighten_mul;
   m_engine.SetTrailParams(tight_pips, tight_step);
  }
else
   m_engine.SetTrailParams(m_base_trail_pips, m_base_trail_step);

This point requires the EF3M warm-up described in Part 13: FitM2N() must be called in OnInit() from the historical c_t series before the reserve method can be used here. The minimum recommended history is 500 bets; with fewer observations the mixture fit is unstable and the adaptive trail will oscillate.

Point 5 — Signal Array Synchronization on Pyramid Close

When CPyramidEngine detects that the initial position has been closed, it calls ResetState() and goes inactive. The sizing module's signal arrays in the EA do not know this has happened. The most recent open signal's close_t entry still points to the original triple-barrier t1 timestamp, so the signal remains registered as active in the concurrency counter. Every subsequent call to BetSizeProbability or BetSizeBudget overcounts active signals by one. The bridge exposes a single boolean check, WasJustClosed(), that the EA calls after every HandleTransaction() pass:

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

SyncSignalArraysOnClose() walks backward through g_close_t[] and sets the timestamp of the most recent still-open signal to TimeCurrent(). For the concurrency counter, which uses event times at bar granularity, the approximation is exact.


Engine Additions

The five integration points require seven new public methods on CPyramidEngine. None of these modify existing logic; they add 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.

//+------------------------------------------------------------------+
//| UpdateLots: update lot sizes before the next TryOpenInitial call |
//| Returns false when the pyramid is active; no writes occur        |
//+------------------------------------------------------------------+
bool UpdateLots(double lot_initial, double lot_addon1, double lot_addon2)
  {
   if(m_state.active)
     {
      Print("UpdateLots called while pyramid active — ignored.");
      return(false);
     }
   if(lot_addon1 >= lot_initial || lot_addon2 >= lot_addon1)
     {
      Print("UpdateLots: decreasing constraint violated.");
      return(false);
     }
   m_lot_initial = lot_initial;
   m_lot_addon1  = lot_addon1;
   m_lot_addon2  = lot_addon2;
   return(true);
  }

//+------------------------------------------------------------------+
//| SetAddonTriggerPips: advance add-on pip triggers; never retracts |
//| A trigger can only decrease; passing a larger value is ignored   |
//+------------------------------------------------------------------+
void SetAddonTriggerPips(double trig1, double trig2)
  {
   if(trig1 < m_addon1_trigger_pips) m_addon1_trigger_pips = trig1;
   if(trig2 < m_addon2_trigger_pips) m_addon2_trigger_pips = trig2;
  }

//+------------------------------------------------------------------+
//| Read-only accessors used by CPyramidBridge for dynamic triggers  |
//+------------------------------------------------------------------+
double GetAddon1TriggerPips(void) { return(m_addon1_trigger_pips); }
double GetAddon2TriggerPips(void) { return(m_addon2_trigger_pips); }
bool   IsAddon1Open(void)          { return(m_state.addon1_open);   }
bool   IsAddon2Open(void)          { return(m_state.addon2_open);   }

//+------------------------------------------------------------------+
//| SetTrailParams: update trailing stop parameters at runtime       |
//| Used by the reserve-adaptive trail (Integration Point 4)         |
//+------------------------------------------------------------------+
void SetTrailParams(double trail_pips, double trail_step)
  {
   m_trail_pips      = trail_pips;
   m_trail_step_pips = trail_step;
  }

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


The Bridge Class

The full BetSizingPyramidBridge.mqh is in the attached archive. SBridgeConfig carries all bridge configuration in one struct passed to Init(). This keeps the EA's input section clean: the bridge configuration is assembled into a struct in OnInit() and passed once, rather than spreading a dozen parameters across separate function calls.

struct SBridgeConfig
  {
   //--- Point 1 — lot sizing
   double   max_lots;             // Maximum lots at full confidence
   double   addon1_ratio;         // lot_addon1 = lot_initial * ratio
   double   addon2_ratio;         // lot_addon2 = lot_initial * ratio

   //--- Point 2 — budget gate
   bool     use_budget_gate;
   double   budget_min_bet;       // Skip entry when headroom < this value

   //--- Point 3 — dynamic add-on trigger
   bool     use_dynamic_trigger;
   double   dynamic_level_1;      // |BetSizeDynamic| threshold for add-on 1
   double   dynamic_level_2;      // |BetSizeDynamic| threshold for add-on 2

   //--- Point 4 — reserve adaptive trail
   bool     use_reserve_trail;
   double   reserve_tighten_mul;  // Trail multiplier when reserve < alloc_ratio

   double   min_lot;              // Hard floor for lot_initial
  };

CPyramidBridge owns one private CPyramidEngine instance. It also stores the base trail parameters separately so the reserve trail logic can always restore them after tightening. The live lot values are stored after each TryOpenInitial() call to allow the allocation ratio calculation in Point 4 to run without querying broker positions. The public interface exposes five integration methods plus pass-throughs to the engine for operations the EA must call directly:

bool   Init(const SBridgeConfig &cfg, double trail_pips, double trail_step);
bool   InitEngine(int magic, int slip,
                 double lot_i, double lot_a1, double lot_a2,
                 double trig1, double trig2,
                 double stop1, double stop2,
                 bool   trail, double trail_pips, double trail_step);

//--- Points 1 and 3: open initial position with calibrated lots
bool   TryOpenInitial(long direction, double price, double sl,
                     double prob_bet_size, double dynamic_bet_size,
                     string comment = "Pyramid Entry");

//--- Point 2: budget gate check before entry
bool   IsBudgetClearForEntry(const datetime &open_t[], const datetime &close_t[],
                            const int &sides[], datetime now);

//--- Point 3: update dynamic trigger — call every tick when active
void   UpdateDynamicTrigger(double current_dyn_bet);

//--- Point 4: reserve-adaptive trail — call once per bar when active
void   AdaptTrailToReserve(double reserve_bet);

//--- Point 5: did the pyramid just close?
bool   WasJustClosed(bool was_active_before);

//--- Engine pass-throughs
void   Manage(void);
void   HandleTransaction(const MqlTradeTransaction &trans);
void   RecoverState(void);
bool   IsActive(void);
double GetUnifiedStop(void);

The separation between Init() and InitEngine() is deliberate. The bridge configuration is independent of the engine configuration: the two can be changed on separate development cycles without touching the other. The lot values passed to InitEngine() serve as placeholders only; they are overwritten by UpdateLots() at each TryOpenInitial() call.


Wiring a Complete Expert Advisor

Figure 3 shows what the five integration points produce on synthetic data over 30 bars: panel (a) plots the BetSizeProbability output, with threshold lines marking when add-on 1 (|bet_size| > 0.40) and add-on 2 (|bet_size| > 0.65) would be warranted; panel (b) shows the corresponding pyramid lot layers — the same structure shown abstractly in Figure 1 now sized dynamically by the classifier's confidence.

BetSizeProbability driving pyramid lot sizing

Figure 3. 2-panel illustration of BetSizeProbability output driving proportional pyramid lot allocation

  • Panel (a): Discretized bet_size from BetSizeProbability over 30 synthetic bars. The orange dotted line marks the add-on 2 threshold (0.65); the green dotted line marks the add-on 1 threshold (0.40).
  • Panel (b): Stacked pyramid lot layers derived from the signal above. Blue bars show lot_initial. Green shows lot_addon1 (0.60 × lot_initial). Orange shows lot_addon2 (0.30 × lot_initial), which only appears when |bet_size| > 0.65.

The full demonstration EA, BetSizingPyramidEA.mq5, wires all five integration points. The OnTick() sequence is:

//+------------------------------------------------------------------+
//| OnTick: entry point called on every market tick                  |
//| Runs dynamic trigger and Manage() on every tick; reserve trail   |
//| and entry evaluation run on new bars only                        |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   datetime current_bar = iTime(_Symbol, PERIOD_H1, 0);
   bool     is_new_bar  = (current_bar != g_last_bar);
   if(is_new_bar)
      g_last_bar = current_bar;

   //--- Point 3: update dynamic trigger on every tick
   if(g_bridge.IsActive())
     {
      double dyn_bet = MathAbs(BetSizeDynamic(
                                g_current_pos, InpMaxLots * 100,
                                SymbolInfoDouble(_Symbol, SYMBOL_BID),
                                GetForecastPrice(),
                                InpCalDiv * _Point,
                                InpCalBetSize, InpDynFunc).bet_size);
      g_bridge.UpdateDynamicTrigger(dyn_bet);
     }

   //--- Manage pyramid on every tick
   bool was_active = g_bridge.IsActive();
   g_bridge.Manage();

   //--- Point 5: sync arrays on pyramid close
   if(g_bridge.WasJustClosed(was_active))
      SyncSignalArraysOnClose();

   if(is_new_bar)
     {
      //--- Point 4: adapt trail once per bar
      if(g_bridge.IsActive() && InpUseReserveTrail)
        {
         BetSizeResult rv = BetSizeReserve(g_open_t, g_close_t, g_sides,
                                         TimeCurrent(), g_reserve_params);
         g_bridge.AdaptTrailToReserve(MathAbs(rv.bet_size));
        }
      if(!g_bridge.IsActive())
         CheckForEntry();
     }
  }

The call ordering reflects the different time scales of the five points. The dynamic trigger check and Manage() run on every tick because add-on triggers must respond to intrabar price movement. The reserve trail and entry signal run on new bars only: the trail adapts at bar granularity, and the entry signal is evaluated on completed bars to avoid signal noise within a bar.

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

//+------------------------------------------------------------------+
//| CheckForEntry: evaluate signal and submit via the bridge         |
//| Applies the budget gate (Point 2), computes the probability and  |
//| dynamic bet sizes, then calls TryOpenInitial                     |
//+------------------------------------------------------------------+
void CheckForEntry(void)
  {
   datetime now = TimeCurrent();

   //--- Point 2: budget gate
   if(!g_bridge.IsBudgetClearForEntry(g_open_t, g_close_t, g_sides, now))
      return;

   // ... evaluate signal, determine direction, price, sl ...

   //--- Point 1 source: probability bet size
   BetSizeResult prob_r = BetSizeProbability(
                           g_open_t, g_close_t, g_prob, g_pred,
                           2, InpStepSize, InpAvgActive, now);
   if(direction == POSITION_TYPE_BUY  && prob_r.bet_size <= 0.0) return;
   if(direction == POSITION_TYPE_SELL && prob_r.bet_size >= 0.0) return;

   //--- Point 3 source: dynamic bet size at entry bar
   double dyn_bet = MathAbs(BetSizeDynamic(
                             0, InpMaxLots * 100, price, GetForecastPrice(),
                             InpCalDiv * _Point,
                             InpCalBetSize, InpDynFunc).bet_size);

   if(g_bridge.TryOpenInitial(direction, price, sl,
                             prob_r.bet_size, dyn_bet, "Pyramid Entry"))
      AppendNewSignal(now, direction, MathAbs(prob_r.bet_size));
  }

Two calibration decisions must be made before deployment. First, addon1_ratio and addon2_ratio must satisfy the strict constraint: addon1_ratio must be less than 1.0, and addon2_ratio must be less than addon1_ratio. For instruments with coarse lot steps, check that the computed lots survive clipping 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. Run the bet-sizing stack over the warm-up data and inspect the distribution of |bet_size| values on bars where the position was profitable; the median and 75th percentile of that distribution are reasonable starting points for the two thresholds.


Conclusion

The Part 13 bet-sizing module and the pyramiding engine from Position Management: Safe Pyramiding with a Unified Stop in MQL5 solve different parts of the same problem. The sizing module answers how much capital a signal deserves, accounting for classifier confidence, label concurrency, and the empirical distribution of past positions. The pyramid engine answers how to structure that capital across layers with a mathematically provable risk-reduction property — the same property illustrated in Figure 1: each add-on is smaller than the previous, the unified stop advances, and total dollar risk falls at every stage. The two systems were not designed to work together; connecting them requires a thin adapter layer that translates sizing outputs into engine parameters.

CPyramidBridge implements that adapter across five integration points. The probability method drives initial lot sizing. The budget method gates entry when the signal book is at capacity. The dynamic method advances add-on triggers when the price divergence supports them. The reserve method tightens the trailing stop when the empirical distribution of imbalance contracts below the current allocation. A synchronization check keeps the signal arrays consistent when the pyramid closes. Each point is independently controllable through the SBridgeConfig struct; disabling any flag restores the original engine behavior for that aspect.

In Part 16 of the Blueprint series, the sizing layer is connected to the CPCV backtesting framework in the MetaTrader 5 Strategy Tester. The probability estimates flowing into BetSizeProbability are subject to systematic bias from class imbalance and calibration error; their characterization and correction are covered in Part 12.


Attached Files

  File Place in Depends On Description
 1 BetSizingUtils.mqh MQL5\Include\BetSizing\ Unchanged from Part 13. NormCDF, NormICDF, NormPDF, RawMoments, SweepLineActiveCounts, BetSizeResult, Clamp, MathSign.
 2 EF3M.mqh MQL5\Include\BetSizing\ BetSizingUtils.mqh Unchanged from Part 13. M2NParams, DeriveComponentParams, FitM2N, MixtureCDF, ReserveBetSize.
 3 Ch10Snippets.mqh MQL5\Include\BetSizing\ BetSizingUtils.mqh Unchanged from Part 13. GetSignal, AvgActiveSignals, DiscreteSignal, SigmoidBetSize, PowerBetSize, GetW, LimitPrice.
 4 BetSizing.mqh MQL5\Include\BetSizing\ Ch10Snippets.mqh, EF3M.mqh Unchanged from Part 13. BetSizeProbability, BetSizeDynamic, BetSizeBudget, BetSizeReserve, SeedBudgetMaxima.
 5 PyramidUtils.mqh MQL5\Include\Pyramid\ Unchanged from pyramiding article. Pip-value helpers and broker-level stop validation.
 6 PyramidEngine.mqh MQL5\Include\Pyramid\ PyramidUtils.mqh Original CPyramidEngine receives seven new public methods: UpdateLots, SetAddonTriggerPips, GetAddon1TriggerPips, GetAddon2TriggerPips, IsAddon1Open, IsAddon2Open, SetTrailParams. Paste into CPyramidEngine's 
 7 BetSizingPyramidBridge.mqh MQL5\Include\Pyramid\ BetSizing.mqh, PyramidEngine.mqh SBridgeConfig struct and CPyramidBridge class implementing all five integration points.
 8 BetSizingPyramidEA.mq5 MQL5\Experts\ BetSizingPyramidBridge.mqh Demonstration EA wiring an EMA crossover signal to the bridge. All five integration points are active. Replace CheckForEntry() with any classifier output.


References


Attached files |
MQL5.zip (33.87 KB)
Custom Debugging and Profiling Tools for MQL5 Development (Part II): Profiling EAs and Testing Trading Logic 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 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 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 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.