Skip to content

Build Your First Expert Advisor

Create a simple moving average crossover EA from scratch with entry, exit, and position sizing logic.

💡
Before you start

A C++ compiler and a terminal. No MetaTrader, no broker, no account and no money, and nothing below can place a trade. Most Linux machines already have g++; on macOS run xcode-select --install once; on Windows use the Windows Subsystem for Linux or MSYS2.

If you have done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.

The trade calls here are simulated. OrderSend needs a running terminal and a broker connection, so step 5 uses a stand-in that applies the same validation a server applies — which is the part worth practising. When you move this to MetaEditor, test it on a DEMO account first, and leave it there long enough to see it behave over several days.

What We Will Build

We will create a complete Expert Advisor that trades a simple moving average crossover strategy: buy when the fast MA crosses above the slow MA, sell when it crosses below. The EA includes stop-loss, take-profit, and only one position at a time.

Complete EA Source Code

//+------------------------------------------------------------------+
//| MACrossoverEA.mq5                                                 |
//| Simple Moving Average Crossover Expert Advisor                     |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>  // Include the CTrade class

// Input parameters
input int    FastMAPeriod = 10;     // Fast MA Period
input int    SlowMAPeriod = 50;     // Slow MA Period
input double LotSize      = 0.1;   // Lot Size
input int    StopLoss     = 100;   // Stop Loss (points)
input int    TakeProfit   = 200;   // Take Profit (points)
input int    MagicNumber  = 12345; // Magic Number (unique EA ID)

// Global variables
int fastMAHandle, slowMAHandle;
datetime lastBarTime;
CTrade trade;  // Trade execution object

//+------------------------------------------------------------------+
int OnInit()
{
    // Validate inputs
    if(FastMAPeriod >= SlowMAPeriod)
    {
        Print("Error: Fast MA must be smaller than Slow MA");
        return INIT_PARAMETERS_INCORRECT;
    }

    // Create indicator handles
    fastMAHandle = iMA(_Symbol, PERIOD_CURRENT, FastMAPeriod,
                       0, MODE_EMA, PRICE_CLOSE);
    slowMAHandle = iMA(_Symbol, PERIOD_CURRENT, SlowMAPeriod,
                       0, MODE_EMA, PRICE_CLOSE);

    if(fastMAHandle == INVALID_HANDLE || slowMAHandle == INVALID_HANDLE)
    {
        Print("Error creating MA handles");
        return INIT_FAILED;
    }

    // Set magic number for order identification
    trade.SetExpertMagicNumber(MagicNumber);

    lastBarTime = 0;
    Print("MA Crossover EA initialized. Fast: ", FastMAPeriod,
          " Slow: ", SlowMAPeriod);

    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
void OnTick()
{
    // Only trade on new bars (not every tick)
    datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
    if(currentBarTime == lastBarTime) return;
    lastBarTime = currentBarTime;

    // Get MA values for the last 3 completed bars
    double fastMA[], slowMA[];
    ArraySetAsSeries(fastMA, true);
    ArraySetAsSeries(slowMA, true);
    CopyBuffer(fastMAHandle, 0, 1, 3, fastMA);
    CopyBuffer(slowMAHandle, 0, 1, 3, slowMA);

    // Detect crossover on the LAST COMPLETED bar
    bool bullishCross = (fastMA[0] > slowMA[0]) &&
                        (fastMA[1] <= slowMA[1]);
    bool bearishCross = (fastMA[0] < slowMA[0]) &&
                        (fastMA[1] >= slowMA[1]);

    // Check if we have an open position
    bool hasPosition = PositionSelect(_Symbol);

    // BULLISH CROSSOVER — close sell, open buy
    if(bullishCross)
    {
        if(hasPosition && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            trade.PositionClose(_Symbol);

        if(!PositionSelect(_Symbol))  // no position after close
        {
            double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            double sl = ask - StopLoss * _Point;
            double tp = ask + TakeProfit * _Point;
            trade.Buy(LotSize, _Symbol, ask, sl, tp, "MA Cross Buy");
        }
    }

    // BEARISH CROSSOVER — close buy, open sell
    if(bearishCross)
    {
        if(hasPosition && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            trade.PositionClose(_Symbol);

        if(!PositionSelect(_Symbol))
        {
            double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            double sl = bid + StopLoss * _Point;
            double tp = bid - TakeProfit * _Point;
            trade.Sell(LotSize, _Symbol, bid, sl, tp, "MA Cross Sell");
        }
    }
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    IndicatorRelease(fastMAHandle);
    IndicatorRelease(slowMAHandle);
    Print("MA Crossover EA removed. Reason: ", reason);
}
//+------------------------------------------------------------------+

Key Concepts Explained

The CTrade Class

The #include <Trade/Trade.mqh> line imports the standard trade library. The CTrade class provides clean methods for order operations:

trade.Buy(volume, symbol, price, sl, tp, comment);
trade.Sell(volume, symbol, price, sl, tp, comment);
trade.PositionClose(symbol);
trade.PositionModify(ticket, sl, tp);

This is much easier than the raw OrderSend() function.

Magic Number

The Magic Number is a unique identifier for your EA's trades. If you run multiple EAs on the same account, each should have a different Magic Number so they do not interfere with each other's positions.

New Bar Detection

We only check for signals when a new bar opens (not on every tick). This prevents the EA from repeatedly acting on the same signal and ensures we use completed bar data for analysis.

Crossover Detection

A crossover is detected by comparing two consecutive bars: if the fast MA was below the slow MA on bar[1] and is now above on bar[0], that is a bullish crossover. We use bars starting from index 1 (last completed bar) to avoid using the still-forming current bar.

Testing Your EA

1
Open Strategy Tester

In MT5, press Ctrl + R or go to View > Strategy Tester.

2
Configure the test

Select your EA, choose a symbol (e.g., EURUSD), set the timeframe (e.g., H1), and choose a date range with at least 1 year of data.

3
Run and analyze

Click Start. After the backtest, examine the Results tab (trade list), Graph tab (equity curve), and Report tab (statistics).

⚠️
Backtest results are not guarantees

Past performance does not predict future results. Backtests use perfect historical data without real-world issues like slippage, requotes, and variable spreads. Always forward-test on a demo account before going live.

Build a Complete Expert Advisor and Prove Every Guard Earns Its Place, in Five Steps

A first Expert Advisor is usually a signal wrapped in a trade call, and it usually behaves nothing like its author expects — sending dozens of orders for one crossover, stacking positions, or trading on a bar that had not finished. The fix is three guards, and rather than take them on trust you are going to remove each one and count the damage. In the next half hour you will run a complete EA, measure what happens with each guard missing, and see exactly what a trade request contains. Every line of output below came from running these files.

1
Save the header that supplies MetaTrader's built-ins

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as mql5.h. If you already have it from an earlier page, reuse that copy and skip to step 3.

// mql5.h - a small shim that lets a useful SUBSET of MQL5 compile and run
// with an ordinary C++ compiler, so you can practise the language without
// MetaTrader. It is NOT MetaTrader and does not trade anything.
#pragma once
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

typedef std::string string;
typedef long long datetime;
#define input static

template <typename T> int ArraySize(const std::vector<T> &a) { return (int)a.size(); }
template <typename T> void ArrayResize(std::vector<T> &a, int n) { a.resize(n); }

inline void Print() { std::cout << std::endl; }
template <typename T, typename... Rest>
void Print(const T &first, const Rest &...rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) Print(rest...);
    else std::cout << std::endl;
}

inline string DoubleToString(double v, int digits = 8) {
    char buf[64]; snprintf(buf, sizeof buf, "%.*f", digits, v); return string(buf);
}
inline string IntegerToString(long long v) { return std::to_string(v); }
inline double MathAbs(double v)  { return std::fabs(v); }
inline double MathMax(double a, double b) { return a > b ? a : b; }
inline double MathMin(double a, double b) { return a < b ? a : b; }
inline int    MathRound(double v) { return (int)std::lround(v); }

You should see: nothing — this is a header file. Confirm with ls mql5.h (Windows: dir mql5.h).

If not: no compiler? On Ubuntu or Debian sudo apt install g++; on macOS xcode-select --install; on Windows use the Windows Subsystem for Linux or MSYS2.

2
Save the build script

Go: the same folder.

Do: save this as build.sh. It compiles a .mq5 file and executes the result. Make it executable with chmod +x build.sh, or invoke it as sh build.sh.

#!/bin/sh
# build.sh NAME  ->  compiles NAME.mq5 and runs it.
# MQL5 is close enough to C++ that a C++ compiler can run the subset used here,
# with mql5.h supplying the handful of built-ins MetaTrader would provide.
# Uses whichever compiler you have.
set -e
NAME="$1"
if command -v g++ >/dev/null 2>&1 && g++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5" 2>/dev/null; then
    :
elif command -v clang++ >/dev/null 2>&1; then
    clang++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5"
else
    echo "No C++ compiler found. Install g++ (Linux: apt install g++, macOS: xcode-select --install)." >&2
    exit 1
fi
"./$NAME"

You should see: nothing — this is the toolchain. ls should show both mql5.h and build.sh.

If not: Permission denied means the chmod was skipped; use sh build.sh NAME, which needs no execute bit.

3
Run a complete EA end to end

Go: the same folder.

Do: save this as minimal_ea.mq5 and run sh build.sh minimal_ea.

#include "mql5.h"

//+------------------------------------------------------------------+
//| A complete EA, with every guard the previous pages argued for.    |
//+------------------------------------------------------------------+

input int    FastPeriod = 5;
input int    SlowPeriod = 20;
input double RiskPercent = 1.0;

datetime g_last_bar   = 0;      // outside OnTick: must survive between ticks
int      g_positions  = 0;      // stands in for PositionsTotal()
int      g_orders_sent = 0;
int      g_closed = 0;

double Sma(const std::vector<double> &c, int i, int period)
{
    if(i < period - 1) return 0.0;
    double s = 0.0;
    for(int k = 0; k < period; k++) s += c[i - k];
    return s / period;
}

bool IsNewBar(datetime bar_time)
{
    if(bar_time == g_last_bar) return false;
    g_last_bar = bar_time;
    return true;
}

// Signals are computed on CLOSED bars only: index i is the last closed bar.
bool CrossedUp(const std::vector<double> &c, int i)
{
    if(i < SlowPeriod) return false;
    double f_now = Sma(c, i, FastPeriod),  f_prev = Sma(c, i - 1, FastPeriod);
    double s_now = Sma(c, i, SlowPeriod),  s_prev = Sma(c, i - 1, SlowPeriod);
    return (f_prev <= s_prev && f_now > s_now);
}

bool CrossedDown(const std::vector<double> &c, int i)
{
    if(i < SlowPeriod) return false;
    double f_now = Sma(c, i, FastPeriod),  f_prev = Sma(c, i - 1, FastPeriod);
    double s_now = Sma(c, i, SlowPeriod),  s_prev = Sma(c, i - 1, SlowPeriod);
    return (f_prev >= s_prev && f_now < s_now);
}

void OnTick(const std::vector<double> &closes, int last_closed, datetime bar_time)
{
    if(!IsNewBar(bar_time)) return;              // once per bar

    if(g_positions > 0)                          // ask the terminal, not memory
    {
        if(CrossedDown(closes, last_closed))
        {
            g_positions--;
            g_closed++;
            Print("   bar ", bar_time, "  CLOSE (position #", g_closed, ")");
        }
        return;                                  // never stack entries
    }

    if(!CrossedUp(closes, last_closed)) return;  // closed-bar signal

    g_orders_sent++;
    g_positions++;
    Print("   bar ", bar_time, "  BUY  sent (order #", g_orders_sent, ")");
}

void OnStart()
{
    std::vector<double> closes;
    for(int i = 0; i < 120; i++)
        closes.push_back(100.0 + 8.0 * (((i / 25) % 2 == 0) ? (i % 25) : (25 - i % 25)) / 25.0);

    int ticks = 0;
    for(int bar = SlowPeriod + 1; bar < 119; bar++)
        for(int t = 0; t < 6; t++)               // six ticks inside each bar
        {
            ticks++;
            OnTick(closes, bar, 1700000000 + bar * 3600);
        }

    Print("");
    Print("ticks processed : ", ticks);
    Print("orders sent     : ", g_orders_sent);
    Print("positions closed: ", g_closed);
    Print("still open      : ", g_positions);
    Print("");
    Print("Every guard is doing visible work. Remove IsNewBar and the same");
    Print("signal fires six times. Remove the position check and it fires on");
    Print("every later signal while one is already open.");
}

int main() { OnStart(); return 0; }

You should see: 588 ticks producing two entries and one exit:

   bar 1700208800  BUY  sent (order #1)
   bar 1700298800  CLOSE (position #1)
   bar 1700388800  BUY  sent (order #2)

ticks processed : 588
orders sent     : 2
positions closed: 1
still open      : 1

Every guard is doing visible work. Remove IsNewBar and the same
signal fires six times. Remove the position check and it fires on
every later signal while one is already open.

This is a whole EA in about fifty lines, and every part of it comes from an earlier page. IsNewBar makes “once per bar” true. The signal reads only closed bars. The position check asks the state rather than remembering it. And the state that must survive between ticks lives outside OnTick.

In MetaEditor, three things change. OnTick() takes no arguments and fetches its own data with CopyClose or iClose. The position count comes from PositionsTotal() rather than a variable. And the entry line becomes an OrderSend call with the request structure from step 5.

If not: if no orders are sent, the generated price series is not producing a crossover — the zig-zag shape depends on the (i / 25) % 2 arithmetic, so keep it as written. If hundreds are sent, g_last_bar was declared inside a function.

4
Remove each guard and count the damage

Go: the same folder.

Do: save this as guards.mq5 and run sh build.sh guards.

#include "mql5.h"

//+------------------------------------------------------------------+
//| Remove each guard in turn and count the orders.                   |
//+------------------------------------------------------------------+

int  Run(bool use_newbar, bool use_position_check)
{
    // 20 bars, 6 ticks each. The signal is true on bars 5, 6, 7 and 14.
    bool signal[20] = {0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0};

    datetime last_bar = 0;
    int positions = 0, sent = 0;

    for(int bar = 0; bar < 20; bar++)
        for(int tick = 0; tick < 6; tick++)
        {
            datetime bar_time = 1700000000 + bar * 3600;
            if(use_newbar)
            {
                if(bar_time == last_bar) continue;
                last_bar = bar_time;
            }
            if(use_position_check && positions > 0) continue;
            if(!signal[bar]) continue;
            sent++;
            positions++;
        }
    return sent;
}

void OnStart()
{
    Print("120 ticks across 20 bars. The signal is genuinely true on 4 bars.");
    Print("");
    Print("GUARDS IN PLACE                              ORDERS SENT");
    Print("------------------------------------------------------");
    Print("both                                              ", Run(true,  true));
    Print("new-bar only (no position check)                  ", Run(true,  false));
    Print("position check only (no new-bar guard)            ", Run(false, true));
    Print("neither                                           ", Run(false, false));
    Print("");
    Print("With neither guard, four signals become ", Run(false, false), " orders.");
    Print("");
    Print("Note the third row. The position check alone still sends only one,");
    Print("but only because nothing ever closes in this model -- in a real EA");
    Print("with exits, it lets the same signal re-enter as soon as a position");
    Print("closes mid-bar. The two guards answer different questions and you");
    Print("need both.");
}

int main() { OnStart(); return 0; }

You should see: four genuine signals becoming twenty-four orders:

120 ticks across 20 bars. The signal is genuinely true on 4 bars.

GUARDS IN PLACE                              ORDERS SENT
------------------------------------------------------
both                                              1
new-bar only (no position check)                  4
position check only (no new-bar guard)            1
neither                                           24

With neither guard, four signals become 24 orders.

Note the third row. The position check alone still sends only one,
but only because nothing ever closes in this model -- in a real EA
with exits, it lets the same signal re-enter as soon as a position
closes mid-bar. The two guards answer different questions and you
need both.

With no guards, four signals produce twenty-four orders — six per bar, one per tick — and on a live account that is twenty-four positions, twenty-four spreads, and a margin call rather than a strategy.

The third row is the one worth thinking about. The position check alone appears sufficient here, and it is not: this model never closes anything, so the check is doing all the work by accident. In a real EA with exits, a position that closes mid-bar leaves the check satisfied again while the same signal is still true, and the EA re-enters immediately.

Two guards, two different questions. “Is this a new bar?” and “do I already have a position?” are not substitutes for one another, and an EA needs both.

If not: if all four rows show the same number, the boolean arguments are not reaching the guards — each one is tested with an if(use_...) so that removing it is a parameter rather than an edit.

5
Look at what a trade request actually contains

Go: the same folder.

Do: save this as request.mq5 and run sh build.sh request.

#include "mql5.h"

//+------------------------------------------------------------------+
//| What OrderSend actually receives, and what it gives back.         |
//+------------------------------------------------------------------+

struct MqlTradeRequest
{
    string action;      // TRADE_ACTION_DEAL for a market order
    string symbol;
    double volume;
    string type;        // ORDER_TYPE_BUY / ORDER_TYPE_SELL
    double price;
    double sl, tp;
    int    deviation;   // slippage tolerance, in points
    long   magic;       // YOUR id, so you can tell your trades from others
    string comment;
};

struct MqlTradeResult { int retcode; long order; double price; string comment; };

// Stands in for OrderSend: the first three checks are ones a real server
// applies. The magic-number check is NOT -- a real server accepts magic 0
// happily. It is here because it is a check YOU should apply to yourself,
// and the distinction matters (see the note at the end).
bool OrderSend_stub(const MqlTradeRequest &r, MqlTradeResult &res)
{
    if(r.volume <= 0.0)          { res.retcode = 10014; res.comment = "invalid volume";  return false; }
    if(r.price  <= 0.0)          { res.retcode = 10015; res.comment = "invalid price";   return false; }
    if(r.sl > 0 && r.type == "ORDER_TYPE_BUY" && r.sl >= r.price)
                                 { res.retcode = 10016; res.comment = "invalid stops";   return false; }
    if(r.magic == 0)             { res.retcode = -1;    res.comment = "OUR OWN check: no magic set"; return false; }
    res.retcode = 10009; res.order = 5551234; res.price = r.price; res.comment = "done";
    return true;
}

void Try(string label, MqlTradeRequest r)
{
    MqlTradeResult res;
    bool ok = OrderSend_stub(r, res);
    Print("  ", label);
    Print("     returned ", ok ? "true " : "false", "  retcode ", res.retcode,
          "  (", res.comment, ")");
}

void OnStart()
{
    MqlTradeRequest good;
    good.action="TRADE_ACTION_DEAL"; good.symbol="EURUSD"; good.volume=0.10;
    good.type="ORDER_TYPE_BUY"; good.price=1.10250; good.sl=1.10050; good.tp=1.10650;
    good.deviation=10; good.magic=20260822; good.comment="first EA";

    Try("a complete, valid request", good);

    MqlTradeRequest zero = good; zero.volume = 0.0;
    Try("volume left at 0 (integer division!)", zero);

    MqlTradeRequest badsl = good; badsl.sl = 1.10450;
    Try("buy with the stop ABOVE the entry", badsl);

    MqlTradeRequest nomagic = good; nomagic.magic = 0;
    Try("no magic number", nomagic);

    Print("");
    Print("retcode 10009 is TRADE_RETCODE_DONE. Anything else is a refusal,");
    Print("and the RETURN VALUE alone does not tell you which -- always read");
    Print("res.retcode, and log it, because 'the EA did not trade' with no");
    Print("recorded retcode is undiagnosable a week later.");
    Print("");
    Print("The last case is different from the other three. A real server");
    Print("ACCEPTS magic 0 without complaint -- retcode -1 above is this");
    Print("program's own invention, not something MetaTrader returns.");
    Print("");
    Print("It is here because the magic number is how your EA recognises its");
    Print("OWN positions on an account that may also hold manual trades and");
    Print("other EAs. Nothing enforces it, and 'close my position' without it");
    Print("can close somebody else's. Some rules are the server's; some are");
    Print("yours, and only you will ever check them.");
}

int main() { OnStart(); return 0; }

You should see: one accepted request and three refusals:

  a complete, valid request
     returned true   retcode 10009  (done)
  volume left at 0 (integer division!)
     returned false  retcode 10014  (invalid volume)
  buy with the stop ABOVE the entry
     returned false  retcode 10016  (invalid stops)
  no magic number
     returned false  retcode -1  (OUR OWN check: no magic set)

retcode 10009 is TRADE_RETCODE_DONE. Anything else is a refusal,
and the RETURN VALUE alone does not tell you which -- always read
res.retcode, and log it, because 'the EA did not trade' with no
recorded retcode is undiagnosable a week later.

The last case is different from the other three. A real server
ACCEPTS magic 0 without complaint -- retcode -1 above is this
program's own invention, not something MetaTrader returns.

It is here because the magic number is how your EA recognises its
OWN positions on an account that may also hold manual trades and
other EAs. Nothing enforces it, and 'close my position' without it
can close somebody else's. Some rules are the server's; some are
yours, and only you will ever check them.

Reading res.retcode is not optional. The return value tells you the request was processed; the retcode tells you what happened. 10009 is TRADE_RETCODE_DONE; everything else is a refusal with a reason, and an EA that logs “did not trade” without the retcode has thrown away the only evidence.

The second case connects to the syntax page: a volume of zero is what integer division produces, and this is where it finally surfaces — as 10014 invalid volume, several layers away from the arithmetic that caused it.

The fourth case is deliberately different, and the program says so. A real server accepts a magic number of zero without complaint; that check is one you impose on yourself. It matters because the magic number is how an EA recognises its own positions on an account that may also hold manual trades and another EA's — and without it, “close my position” can close somebody else's. Some rules are the server's and some are yours, and telling them apart is part of writing trading code.

One more field to notice: deviation is how much slippage you will accept, in points. Set it to zero and requests are rejected whenever the price moves between your decision and the server's receipt, which on a fast market is most of the time.

If not: if every case is accepted, the guard conditions in the stub were removed. The retcode -1 on the last case is this program's own invention and is not a MetaTrader code — that is stated in the output on purpose.

🎉
Check yourself before moving on

Without scrolling up: an EA is meant to hold at most one position. Overnight it opened eleven, all within four minutes, all on the same signal. It does have a check that returns early when a position is open. What are the two most likely explanations, and how would you separate them? Answer: the first is that the position check consults the EA's own counter rather than the terminal, and the counter was reset — a setting change, a recompile or a terminal restart runs OnDeinit and then OnInit, and every global variable goes back to zero while the positions themselves carry on existing. The second is that there is no new-bar guard, so the signal was evaluated on every tick; step 4 showed that turning four signals into twenty-four orders, and if the position check is asynchronous — the order is sent, and the position does not appear in PositionsTotal() until the server confirms — several ticks can pass the check before the first position registers, which produces exactly a burst within a few minutes. To separate them, look at the log: if the burst follows a restart or a parameter change, it is the first. If it happens on a fresh signal with no restart nearby, it is the second, and the fix is a new-bar guard plus tracking the in-flight order rather than only the confirmed position.

Now do it without the page: add a stop-loss and take-profit to minimal_ea.mq5 — not as numbers, but computed from the entry price and a distance in points — and then add the position-size function from the functions page so the volume is derived from risk rather than fixed. That turns the EA from a demonstration into something with the right shape. Then, before it ever sees a live account, run it in the strategy tester and check one thing: does the trade count change substantially between “every tick” and “open prices only” modelling? If it does, one of your guards is not working.

Improvements for Production

This EA is educational. A production EA would also need:

  • Dynamic lot sizing based on account balance and risk percentage
  • Trailing stop-loss to lock in profits
  • Trading session filters (avoid low-liquidity hours)
  • News event filters
  • Error handling and retry logic for failed orders
  • Magic number filtering to only manage its own positions
  • Logging for diagnostics and auditing