Skip to main content
On this page

MQL5 Coding Best Practices: Build Cleaner, Safer EAs

· 6 min read
Pineify Team
Pine Script and AI trading workflow research team

Reliable MQL5 development separates data acquisition, signal decisions, risk checks and execution state. Formatting helps review, but it cannot compensate for comparing indicator handles instead of values or treating an accepted request as a completed trade. The first task is to make the EA's behavior observable and testable.

MQL5 Coding Best Practices: Write Cleaner, Safer, Profitable Expert Advisors

Understand Events and Reinitialization​

OnInit initializes the program and validates inputs. It can run again after reinitialization, not only on the first chart attachment. OnDeinit releases resources and records why the program stopped. OnTimer handles a configured timer, while OnTick handles NewTick events for the chart symbol.

Events are processed sequentially. If a NewTick event is already queued or executing, another is not added to that EA's queue. Therefore, OnTick is not a guarantee that your handler observes every market tick. Disabling AutoTrading does not stop the EA's calculations; it prevents trading requests. See the official OnTick reference.

A once-per-bar entry policy needs explicit state. Position management may still need more frequent evaluation, so do not put every activity behind the same new-bar gate without considering its purpose.

A Complete Closed-Bar Observation Example​

This EA logs a bullish EMA crossover. It does not place orders. It demonstrates handle creation, sufficient-history checks, copied-value validation and a per-bar observation gate. The final source passed the MQL5 MetaEditor syntax checker; no trading-performance or runtime test is claimed.

#property strict
input int FastPeriod = 10;
input int SlowPeriod = 50;
int fastHandle = INVALID_HANDLE;
int slowHandle = INVALID_HANDLE;
datetime lastProcessedBar = 0;

int OnInit() {
if(FastPeriod < 1 || SlowPeriod <= FastPeriod) {
return INIT_PARAMETERS_INCORRECT;
}
fastHandle = iMA(_Symbol, _Period, FastPeriod, 0, MODE_EMA, PRICE_CLOSE);
slowHandle = iMA(_Symbol, _Period, SlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE) {
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}

void OnDeinit(const int reason) {
if(fastHandle != INVALID_HANDLE) IndicatorRelease(fastHandle);
if(slowHandle != INVALID_HANDLE) IndicatorRelease(slowHandle);
}

void OnTick() {
datetime currentBar = iTime(_Symbol, _Period, 0);
if(currentBar == 0 || currentBar == lastProcessedBar) return;
if(BarsCalculated(fastHandle) < SlowPeriod + 2 ||
BarsCalculated(slowHandle) < SlowPeriod + 2) return;

double fastValues[2], slowValues[2];
if(CopyBuffer(fastHandle, 0, 1, 2, fastValues) != 2 ||
CopyBuffer(slowHandle, 0, 1, 2, slowValues) != 2) return;
// Static arrays: [0] is the older bar 2; [1] is closed bar 1.
for(int i = 0; i < 2; i++) {
if(fastValues[i] == EMPTY_VALUE || slowValues[i] == EMPTY_VALUE ||
!MathIsValidNumber(fastValues[i]) ||
!MathIsValidNumber(slowValues[i])) return;
}
lastProcessedBar = currentBar;
bool crossedUp = fastValues[0] <= slowValues[0] &&
fastValues[1] > slowValues[1];
if(crossedUp) {
Print("Closed-bar bullish EMA crossover observed at ",
TimeToString(currentBar));
}
}

MQL5 iMA returns an indicator handle, not the current moving-average price. The example obtains values through CopyBuffer. In these static arrays, index 0 contains the older of the two copied values: bar 2. Index 1 contains completed bar 1. This ordering follows the CopyBuffer reference.

The gate is updated only after successful reads, allowing another tick to retry unavailable data. On first attachment or restart, the example can inspect the most recent completed pair again. It does not persist a trading decision across restarts; an order-placing EA needs additional state reconciliation and duplicate prevention.

Keep Execution Separate from Signal Detection​

Before submitting a trade, validate the symbol's volume step and limits, tick size, allowed order types, stops/freeze levels, account permissions and supported filling mode. A free-margin balance alone does not validate a specific request.

Use OrderCheck to inspect the proposed request, then examine the submission response and actual account state. A successful check does not guarantee execution. Likewise, OrderSend returning true does not mean that the requested deal has filled.

Handle rejection, partial outcomes and uncertain responses explicitly. Blind retries can create duplicate exposure. Track orders, deals and positions according to the account's netting or hedging mode, and ensure the EA manages only its intended activity.

Use Units and State in Names​

Names such as stopDistancePoints, stopPrice and riskAmount distinguish quantities that must not be mixed. A pip is not universally one point, and rounding to decimal digits alone does not necessarily align a price with the instrument's tick size.

Choose a consistent brace and naming style; camelCase or another readable convention is a project choice, not an MQL5 language requirement. Both MQL4 and MQL5 support object-oriented programming. Split signal, risk and execution responsibilities into focused functions or modules when that makes their contracts easier to inspect.

Test Failure Paths as Well as Signals​

ScenarioExpected evidence
Insufficient history or failed buffer copyNo decision from invalid values; diagnostic or retry policy.
Multiple ticks in one entry barNo unintended repeated entry request.
Terminal restart with an open positionExisting exposure is reconciled before new action.
Broker rejects volume or stopError is recorded; no uncontrolled retry loop.
Timeframe or input changeReinitialization creates the intended handles and state.

Use MetaEditor's debugger for step-by-step inspection and logs for timestamps, inputs, request identifiers and response codes. Avoid flooding the event handler with unnecessary output, and never log credentials.

Begin with a short visual Strategy Tester run, then expand data coverage and cost assumptions. Preserve a later period for evaluation before tuning. Demo observation checks operational behavior, but no fixed two-week or four-week duration proves readiness.

Deployment Is an Operational Process​

A VPS can improve availability, but it does not fix faulty code or guarantee a particular latency. Measure the actual connection and monitor disconnects, repeated rejections and unexpected exposure. Define a restart procedure, a version rollback path and a way to disable new entries while reviewing existing positions.

Pineify can help draft or review MQL5 code and use compiler diagnostics during revision. Review its output against the same data, state and execution contracts. No zero-error generation guarantee or comparison benchmark is asserted here.

Pineify Website

Frequently Asked Questions​

Does OnTick run exactly once for every market tick?
No. NewTick events can be coalesced while one is queued or processing, and events are handled sequentially.
Can I compare two iMA return values as prices?
No. In MQL5 they are handles. Read the required indicator buffers and verify the returned data count.
Does OrderSend returning true prove a fill?
No. Inspect the response codes and reconcile the resulting orders, deals and positions.
Does clean code make an EA profitable?
No. Code quality supports correctness and maintainability. Strategy performance requires a separate, reproducible evaluation.