Implementing a Fluent Interface Builder Pattern for MQL5 Order Construction
Introduction
Every order submission in MQL5 begins with populating an MqlTradeRequest structure. The structure contains twenty-one fields covering action type, symbol, volume, price, stop levels, expiration, filling mode, and identifiers. In practice, most fields are interdependent. A market order must not include an expiration timestamp. A limit order requires a valid price distinct from the current market. A stop loss above a long entry will be rejected by the broker or, on permissive servers, normalized in a way that may close the position immediately after opening.
The native approach to populating this structure involves direct field assignment in sequence, with no language-level enforcement of logical consistency between fields. The compiler validates types but not semantics. If a developer sets request.sl = entry_price + 500 * _Point for a buy order, the code remains syntactically valid but fails at execution. It returns an error that may be ignored. In the worst case, server-side normalization can result in an unprotected position, depending on broker rules.
Consider what a minimally correct market buy order population looks like in the raw struct pattern:
MqlTradeRequest request = {}; MqlTradeResult result = {}; request.action = TRADE_ACTION_DEAL; request.symbol = _Symbol; request.volume = 0.10; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK); request.sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - 500 * _Point; request.tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + 1000 * _Point; request.deviation = 10; request.magic = 100001; request.comment = "Manual buy"; request.type_filling = ORDER_FILLING_IOC; if(!OrderSend(request, result)) PrintFormat("Error: %d", GetLastError());
This block requires the developer to know, recall, and correctly apply the relationship between action, type, price, sl, and tp without any scaffolding. In a production EA with multiple order types submitted through different code paths, a single copy-paste error in one of these assignments will produce a structurally inconsistent request. The OrderCheck() function can catch some of these errors before server dispatch, but calling it correctly requires additional boilerplate that is often omitted under development pressure.
The fluent builder pattern restructures this process. Instead of assigning struct fields directly, the developer calls self-documenting methods on a builder object in sequence. Each method validates its own input domain before storing the value, and the terminal Send() method performs a final consistency check across all stored fields before constructing the request and dispatching it. The call chain itself documents the order's intent in a form that is readable as prose.

Figure 1: Comparison of coupled manual MqlTradeRequest field assignments (left) versus the step-by-step validated fluent COrderBuilder chain (right).
Builder Architecture and State Machine Model
The COrderBuilder class operates as a lightweight state machine. Its internal state consists of a shadow copy of the eventual MqlTradeRequest fields, augmented by a set of boolean validity flags that track which builder methods have been called and whether each call produced a valid input. The Send() method evaluates these flags collectively before constructing the final struct.
This flag-based state model is preferable to an exception-throwing approach for two reasons specific to MQL5. First, MQL5 does not support C++-style exception propagation; error handling must be done through return values and state inspection. Second, a builder object that accumulates errors silently and reports them at the point of dispatch gives the caller a single, predictable location to check for configuration failures, rather than requiring error handling at every method call site.

Figure 2: State machine diagram of the COrderBuilder, illustrating valid configuration paths, the global INVALID failure state, and the terminal Send() evaluation gate used to efficiently accumulate and handle MQL5 errors.
The builder maintains a separate m_error_message string that accumulates the description of the first validation failure encountered. This string is readable via a GetLastError() accessor method, following the same diagnostic pattern used by MQL5's own trade functions. Callers who need only a success or failure signal check the return value of Send(); callers who need diagnostic output for logging read the error string.
Internal Field Map
The builder's private member layout mirrors the logical groups within MqlTradeRequest rather than its physical field order. This grouping reflects the semantic dependencies that the builder enforces:
| Builder Member | Maps To | Dependency Group | Default Value |
|---|---|---|---|
| m_symbol | request.symbol | Identity | "" (invalid until set) |
| m_volume | request.volume | Identity | 0.0 (invalid until set) |
| m_magic | request.magic | Identity | 0 |
| m_comment | request.comment | Identity | "" |
| m_action | request.action | Direction | TRADE_ACTION_DEAL |
| m_order_type | request.type | Direction | ORDER_TYPE_BUY |
| m_price | request.price | Price Level | 0.0 |
| m_sl | request.sl | Stop Level | 0.0 (optional) |
| m_tp | request.tp | Stop Level | 0.0 (optional) |
| m_deviation | request.deviation | Execution | 10 |
| m_filling | request.type_filling | Execution | ORDER_FILLING_IOC |
| m_expiration | request.expiration | Pending Only | 0 |
| m_stoplimit_price | request.stoplimit | Pending Only | 0.0 |
Validity Flag Set
| Flag | Set By | Guards Against |
|---|---|---|
| m_symbol_valid | Symbol() | Empty string, unrecognized symbols |
| m_volume_valid | Volume() | Zero, negative, below minimum lot |
| m_direction_valid | Buy(), Sell() | Uninitialized direction state |
| m_price_valid | AtMarket(), AtPrice(), AtStop() | Zero price, pending order with no price |
| m_stops_consistent | StopLoss(), TakeProfit() | Inverted SL/TP relative to direction |
Method Design and Validation Logic
Each builder method returns a pointer to the builder instance (COrderBuilder*). This return type is what enables method chaining: the receiver of a method call is the same object, allowing the next method to be appended immediately. In MQL5, this pattern is implemented by returning *this at the end of each chainable method, which dereferences the implicit this pointer to produce a reference to the current instance.
The Pointer Return Mechanism
Each chainable builder method returns COrderBuilder*, a pointer to the current object instance. This return type is what enables method chaining in MQL5. At the end of every chainable method, return this; yields the address of the current instance as a typed pointer, which the compiler resolves correctly as the receiver for the next method call in the chain.
MQL5 does not support reference return types on class methods. A declaration of the form COrderBuilder &Buy() produces a compile-time error in MetaEditor. The pointer return type COrderBuilder* is the correct and only supported substitute for the C++ *this reference pattern in MQL5. In MQL5, method chaining is typically implemented via pointers; calls are written in a chained form depending on the object declaration (instance vs. pointer).
The practical implication for callers is that the builder variable may be declared either as a stack instance or as a heap pointer. For a stack instance, the chain is called on the object directly and the pointer returned by each method is used only as the receiver for the next call, never stored. For a heap-allocated instance, the caller is responsible for deleting the object when finished:
//--- Stack instance: object is destroyed automatically at scope exit COrderBuilder builder; MqlTradeResult result = {}; bool ok = builder->Symbol(_Symbol).Volume(0.10).Buy() .AtMarket().StopLoss(sl).TakeProfit(tp) .Send(result); //--- Heap instance: caller manages lifetime explicitly COrderBuilder *builder = new COrderBuilder(); MqlTradeResult result = {}; bool ok = builder->Symbol(_Symbol).Volume(0.10).Buy() .AtMarket().StopLoss(sl).TakeProfit(tp) .Send(result); delete builder;
The Send() method and Reset() method do not return COrderBuilder*. Send() returns bool as the terminal result of the dispatch gate, and Reset() returns void. These two methods intentionally break the chain because they represent state transitions that should not be silently continued: after Send() the caller must inspect the result, and after Reset() the builder is in a blank state that requires a fresh Symbol() call before any other method is meaningful.
Direction Methods and Action Coupling
The Buy() and Sell() methods set both the order type and the action simultaneously, because in MQL5 the action field and the type field are always paired and their pairing depends on the order category. A market buy is TRADE_ACTION_DEAL plus ORDER_TYPE_BUY. A pending buy limit is TRADE_ACTION_PENDING plus ORDER_TYPE_BUY_LIMIT. The builder exposes separate methods for each combination rather than requiring the caller to set these two fields independently, eliminating the pairing error entirely.
| Builder Method | Sets action | Sets type | Order Category |
|---|---|---|---|
| Buy() | TRADE_ACTION_DEAL | ORDER_TYPE_BUY | Market execution |
| Sell() | TRADE_ACTION_DEAL | ORDER_TYPE_SELL | Market execution |
| BuyLimit(double price) | TRADE_ACTION_PENDING | ORDER_TYPE_BUY_LIMIT | Pending limit |
| SellLimit(double price) | TRADE_ACTION_PENDING | ORDER_TYPE_SELL_LIMIT | Pending limit |
| BuyStop(double price) | TRADE_ACTION_PENDING | ORDER_TYPE_BUY_STOP | Pending stop |
| SellStop(double price) | TRADE_ACTION_PENDING | ORDER_TYPE_SELL_STOP | Pending stop |
| BuyStopLimit(double price, double stoplimit) | TRADE_ACTION_PENDING | ORDER_TYPE_BUY_STOP_LIMIT | Pending stop-limit |
| SellStopLimit(double price, double stoplimit) | TRADE_ACTION_PENDING | ORDER_TYPE_SELL_STOP_LIMIT | Pending stop-limit |
Stop Level Validation
Stop loss and take profit validation is the most structurally significant check the builder performs, because the error here is directional and therefore cannot be caught by range checks alone. A stop loss of 1.0800 is numerically valid in isolation. Whether it is logically valid depends entirely on whether the order is a buy or a sell and what the entry price is.
The StopLoss() method defers its consistency check to the internal ValidateStops() helper, which is also called by TakeProfit() and by Send(). This deferred design is necessary because the developer may call StopLoss() before the price is finalized for a pending order, or before calling Buy() versus Sell(). The builder stores the raw value immediately and re-runs the consistency check each time a related field changes.
bool COrderBuilder::ValidateStops() { //--- Skip validation if direction has not been established if(!m_direction_valid) return(true); double entry = (m_price > 0.0) ? m_price : SymbolInfoDouble(m_symbol, SYMBOL_ASK); if(m_order_type == ORDER_TYPE_SELL || m_order_type == ORDER_TYPE_SELL_LIMIT || m_order_type == ORDER_TYPE_SELL_STOP || m_order_type == ORDER_TYPE_SELL_STOP_LIMIT) entry = (m_price > 0.0) ? m_price : SymbolInfoDouble(m_symbol, SYMBOL_BID); bool is_buy = (m_order_type == ORDER_TYPE_BUY || m_order_type == ORDER_TYPE_BUY_LIMIT || m_order_type == ORDER_TYPE_BUY_STOP || m_order_type == ORDER_TYPE_BUY_STOP_LIMIT); double point = SymbolInfoDouble(m_symbol, SYMBOL_POINT); long stops_level = SymbolInfoInteger(m_symbol, SYMBOL_TRADE_STOPS_LEVEL); double min_distance = stops_level * point; if(m_sl > 0.0) { if(is_buy && m_sl >= entry) { m_error_message = StringFormat( "StopLoss %.5f is at or above buy entry %.5f.", m_sl, entry); m_stops_consistent = false; return(false); } if(!is_buy && m_sl <= entry) { m_error_message = StringFormat( "StopLoss %.5f is at or below sell entry %.5f.", m_sl, entry); m_stops_consistent = false; return(false); } if(MathAbs(entry - m_sl) < min_distance) { m_error_message = StringFormat( "StopLoss distance %.5f is below minimum stops level %ld points.", MathAbs(entry - m_sl) / point, stops_level); m_stops_consistent = false; return(false); } } if(m_tp > 0.0) { if(is_buy && m_tp <= entry) { m_error_message = StringFormat( "TakeProfit %.5f is at or below buy entry %.5f.", m_tp, entry); m_stops_consistent = false; return(false); } if(!is_buy && m_tp >= entry) { m_error_message = StringFormat( "TakeProfit %.5f is at or above sell entry %.5f.", m_tp, entry); m_stops_consistent = false; return(false); } if(MathAbs(entry - m_tp) < min_distance) { m_error_message = StringFormat( "TakeProfit distance %.5f is below minimum stops level %ld points.", MathAbs(entry - m_tp) / point, stops_level); m_stops_consistent = false; return(false); } } m_stops_consistent = true; return(true); }
The broker's SYMBOL_TRADE_STOPS_LEVEL constraint is checked here explicitly. This is a minimum distance in points that the broker enforces between the order price and any stop or take profit level. Submissions that violate this constraint return retcode 10016 (TRADE_RETCODE_INVALID_STOPS) from the server. Catching this in the builder before server dispatch eliminates a round-trip failure and its associated handling cost.

Figure 3: Valid versus invalid SL/TP placements relative to the broker-enforced SYMBOL_TRADE_STOPS_LEVEL exclusion zones for both buy and sell orders.
The Send() Method: Pre-Dispatch Gate
The Send() method is the terminal node of every builder chain. Its responsibilities are ordered deliberately: first it checks that all mandatory fields have been validated, then it runs a final cross-field consistency pass, then it calls OrderCheck() to invoke the broker's pre-flight validation, and only then does it call OrderSend().
This four-stage gate structure means that most configuration errors are caught locally before any network communication occurs. The OrderCheck() call in stage three is particularly important for pending orders, where the relationship between the pending price and current market conditions involves server-side rules that the builder cannot replicate independently without querying live tick data.
- Stage 1: Flag completeness check → All m_*_valid flags true?
- Stage 2: Cross-field consistency → ValidateStops() passes?
- Stage 3: Broker pre-flight → OrderCheck() returns true?
- Stage 4: Server dispatch → OrderSend() succeeds?
If any stage fails, Send() returns false and sets m_error_message. The MqlTradeResult output remains in the state produced by the failing stage. Stages one and two produce no server communication. Stage three populates result.retcode indirectly: OrderCheck() requires a MqlTradeCheckResult parameter rather than MqlTradeResult, so the builder declares a local MqlTradeCheckResult check_result for the call and manually copies check_result.retcode into the caller's result.retcode on failure. Stage four populates the full result structure.

Figure 4: Execution flowchart of the four-stage Send() validation pipeline, distinguishing between zero-latency local checks and server-side network round-trips.
The Reset() Design Choice
After a successful or failed Send(), the builder's state is preserved. It is not automatically reset. This design choice allows the caller to inspect the builder's final state for diagnostic purposes after a failure, and it allows the builder to be reused for a structurally similar order by modifying only the fields that differ. A Reset() method is provided for callers that want to start a fresh configuration.
The alternative design, auto-resetting on every Send() call, would force the caller to re-supply all fields for every order even when submitting a series of structurally identical orders with only the volume varying. With manual reset, there is a small risk of reusing stale state, but it improves efficiency for repetitive submissions. The documentation of this behavior is itself a mitigation: any caller reading the builder's API will encounter the explicit Reset() method, which communicates that the state persists.
Method Reference Table
The complete public interface of COrderBuilder is documented below. The table lists every chainable and terminal method, its effect on the internal state, and the validation it enforces before storing the value.
| Builder Method | MqlTradeRequest Target Fields | Validation Constraint | Fail-Safe Response |
|---|---|---|---|
| Symbol(string) → COrderBuilder* | symbol | Non-empty, passes SymbolSelect() | m_symbol_valid = false, error message set |
| Volume(double) → COrderBuilder* | volume | Within [SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX], normalized to lot step | Out-of-range sets flag false, error message set |
| Magic(ulong) → COrderBuilder* | magic | Unrestricted; stored directly | N/A |
| Comment(string) → COrderBuilder* | comment | Unrestricted; stored directly | N/A |
| Deviation(ulong) → COrderBuilder* | deviation | Unrestricted; stored directly | N/A |
| Filling(ENUM_ORDER_TYPE_FILLING) → COrderBuilder* | type_filling | Unrestricted; stored directly | N/A |
| Buy() → COrderBuilder* | action, type | Requires m_symbol_valid true | Error message set, direction flag false |
| Sell() → COrderBuilder* | action, type | Requires m_symbol_valid true | Error message set, direction flag false |
| BuyLimit(double) → COrderBuilder* | action, type, price | Price below current ask | Error message set, price/direction flags false |
| SellLimit(double) → COrderBuilder* | action, type, price | Price above current bid | Error message set, price/direction flags false |
| BuyStop(double) → COrderBuilder* | action, type, price | Price above current ask | Error message set, price/direction flags false |
| SellStop(double) → COrderBuilder* | action, type, price | Price below current bid | Error message set, price/direction flags false |
| BuyStopLimit(double, double) → COrderBuilder* | action, type, price, stoplimit | Stop above ask; limit below stop | Error message set, flags false |
| SellStopLimit(double, double) → COrderBuilder* | action, type, price, stoplimit | Stop below bid; limit above stop | Error message set, flags false |
| AtMarket() → COrderBuilder* | price (reference), type_filling | Direction must be set; live quote must be available | Symbol must be valid; error if quote unavailable |
| AtPrice(double) → COrderBuilder* | price | Non-zero, positive | Error message set, price flag false |
| StopLoss(double) → COrderBuilder* | sl | Directional validity, stops-level distance | m_stops_consistent = false, error message set |
| TakeProfit(double) → COrderBuilder* | tp | Directional validity, stops-level distance | m_stops_consistent = false, error message set |
| Expiry(datetime) → COrderBuilder* | expiration, type_time | Wraps request.expiration. Future timestamp; sets ORDER_TIME_SPECIFIED | Past timestamps rejected, error message set |
| Reset() → void | All fields | Resets all fields and flags to defaults | N/A |
| Send(MqlTradeResult &) → bool | Constructs full MqlTradeRequest | Four-stage gate: flags, consistency, OrderCheck(), OrderSend() | Returns false at first failing stage |
| IsValid() → bool | None (diagnostic) | Returns true only if all validity flags are true | N/A |
| ErrorMessage() → string | None (diagnostic) | Returns the last accumulated error string | N/A |
Overhead Analysis and Practical Constraints
Validation latency: Every StopLoss() and TakeProfit() call invokes SymbolInfoDouble() and SymbolInfoInteger() to retrieve the current point value and stops level. These are memory reads against the terminal's symbol cache and do not involve network calls; their latency is in the range of tens of nanoseconds on typical hardware. The SymbolSelect() call in Symbol() is the costliest of the per-method validations, as it may trigger a symbol subscription request if the symbol is not already in the Market Watch list. This should be considered when constructing builders for symbols outside the EA's primary instrument in latency-sensitive contexts.
Object reuse vs. fresh instantiation: The builder is a class with no dynamic members beyond its string fields. Declaring it as a stack-local variable and allowing it to be destroyed at scope exit is both safe and leak-free for single-submission use. For EAs that submit orders at high frequency, declaring a single builder at class scope and calling Reset() between submissions avoids repeated construction and destruction of the object's member string fields, which are heap-managed in MQL5. If the builder is heap-allocated via new COrderBuilder() for pointer-chain syntax, the calling code must call delete on the pointer when the builder is no longer needed. Assigning NULL to the pointer after deletion is recommended to prevent accidental double-free in long-running EA sessions.
The method order constraint: Method chaining has a subtlety that does not exist in struct population: the caller must call direction methods before stop level methods, because stop validation is directional. Calling StopLoss(1.0800) before Buy() on a builder with no direction set will skip the directional consistency check and store the value unconditionally, deferring the directional check to the next method that provides direction or to the final Send() call. This is the intended behavior, but developers must understand that the builder's internal ValidateStops() is re-invoked during Send() regardless of when StopLoss() was called, so a late direction assignment will still trigger the correct validation before dispatch.
No multi-threading safety: MQL5 EAs execute on a single thread per instance. The builder is not thread-safe by design. If future MetaTrader 5 runtime versions support concurrent execution contexts within a single EA, the builder's state members would require mutex protection.
OrderCheck() limitations: The broker pre-flight call in stage three of Send() validates the request against current margin and account conditions but does not guarantee acceptance by the server, because market conditions can change in the network round-trip between OrderCheck() and OrderSend(). The builder cannot eliminate this race; it can only reduce the probability of submission failure through pre-dispatch validation.
Usage Patterns
A market buy with stops, using the builder in a local variable:
MqlTradeResult result = {}; COrderBuilder builder; bool ok = builder.Symbol(_Symbol) .Volume(0.10) .Magic(100001) .Comment("Trend entry") .Deviation(10) .Buy() .AtMarket() .StopLoss(SymbolInfoDouble(_Symbol, SYMBOL_ASK) - 500 * _Point) .TakeProfit(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + 1000 * _Point) .Send(result); if(!ok) PrintFormat("[COrderBuilder] Submission failed: %s", builder.ErrorMessage()); else PrintFormat("[COrderBuilder] Order placed. Deal: %lld", result.deal);
A pending buy limit order with expiration:
MqlTradeResult result = {}; COrderBuilder builder; double limit_price = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - 200 * _Point; bool ok = builder.Symbol(_Symbol) .Volume(0.05) .Magic(100002) .BuyLimit(limit_price) .StopLoss(limit_price - 300 * _Point) .TakeProfit(limit_price + 600 * _Point) .Expiry(TimeCurrent() + 3600) .Send(result); if(!ok) PrintFormat("[COrderBuilder] Pending order failed: %s", builder.ErrorMessage());
A builder instance reused across multiple submissions in a class context:
//--- In EA class header: declared as a value member, not a pointer COrderBuilder m_builder; //--- In OnTick() or order dispatch method MqlTradeResult result = {}; m_builder.Reset(); bool ok = m_builder.Symbol(_Symbol) .Volume(inp_lot_size) .Magic(inp_magic_number) .Sell() .AtMarket() .StopLoss(SymbolInfoDouble(_Symbol, SYMBOL_BID) + 400 * _Point) .TakeProfit(SymbolInfoDouble(_Symbol, SYMBOL_BID) - 800 * _Point) .Send(result);

Figure 5: UML sequence diagram illustrating the fluent builder configuration process and the sequential internal and external validation gates triggered during terminal Send() execution.
Conclusion
The COrderBuilder class relocates semantic validation from an implicit, developer-responsibility concern to an explicit, structure-enforced guarantee. By binding directional logic to stop level arithmetic, coupling action and order type into single method calls, and running a multi-stage pre-dispatch gate before any server communication occurs, the builder eliminates the class of errors that raw MqlTradeRequest population leaves open indefinitely.
The trade-off is a modest increase in per-submission code path length. Each method call carries a small validation overhead, and the Send() method's four-stage gate adds execution steps that do not exist in a direct struct-and-send pattern. For the vast majority of EA designs, where order submission occurs on signal events measured in seconds or minutes rather than microseconds, this overhead is architecturally negligible. For systems where tick-level latency is a hard constraint, the validation logic can be pre-run once during signal generation, with Send() called only after all flags are known to be valid, reducing the gate's dispatch-time cost to a single OrderCheck() and OrderSend() sequence.
The pattern also enforces a discipline that scales across development teams. A developer reading a chain of builder calls can determine the complete intent of an order submission without tracing field assignments across multiple functions. This readability benefit compounds over the lifecycle of a complex EA where order logic evolves, is handed between developers, or is audited against regulatory requirements.
Programs used in the article:
| # | Name | Type | Description |
|---|---|---|---|
| 1 | OrderBuilder.mqh | Include File | COrderBuilder class: full method-chaining fluent builder, internal state machine, multi-stage pre-dispatch validation gate, and stop level consistency engine. |
| 2 | OrderBuilderDemo.mq5 | Demo EA | Demonstration EA exercising COrderBuilder across market buy, market sell, pending buy limit, pending sell stop, and pending buy stop-limit order types; includes error-state trapping and diagnostic output. |
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.
MQL5 Wizard Techniques you should know (Part 95): Using Disjoint Set Union and Deep Belief Network in a Custom Signal Class
Engineering a Self-Healing Expert Advisor in MQL5 (Part 2): Restart-Safe Virtual Trade Protection
CSV Data Analysis (Part 3): Engineering a Python Analytics Pipeline for MetaTrader 5 CSV Exports
Building a Type-Safe Event Bus in MQL5: Decoupling EA Components Without Global Variables
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use