Automating Classic Market Methods in MQL5 (Part 6): Jesse Livermore's Pivotal Point System
Introduction
Jesse Livermore is the most quoted trader in history. "It was never my thinking that made the big money for me; it was always my sitting." The "markets are never wrong—opinions often are." "The big money is not in the buying and the selling but in the sitting." His words fill trading books published a century after he wrote them.
What is quoted far less often is the precise, rule-based system behind those words. Livermore was not a discretionary trader in the modern sense. He did not trade on gut feel or news. He developed what he called the "Market Key." It classifies market behavior into four states, identifies pivotal price levels, and scales into positions only when price confirms the anticipated direction. When the market behaved abnormally, he exited. He did not argue with it.
This article builds the market key as a state machine in MQL5. The states are uptrend, natural reaction, natural rally, and downtrend—exactly as Livermore defined them. The EA identifies the pivotal point from a prior consolidation and confirms it with volume expansion on the breakout bar. The entry pyramids across four tranches as price continues to confirm the move. The exit fires the moment the price shows the volume signature Livermore called "abnormal."
Before any trade logic runs, "OnInit()" validates all inputs: the four tranche percentages must sum to exactly 100, the account must be a hedging account, and key parameters must be within sensible bounds. The EA refuses to start if any validation fails. This is not defensive over-engineering. It is the minimum correctness guarantee required for a four-tranche pyramid system.
The central question this EA is designed to answer is this:
Has price broken a pivotal level on expanding volume, confirmed the move through a low-volume natural reaction and a high-volume resumption, and shown no abnormal behavior that would signal the trend has ended?
We will cover the following topics:
- Livermore's Market Key: Theory and Rules
- What Makes a Valid Signal in This Implementation
- Architecture—The State Machine and the Pivot Engine
- Implementation in MQL5
- Known Limitations
- Conclusion
Livermore's Market Key: Theory and Rules
Livermore described his market key in his 1940 book "How to Trade in Stocks." The system classifies every bar into one of four market states and tracks pivotal price levels—the high-water marks and low-water marks of the current campaign.

Fig. 1. Livermore's market key overview.
The Four States
- An uptrend is active when price has broken above a pivotal high on expanding volume and has not shown abnormal behavior since.
- A natural reaction is the expected low-volume pullback that follows an advance. Volume contracts during the reaction. This is healthy and expected. Livermore used it as an opportunity to add to his position when the reaction ended and the advance resumed. A reaction that occurs in high volume is not natural—it is abnormal, and Livermore treated it as an exit signal.
- A natural rally is the mirror of the natural reaction—the expected low-volume bounce within a downtrend. Volume contracts during the rally. When the rally ends and the price resumes downward on expanding volume, Livermore adds to his short position.
- A downtrend is active when price has broken below a pivotal low on expanding volume and has not shown abnormal behavior since.

Fig. 2. The four market states.
The Pivotal Point
A pivotal point is a specific price level where the market makes a decisive move after a period of consolidation. The EA identifies consolidation as a window of "InpConsolBars," recent bars whose range is narrower than "InpConsolATR" times the ATR. The highest high of that window is the bull pivot. The lowest low is the bear pivot. These values are frozen the moment the zone is identified—they do not change while the EA waits for the breakout.
The stop for all tranches is placed at the pivot level itself: the consolidation low for long campaigns and the consolidation high for short campaigns. This is Livermore's own stop specification—price returning to the pivot means the breakout premise has failed.
The breakout must clear the frozen pivot by at least "InpPivotClearATR" times the ATR and must occur on volume that exceeds the consolidation average by "InpVolExpansion." A breakout that barely ticks above the pivot on weak volume is not a Livermore signal.
The one-day reversal: if a bar's high exceeds the pivot but the bar closes back below it, the breakout is rejected. The closing price is what matters—a bar that reached above the pivot and reversed before the close is a warning signal, not a confirmation. Note that this check is distinct from the breakout bar check: a genuine bull breakout requires the close to be above the pivot plus clearance. The one-day reversal fires when the high exceeded the pivot but the close did not.
Natural Reaction and Resumption
After a high-volume breakout, Livermore expected a pause. During the natural reaction, volume contracts. Price retraces somewhat but does not break back through the pivot. Within a few sessions, volume expands again, and price resumes the advance. This resumption is a second confirmation—and the signal for the second and third pyramid tranches.
Abnormal Behavior
If the volume during the reaction was actually high, or if price broke back through the pivot during the reaction, the pattern had failed. Livermore called this "abnormal" behavior.
Livermore's exit signal was explicit: if price makes a large move against the trend on expanding volume, the campaign is over. He did not wait for a specific price level to be broken. He exited on the volume signature.
In this EA, abnormal behavior is defined as a bar that moves against the trend by more than one ATR and has volume above the average of the current campaign. When this occurs, all units are closed immediately regardless of where the stop is.
The Pyramid Entry
Livermore never entered his full position at the pivot. He tested the market first. The structure is four tranches, and the risk percentage applies to the full four-tranche position combined—not to each tranche individually. The full position size is calculated once at the first tranche entry and stored. All subsequent tranches are expressed as percentages of that stored value.
- Tranche 1 is the probe—20% of the intended position. It enters on the breakout bar. If the market immediately reverses, only 20% is at risk.
- Tranche 2 enters when the price advances "InpTranche2ATR" from the first entry, confirming the initial move had follow-through.
- Tranche 3 enters when the natural reaction ends and price resumes—a bar that breaks above the reaction swing high on expanding volume.
- Tranche 4 completes the position when the price advances a further "InpTranche4ATR" from the third tranche entry. It cannot open on the same bar as Tranche 3—a minimum one-bar confirmation is required.
Livermore's exit signal: a bar that moves against the trend by more than "InpAbnormalMoveATR" times the ATR on volume above "InpAbnormalVolMult" times the campaign average. When this occurs, all units close immediately. A reaction that shows high volume rather than contracting volume also triggers an exit—it is, by definition, abnormal.
What Makes a Valid Signal in This Implementation?
Six conditions must be passed before the first tranche enters.
- The consolidation condition: the last "InpConsolBars" completed bars must have a range narrower than "InpConsolATR" times the ATR. The zone is frozen immediately—the high, low, and average volumes are never updated while waiting for the breakout.
- The volume expansion condition: breakout bar volume must exceed the frozen consolidation average by "InpVolExpansion." Default is 1.2-20% above average.
- The pivot clearance condition: the breakout bar's close must exceed the frozen pivot by at least "InpPivotClearATR" times the ATR. The default is 0.3 ATR.
- The no-reversal condition: the bar's high must not have exceeded the pivot while its close fell back below it. This is the one-day reversal—high above the pivot, and close below the pivot.
- The account mode condition: the account must be a hedging account. Each tranche is a separate position with its own ticket. Netting accounts cannot support this architecture.
- The input validation condition: tranche percentages must sum to 100, and all key parameters must be within valid ranges. Validation runs in "OnInit()" before any other logic.
For the natural reaction to be valid, the price must pull back at least "InpReactionATR" from the swing high, and volume during the reaction must be below "InpReactionVolRatio" times the campaign average (default 0.8—volume must contract). High-volume reactions are treated as abnormal behavior and close the campaign.
To confirm the resumption, a reaction bar must break above the reaction swing-high reference on volume above the campaign average. This is the specific price event Livermore described as the end of the reaction—not just any upward close but a genuine breakout of the reaction's own swing high on expanding volume.
Architecture—The State Machine and the Pivot Engine
The EA uses six states: "STATE_IDLE," "STATE_CONSOLIDATING," "STATE_UPTREND," "STATE_NATURAL_REACTION," "STATE_DOWNTREND," and "STATE_NATURAL_RALLY."
The pivot engine runs in "STATE_IDLE" through "LockConsolidation," which freezes the zone on the first qualifying scan, then transitions to "STATE_CONSOLIDATING." In "STATE_CONSOLIDATING," the frozen zone is never re-scanned—only "CheckPivotBreak()" runs on each new bar. This design ensures the breakout bar's close is always compared against a static reference that the breakout bar itself cannot contaminate.
The campaign struct stores "full_lots"—the full position size computed once at Tranche 1. All subsequent tranches call "TrancheLots(full_lots, pct)" to derive their size. If a tranche's calculated lot size rounds below the broker's minimum, "TrancheLots()" returns 0 and the tranche is skipped—it never silently substitutes "min_lot" and creates unintended risk.
"CloseAllUnits()" iterates all four slots, 0 through 3, unconditionally, checking each ticket for validity. It does not use "units_open" as the loop bound—this prevents gaps in the ticket array from causing positions to be missed when closing.
The EA is designed for hedging accounts only. This is validated in "OnInit," and the EA refuses to start on netting accounts.
Implementation in MQL5
The EA is built section by section.
Includes, Enumerations, and Input Parameters
//+------------------------------------------------------------------+ //| LivermoreEA.mq5 | //| Copyright 2026, Tola Moses Hector | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Tola Moses Hector" #property link "https://www.mql5.com" #property version "1.00" #property description "Automating Classic Market Methods in MQL5 Part 6" #property description "Jesse Livermore Pivotal Point System" #property description "Market Key states, pyramid entry, abnormal behavior exit" #property description "Daily timeframe — hedging account required" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Market Key State Machine | //+------------------------------------------------------------------+ enum ENUM_LV_STATE { STATE_IDLE, // No structure active — scanning for consolidation STATE_CONSOLIDATING, // Consolidation zone locked — watching for pivot break STATE_UPTREND, // Pivot broken upward — campaign active long STATE_NATURAL_REACTION, // Low-volume pullback within uptrend STATE_DOWNTREND, // Pivot broken downward — campaign active short STATE_NATURAL_RALLY // Low-volume bounce within downtrend }; //+------------------------------------------------------------------+ //| Input Parameters | //+------------------------------------------------------------------+ input group "=== Pivot Detection ===" input int InpConsolBars = 10; //--- Minimum bars to form a consolidation zone input double InpConsolATR = 2.5; //--- Maximum consolidation range in ATR units input double InpVolExpansion = 1.2; //--- Volume multiplier required on breakout bar input double InpPivotClearATR = 0.3; //--- Minimum pivot clearance in ATR units input group "=== Trend and Reaction ===" input double InpReactionATR = 1.0; //--- Minimum reaction depth in ATR to confirm Natural Reaction input double InpReactionVolRatio= 0.8; //--- Reaction volume must be below this ratio of campaign avg input double InpAbnormalVolMult = 1.5; //--- Volume multiplier that defines abnormal behavior input double InpAbnormalMoveATR = 1.0; //--- Move size in ATR that defines abnormal behavior input group "=== Pyramid Entry ===" input double InpTranche1Pct = 20.0; //--- Tranche 1 size as percent of full position input double InpTranche2Pct = 20.0; //--- Tranche 2 size as percent of full position input double InpTranche3Pct = 20.0; //--- Tranche 3 size as percent of full position input double InpTranche4Pct = 40.0; //--- Tranche 4 size as percent of full position (must sum to 100) input double InpTranche2ATR = 1.0; //--- ATR advance from T1 to trigger T2 entry input double InpTranche4ATR = 1.0; //--- ATR advance from T3 to trigger T4 entry input group "=== Risk ===" input double InpRiskPercent = 1.0; //--- Total position risk as percent of balance input int InpATRPeriod = 14; //--- ATR period input group "=== General ===" input int InpMagicNumber = 100601; //--- Magic number input int InpSlippage = 10; //--- Slippage in points input bool InpShowLabels = true; //--- Draw labels on chart
The four tranche percentages must sum to 100. "OnInit()" enforces this and refuses to start if the sum differs by more than 0.01. The comment on "InpTranche1Pct" makes this requirement visible in the input panel.
Input Validation
"ValidateInputs()" runs before any other initialization. It checks the tranche sum, the key parameter ranges, and the account mode.
//+------------------------------------------------------------------+ //| Validates all input parameters before EA start | //+------------------------------------------------------------------+ bool ValidateInputs() { double tranche_sum = InpTranche1Pct + InpTranche2Pct + InpTranche3Pct + InpTranche4Pct; if(MathAbs(tranche_sum - 100.0) > 0.01) { Print("LivermoreEA: Tranche percentages must sum to 100. Current sum: ", tranche_sum); return false; } if(InpConsolBars < 3) { Print("LivermoreEA: InpConsolBars must be >= 3."); return false; } if(InpATRPeriod < 2) { Print("LivermoreEA: InpATRPeriod must be >= 2."); return false; } if(InpRiskPercent <= 0) { Print("LivermoreEA: InpRiskPercent must be > 0."); return false; } if(InpVolExpansion < 1.0) { Print("LivermoreEA: InpVolExpansion must be >= 1.0."); return false; } if(InpTranche1Pct <= 0 || InpTranche2Pct <= 0 || InpTranche3Pct <= 0 || InpTranche4Pct <= 0) { Print("LivermoreEA: All tranche percentages must be > 0."); return false; } //--- Require hedging account mode if(AccountInfoInteger(ACCOUNT_MARGIN_MODE) != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { Print("LivermoreEA: Hedging account required. This EA uses individual position tickets per tranche."); return false; } return true; }
The Campaign State Structure
The structure stores "full_lots"—the full position size computed once at Tranche 1—and "t3_open_time" to prevent Tranche 4 from opening on the same bar as Tranche 3. The tickets array is always indexed by slots 0 through 3, never by "units_open."
//+------------------------------------------------------------------+ //| Active campaign state | //+------------------------------------------------------------------+ struct SLivermoreCampaign { bool is_long; double pivot_price; double consol_high; // Frozen consolidation zone high double consol_low; // Frozen consolidation zone low double avg_consol_vol; // Frozen consolidation average volume double avg_campaign_vol; // Running average volume during campaign int campaign_bars; int units_open; double full_lots; // Computed once at T1 — used by all tranches double entry_prices[4]; double lots[4]; ulong tickets[4]; // Indexed by slot [0..3] — not by units_open double t1_entry; double t3_entry; datetime t3_open_time; // Bar time T3 opened — prevents T4 on same bar bool t2_open; bool t3_open; bool t4_open; double reaction_high; // Swing high reference for reaction detection double reaction_low_bar; // Lowest close reached during Natural Reaction double reaction_avg_vol; // Average volume during the reaction int reaction_bars; // Number of bars in the current reaction };
The "Frozen" comment on "consol_high," "consol_low," and "avg_consol_vol" reflects the design intent: once these three fields are written by "LockConsolidation()," they are never modified again until the campaign resets. The breakout bar is always measured against a static reference.
Global Variables
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ ENUM_LV_STATE g_state = STATE_IDLE; SLivermoreCampaign g_campaign; CTrade g_trade; int g_atr_handle = INVALID_HANDLE; datetime g_last_bar = 0; int g_consol_watch = 0;
Utility Functions
//+------------------------------------------------------------------+ //| Returns pip size for the current symbol | //+------------------------------------------------------------------+ double PipSize() { int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point; } //+------------------------------------------------------------------+ //| Computes full-position lot size from risk and stop distance | //+------------------------------------------------------------------+ double CalcFullLots(double sl_pips) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double risk_amt = balance * InpRiskPercent / 100.0; double tick_val = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double pip_size = PipSize(); if(tick_size <= 0 || tick_val <= 0 || sl_pips <= 0) return 0; double pip_value = (pip_size / tick_size) * tick_val; double lots = risk_amt / (sl_pips * pip_value); double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); if(step <= 0 || min_lot <= 0 || max_lot <= 0) return 0; lots = MathFloor(lots / step) * step; return MathMax(min_lot, MathMin(max_lot, lots)); } //+------------------------------------------------------------------+ //| Returns lot size for a specific tranche from the stored full_lots| //| Returns 0 — not min_lot — if calculated size is below minimum | //+------------------------------------------------------------------+ double TrancheLots(double full_lots, double pct) { double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); if(step <= 0 || min_lot <= 0 || max_lot <= 0 || full_lots <= 0 || pct <= 0) return 0; double lots = MathFloor((full_lots * pct / 100.0) / step) * step; lots = NormalizeDouble(lots, 2); if(lots < min_lot) return 0; // Do not open if below minimum — never substitute min_lot if(lots > max_lot) lots = max_lot; return lots; }"TrancheLots()" returning 0 when the calculated size is below minimum is an important safety decision. On small accounts where the full position size is minimal, a 20% tranche might round below the minimum lot. Substituting "min_lot" would open a position larger than intended relative to the risk model. Returning 0 skips the tranche cleanly—the EA logs the skip and continues without opening an unintended size.
Chart Drawing Helpers
//+------------------------------------------------------------------+ //| Draws a horizontal line | //+------------------------------------------------------------------+ void DrawHLine(string name, double price, color clr, ENUM_LINE_STYLE style) { if(!InpShowLabels) return; string obj = "LV_" + name; ObjectDelete(0, obj); ObjectCreate(0, obj, OBJ_HLINE, 0, 0, price); ObjectSetInteger(0, obj, OBJPROP_COLOR, clr); ObjectSetInteger(0, obj, OBJPROP_STYLE, style); ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1); ChartRedraw(0); } //+------------------------------------------------------------------+ //| Places a text label | //+------------------------------------------------------------------+ void DrawLabel(string name, datetime time, double price, string text, color clr) { if(!InpShowLabels) return; string obj = "LV_" + name; ObjectDelete(0, obj); ObjectCreate(0, obj, OBJ_TEXT, 0, time, price); ObjectSetString(0, obj, OBJPROP_TEXT, text); ObjectSetInteger(0, obj, OBJPROP_COLOR, clr); ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9); ChartRedraw(0); } //+------------------------------------------------------------------+ //| Removes all chart objects created by this EA | //+------------------------------------------------------------------+ void ClearLabels() { int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "LV_") == 0) ObjectDelete(0, name); } ChartRedraw(0); } //+------------------------------------------------------------------+ //| Resets the campaign structure | //+------------------------------------------------------------------+ void ResetCampaign() { g_campaign.is_long = true; g_campaign.pivot_price = 0; g_campaign.consol_high = 0; g_campaign.consol_low = 0; g_campaign.avg_consol_vol = 0; g_campaign.avg_campaign_vol = 0; g_campaign.campaign_bars = 0; g_campaign.units_open = 0; g_campaign.full_lots = 0; g_campaign.t1_entry = 0; g_campaign.t3_entry = 0; g_campaign.t3_open_time = 0; g_campaign.t2_open = false; g_campaign.t3_open = false; g_campaign.t4_open = false; g_campaign.reaction_high = 0; g_campaign.reaction_low_bar = 0; g_campaign.reaction_avg_vol = 0; g_campaign.reaction_bars = 0; g_consol_watch = 0; for(int i = 0; i < 4; i++) { g_campaign.entry_prices[i] = 0; g_campaign.lots[i] = 0; g_campaign.tickets[i] = 0; } }
Locking the Consolidation Zone
"LockConsolidation()" scans "InpConsolBars" completed bars once and freezes the zone. The frozen values are never updated while in "STATE_CONSOLIDATING."
//+------------------------------------------------------------------+ //| Locks consolidation zone — called only from STATE_IDLE | //+------------------------------------------------------------------+ bool LockConsolidation(double atr) { double high[], low[]; long vol[]; ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(vol, true); if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, InpConsolBars, high) < InpConsolBars) return false; if(CopyLow(_Symbol, PERIOD_CURRENT, 1, InpConsolBars, low) < InpConsolBars) return false; if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 1, InpConsolBars, vol) < InpConsolBars) return false; double rh = 0, rl = DBL_MAX, avg_vol = 0; for(int i = 0; i < InpConsolBars; i++) { if(high[i] > rh) rh = high[i]; if(low[i] < rl) rl = low[i]; avg_vol += (double)vol[i]; } avg_vol /= InpConsolBars; double range = rh - rl; if(range > atr * InpConsolATR || range <= 0) return false; g_campaign.consol_high = rh; g_campaign.consol_low = rl; g_campaign.avg_consol_vol = avg_vol; Print(StringFormat("LivermoreEA: Zone LOCKED | High:%.5f | Low:%.5f | Range:%.5f | AvgVol:%.0f", rh, rl, range, avg_vol)); DrawHLine("CONSOL_HIGH", rh, clrSilver, STYLE_DOT); DrawHLine("CONSOL_LOW", rl, clrSilver, STYLE_DOT); return true; }
Checking for a Pivot Breakout
"CheckPivotBreak()" compares the last completed bar against the frozen zone. The one-day reversal check is now correctly formulated: the bar's high exceeded the pivot while its close fell back below it—two separate price levels, which can simultaneously be true.
//+------------------------------------------------------------------+ //| Checks for pivot breakout against frozen consolidation zone | //| Returns 1 bull, -1 bear, 0 none | //| One-day reversal: high exceeded pivot but bar closed below it | //+------------------------------------------------------------------+ int CheckPivotBreak(double atr) { double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1); double low1 = iLow(_Symbol, PERIOD_CURRENT, 1); long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); double clearance = atr * InpPivotClearATR; if((double)vol1 < g_campaign.avg_consol_vol * InpVolExpansion) return 0; //--- Bull breakout: close above consol high plus clearance if(close1 > g_campaign.consol_high + clearance) { //--- One-day reversal: high exceeded pivot but bar closed back below pivot //--- This is the correct formulation: high > pivot AND close < pivot if(high1 > g_campaign.consol_high && close1 < g_campaign.consol_high) { Print("LivermoreEA: One-day reversal — high exceeded pivot but closed below it. Rejected."); return 0; } g_campaign.pivot_price = g_campaign.consol_high; Print(StringFormat("LivermoreEA: BULL BREAK | Close:%.5f | Pivot:%.5f | Vol:%I64d", close1, g_campaign.pivot_price, vol1)); return 1; } //--- Bear breakout: close below consol low minus clearance if(close1 < g_campaign.consol_low - clearance) { //--- One-day reversal: low exceeded pivot but closed above it if(low1 < g_campaign.consol_low && close1 > g_campaign.consol_low) { Print("LivermoreEA: One-day reversal (bear) — rejected."); return 0; } g_campaign.pivot_price = g_campaign.consol_low; Print(StringFormat("LivermoreEA: BEAR BREAK | Close:%.5f | Pivot:%.5f | Vol:%I64d", close1, g_campaign.pivot_price, vol1)); return -1; } return 0; }
Opening a Pyramid Tranche
"OpenTranche()" uses "g_campaign.full_lots" through the "TrancheLots()" call in the state machine—not a fresh "CalcFullLots()" call. If lots come back as 0, the tranche is logged and skipped.
//+------------------------------------------------------------------+ //| Opens one pyramid tranche using stored full_lots | //+------------------------------------------------------------------+ bool OpenTranche(int slot, double lots, bool is_long, double sl_price, string label) { if(lots <= 0) { Print(StringFormat("LivermoreEA: %s skipped — calculated lots below minimum.", label)); return false; } long stop_lv = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); double min_dist = stop_lv * _Point; bool ok = false; if(is_long) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(ask - sl_price < min_dist) sl_price = NormalizeDouble(ask - min_dist - _Point, _Digits); ok = g_trade.Buy(lots, _Symbol, ask, sl_price, 0, "Livermore T" + IntegerToString(slot + 1)); if(ok) { g_campaign.entry_prices[slot] = ask; g_campaign.lots[slot] = lots; ulong deal = g_trade.ResultDeal(); if(deal > 0 && HistoryDealSelect(deal)) g_campaign.tickets[slot] = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID); else g_campaign.tickets[slot] = g_trade.ResultOrder(); g_campaign.units_open++; datetime t = iTime(_Symbol, PERIOD_CURRENT, 1); DrawLabel(label, t, iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 5, label, clrDodgerBlue); Print(StringFormat("LivermoreEA: %s | Lots:%.2f | Entry:%.5f | SL:%.5f", label, lots, ask, sl_price)); } } else { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(sl_price - bid < min_dist) sl_price = NormalizeDouble(bid + min_dist + _Point, _Digits); ok = g_trade.Sell(lots, _Symbol, bid, sl_price, 0, "Livermore T" + IntegerToString(slot + 1)); if(ok) { g_campaign.entry_prices[slot] = bid; g_campaign.lots[slot] = lots; ulong deal = g_trade.ResultDeal(); if(deal > 0 && HistoryDealSelect(deal)) g_campaign.tickets[slot] = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID); else g_campaign.tickets[slot] = g_trade.ResultOrder(); g_campaign.units_open++; datetime t = iTime(_Symbol, PERIOD_CURRENT, 1); DrawLabel(label, t, iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 5, label, clrOrangeRed); Print(StringFormat("LivermoreEA: %s | Lots:%.2f | Entry:%.5f | SL:%.5f", label, lots, bid, sl_price)); } } return ok; }
Closing All Campaign Units
"CloseAllUnits()" iterates all four slots unconditionally. Skipping slots with a zero ticket handles the case where tranches were skipped due to minimum lot constraints or were never opened.
//+------------------------------------------------------------------+ //| Closes all open campaign units — iterates all 4 slots | //+------------------------------------------------------------------+ void CloseAllUnits(string reason) { double total_profit = 0; for(int i = 3; i >= 0; i--) // Always iterate all 4 slots, not units_open { ulong ticket = g_campaign.tickets[i]; if(ticket == 0) continue; // Slot was never filled if(!PositionSelectByTicket(ticket)) continue; // Already closed total_profit += PositionGetDouble(POSITION_PROFIT); if(!g_trade.PositionClose(ticket)) Print("LivermoreEA: Close failed | Ticket:", ticket); } Print(StringFormat("LivermoreEA: Campaign closed | Reason:%s | P&L:%.2f | Units:%d", reason, total_profit, g_campaign.units_open)); g_state = STATE_IDLE; ResetCampaign(); ClearLabels(); }
Checking for Abnormal Behavior
"CheckAbnormalBehavior()" runs on every bar during an active campaign.
//+------------------------------------------------------------------+ //| Returns true if last bar showed abnormal behavior | //+------------------------------------------------------------------+ bool CheckAbnormalBehavior(double atr) { double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); double open1 = iOpen(_Symbol, PERIOD_CURRENT, 1); long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); double move = MathAbs(close1 - open1); if(move < atr * InpAbnormalMoveATR) return false; if(g_campaign.avg_campaign_vol <= 0) return false; if((double)vol1 < g_campaign.avg_campaign_vol * InpAbnormalVolMult) return false; bool against_trend = (g_campaign.is_long && close1 < open1) || (!g_campaign.is_long && close1 > open1); if(!against_trend) return false; Print(StringFormat("LivermoreEA: ABNORMAL BEHAVIOR | Move:%.5f | Vol:%I64d | AvgVol:%.0f", move, vol1, g_campaign.avg_campaign_vol)); return true; }
The State Machine
"ProcessStateMachine()" implements the complete market key. Key changes from earlier versions: full_lots is stored once at T1 and reused for T2 through T4; the natural reaction requires volume contraction; resumption requires a genuine break of the reaction swing high; T4 is blocked on the same bar as T3. //+------------------------------------------------------------------+ //| Processes the Livermore Market Key state machine for one bar | //+------------------------------------------------------------------+ void ProcessStateMachine(double atr) { //--- Update campaign running average volume if(g_state == STATE_UPTREND || g_state == STATE_NATURAL_REACTION || g_state == STATE_DOWNTREND || g_state == STATE_NATURAL_RALLY) { g_campaign.campaign_bars++; long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); g_campaign.avg_campaign_vol = (g_campaign.avg_campaign_vol * (g_campaign.campaign_bars - 1) + (double)vol1) / g_campaign.campaign_bars; } switch(g_state) { //-------------------------------------------------------------- case STATE_IDLE: if(LockConsolidation(atr)) { g_state = STATE_CONSOLIDATING; g_consol_watch = 0; } break; //-------------------------------------------------------------- case STATE_CONSOLIDATING: { g_consol_watch++; if(g_consol_watch > InpConsolBars * 3) { Print("LivermoreEA: Consolidation watch expired — resetting."); g_state = STATE_IDLE; ResetCampaign(); ClearLabels(); break; } int breakout = CheckPivotBreak(atr); if(breakout == 0) break; bool is_long = (breakout == 1); //--- Stop placed at the pivot level (opposite side of consolidation) //--- This matches the article: stop at pivot = consol_low for longs, consol_high for shorts double sl_price = is_long ? g_campaign.consol_low : g_campaign.consol_high; double entry_px = is_long ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl_pips = MathAbs(entry_px - sl_price) / PipSize(); //--- Compute full_lots ONCE and store in campaign — used by all tranches g_campaign.full_lots = CalcFullLots(sl_pips); double t1_lots = TrancheLots(g_campaign.full_lots, InpTranche1Pct); g_campaign.is_long = is_long; if(OpenTranche(0, t1_lots, is_long, sl_price, "T1")) { g_campaign.t1_entry = g_campaign.entry_prices[0]; g_campaign.reaction_high = iHigh(_Symbol, PERIOD_CURRENT, 1); // Track swing reference DrawHLine("PIVOT", g_campaign.pivot_price, clrOrange, STYLE_DASH); DrawHLine("STOP", sl_price, clrCrimson, STYLE_DOT); g_state = is_long ? STATE_UPTREND : STATE_DOWNTREND; } else { g_state = STATE_IDLE; ResetCampaign(); } } break; //-------------------------------------------------------------- case STATE_UPTREND: { if(CheckAbnormalBehavior(atr)) { CloseAllUnits("Abnormal Behavior"); break; } if(iClose(_Symbol, PERIOD_CURRENT, 1) < g_campaign.pivot_price) { CloseAllUnits("Pivot Failure"); break; } double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1); double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); //--- Track the running swing high for reaction detection if(high1 > g_campaign.reaction_high) g_campaign.reaction_high = high1; //--- T2: use stored full_lots, not a new CalcFullLots call if(!g_campaign.t2_open && close1 >= g_campaign.t1_entry + atr * InpTranche2ATR) { double lots = TrancheLots(g_campaign.full_lots, InpTranche2Pct); if(OpenTranche(1, lots, true, g_campaign.pivot_price, "T2")) g_campaign.t2_open = true; } //--- Natural Reaction: price pulls back InpReactionATR from tracked swing high if(g_campaign.reaction_high - close1 > atr * InpReactionATR) { g_campaign.reaction_low_bar = close1; g_campaign.reaction_avg_vol = (double)iTickVolume(_Symbol, PERIOD_CURRENT, 1); g_campaign.reaction_bars = 1; g_state = STATE_NATURAL_REACTION; Print(StringFormat("LivermoreEA: Natural Reaction started | SwingHigh:%.5f | Close:%.5f", g_campaign.reaction_high, close1)); } } break; //-------------------------------------------------------------- case STATE_NATURAL_REACTION: { if(CheckAbnormalBehavior(atr)) { CloseAllUnits("Abnormal Behavior During Reaction"); break; } if(iClose(_Symbol, PERIOD_CURRENT, 1) < g_campaign.pivot_price) { CloseAllUnits("Pivot Failure in Reaction"); break; } long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1); //--- Track reaction stats g_campaign.reaction_bars++; g_campaign.reaction_avg_vol = (g_campaign.reaction_avg_vol * (g_campaign.reaction_bars - 1) + (double)vol1) / g_campaign.reaction_bars; if(close1 < g_campaign.reaction_low_bar) g_campaign.reaction_low_bar = close1; //--- Volume must be contracting (below campaign average) for a genuine reaction bool vol_contracting = (g_campaign.avg_campaign_vol <= 0 || (double)vol1 < g_campaign.avg_campaign_vol * InpReactionVolRatio); if(!vol_contracting) { //--- High volume during reaction = not a Natural Reaction = abnormal Print("LivermoreEA: High volume during reaction — treating as abnormal."); CloseAllUnits("High Volume Reaction"); break; } //--- Resumption: price breaks above the reaction swing high on expanding volume //--- Use the reaction high (highest high during reaction period) double reaction_swing_high = g_campaign.reaction_high; bool expanding_vol = (g_campaign.avg_campaign_vol > 0 && (double)vol1 > g_campaign.avg_campaign_vol); bool broke_reaction_high = (high1 > reaction_swing_high); if(expanding_vol && broke_reaction_high) { if(!g_campaign.t3_open) { double lots = TrancheLots(g_campaign.full_lots, InpTranche3Pct); if(OpenTranche(2, lots, true, g_campaign.pivot_price, "T3")) { g_campaign.t3_entry = g_campaign.entry_prices[2]; g_campaign.t3_open_time = iTime(_Symbol, PERIOD_CURRENT, 1); g_campaign.t3_open = true; } } //--- Reset reaction high to new high for next reaction detection g_campaign.reaction_high = high1; g_state = STATE_UPTREND; Print("LivermoreEA: Natural Reaction ended — resuming uptrend."); } } break; //-------------------------------------------------------------- case STATE_DOWNTREND: { if(CheckAbnormalBehavior(atr)) { CloseAllUnits("Abnormal Behavior"); break; } if(iClose(_Symbol, PERIOD_CURRENT, 1) > g_campaign.pivot_price) { CloseAllUnits("Pivot Failure"); break; } double low1 = iLow(_Symbol, PERIOD_CURRENT, 1); double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); if(low1 < g_campaign.reaction_high || g_campaign.reaction_high == 0) g_campaign.reaction_high = low1; // Track swing low reference for downtrend if(!g_campaign.t2_open && close1 <= g_campaign.t1_entry - atr * InpTranche2ATR) { double lots = TrancheLots(g_campaign.full_lots, InpTranche2Pct); if(OpenTranche(1, lots, false, g_campaign.pivot_price, "T2")) g_campaign.t2_open = true; } //--- Natural Rally: price bounces InpReactionATR from tracked swing low double swing_low = g_campaign.reaction_high; // reused field for short campaigns if(close1 - swing_low > atr * InpReactionATR) { g_campaign.reaction_low_bar = close1; g_campaign.reaction_avg_vol = (double)iTickVolume(_Symbol, PERIOD_CURRENT, 1); g_campaign.reaction_bars = 1; g_state = STATE_NATURAL_RALLY; Print("LivermoreEA: Natural Rally started."); } } break; //-------------------------------------------------------------- case STATE_NATURAL_RALLY: { if(CheckAbnormalBehavior(atr)) { CloseAllUnits("Abnormal Behavior During Rally"); break; } if(iClose(_Symbol, PERIOD_CURRENT, 1) > g_campaign.pivot_price) { CloseAllUnits("Pivot Failure in Rally"); break; } long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); double low1 = iLow(_Symbol, PERIOD_CURRENT, 1); g_campaign.reaction_bars++; g_campaign.reaction_avg_vol = (g_campaign.reaction_avg_vol * (g_campaign.reaction_bars - 1) + (double)vol1) / g_campaign.reaction_bars; bool vol_contracting = (g_campaign.avg_campaign_vol <= 0 || (double)vol1 < g_campaign.avg_campaign_vol * InpReactionVolRatio); if(!vol_contracting) { Print("LivermoreEA: High volume during rally — abnormal."); CloseAllUnits("High Volume Rally"); break; } double reaction_swing_low = g_campaign.reaction_high; // swing low reference bool expanding_vol = (g_campaign.avg_campaign_vol > 0 && (double)vol1 > g_campaign.avg_campaign_vol); bool broke_reaction_low = (low1 < reaction_swing_low); if(expanding_vol && broke_reaction_low) { if(!g_campaign.t3_open) { double lots = TrancheLots(g_campaign.full_lots, InpTranche3Pct); if(OpenTranche(2, lots, false, g_campaign.pivot_price, "T3")) { g_campaign.t3_entry = g_campaign.entry_prices[2]; g_campaign.t3_open_time = iTime(_Symbol, PERIOD_CURRENT, 1); g_campaign.t3_open = true; } } g_campaign.reaction_high = low1; g_state = STATE_DOWNTREND; Print("LivermoreEA: Natural Rally ended — resuming downtrend."); } } break; } //--- T4: after T3, further InpTranche4ATR advance — NOT on same bar as T3 if((g_state == STATE_UPTREND || g_state == STATE_DOWNTREND) && g_campaign.t3_open && !g_campaign.t4_open && g_campaign.t3_entry > 0) { datetime current_bar_time = iTime(_Symbol, PERIOD_CURRENT, 1); if(current_bar_time == g_campaign.t3_open_time) return; // Block T4 on same bar as T3 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); bool t4_ok = g_campaign.is_long ? close1 >= g_campaign.t3_entry + atr * InpTranche4ATR : close1 <= g_campaign.t3_entry - atr * InpTranche4ATR; if(t4_ok) { double lots = TrancheLots(g_campaign.full_lots, InpTranche4Pct); if(OpenTranche(3, lots, g_campaign.is_long, g_campaign.pivot_price, "T4")) g_campaign.t4_open = true; } } }
OnInit, OnDeinit, and OnTick
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(!ValidateInputs()) return INIT_FAILED; g_atr_handle = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod); if(g_atr_handle == INVALID_HANDLE) { Print("LivermoreEA: ATR handle creation failed."); return INIT_FAILED; } g_trade.SetExpertMagicNumber(InpMagicNumber); g_trade.SetDeviationInPoints(InpSlippage); g_state = STATE_IDLE; g_last_bar = 0; ResetCampaign(); Print(StringFormat("LivermoreEA v2 initialized | Symbol:%s | TF:%s | Magic:%d", _Symbol, EnumToString(Period()), InpMagicNumber)); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { IndicatorRelease(g_atr_handle); ClearLabels(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0); if(current_bar == g_last_bar) return; g_last_bar = current_bar; double atr_buf[]; ArraySetAsSeries(atr_buf, true); if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1) return; double atr = atr_buf[0]; ProcessStateMachine(atr); } //+------------------------------------------------------------------+
What to Expect
Livermore's method is designed for major trending moves, not frequent signals. Expect between 8 and 20 qualifying campaigns per year on EURUSD D1. Many campaigns will close with only one or two tranches open before the abnormal behavior exit fires—this is correct behavior. Livermore emphasized that small losses on failed probes are far less damaging than large losses from fully committed positions in the wrong direction.
The validation at startup will print "LivermoreEA initialized | HedgingMode:OK," confirming the account check passed. The journal provides a complete audit trail: zone lock; breakout confirmation with volume ratio; each tranche entry, including the stored full_lots value; natural reaction detection with volume contraction confirmation; resumption with swing high break confirmation; and every close with its reason.
If no consolidations are found, increase "InpConsolATR" to 3.0. If breakouts are not firing, reduce "InpVolExpansion" to 1.1 or "InpPivotClearATR" to 0.2.

Fig. 3. Visual demonstration of the system.
Known Limitations
The EA does not recover state after a restart. All campaign data—the state machine position, tranche tickets, full_lots, and the pivot level—lives in memory. If the terminal restarts while trades are open, the EA loses track of them and returns to “STATE IDLE.” For a production-level system an "OnInit()" state recovery function that scans open positions by magic number would address this but is not included in this implementation.
The natural reaction volume check requires the campaign average volume to be above zero before it can fire. On the first bar after T1 opens, this average has only one data point. The volume contraction check becomes meaningful after a few campaign bars have accumulated. The abnormal behavior check has the same warm-up requirement.
The one-day reversal check applies only to the breakout bar. A reversal on the second or third bar after the breakout—where price re-enters the consolidation zone—is caught by the pivot failure check, not the one-day reversal check. These are two different scenarios, and both are handled, but through different mechanisms.
The T4 same-bar block compares the bar time of the last completed bar against "t3_open_time." This correctly prevents T4 from opening on the same bar as T3. However, if T3 opens at the very end of a strong session and T4 conditions are immediately met on the next bar, T4 will open one bar after T3, which is the intended behavior.
The EA is designed for hedging accounts only. This is enforced at startup. On netting accounts, the four-tranche pyramid cannot be tracked by individual ticket—all tranches would merge into a single net position, and the lot management would be incorrect.
Conclusion
Jesse Livermore's method is often summarized as "trade pivotal points on volume." That summary is accurate but incomplete. The pivotal point is only the beginning. What follows—the natural reaction with its volume contraction requirement, the resumption signal, the pyramid addition on confirmation, and the abnormal behavior exit—is a complete sequential system where each step only means something in the context of the steps that preceded it.
Three design decisions in this EA separate it from simpler implementations. The consolidation zone is frozen the moment it is identified—the breakout bar compares against a stable reference it cannot contaminate. The full position size is computed once at Tranche 1 and stored—Tranches 2, 3, and 4 are always fixed percentages of that original size, so the total pyramid risk is controlled as a single figure. The close loop in "CloseAllUnits()" iterates all four slots unconditionally—gaps in the ticket array from skipped tranches never cause positions to be missed.
All code was compiled in MetaTrader 5. Copy "LivermoreEA.mq5" to "MQL5\Experts\" and compile in MetaEditor with no additional dependencies. Recommended for EURUSD and GBPUSD on the daily timeframe. Always test on a demo account before live deployment.
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.
Building Your Personal Expert Advisor (Part 4): Risk Management III—Risk Models and Order Execution
MQL5 Bootstrap (IV): Trailing and Break-even Stop Helpers
Features of Experts Advisors
Implementing a Continuous LLM Adaptation System for Algorithmic Trading
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use