MA Grid cBot

This cBot follows a mean reversion strategy, also known as counter trend. It’s best suited for ranging forex markets, therefore it cannot handle long trending moves.

The strategy:

When the current price deviates enough pips from the MA, we enter a high probability reversal zone. We open a trade expecting the price to return to the MA. If the price still goes against us, we keep adding positions each ‘x’ pips (hence the grid) while incrementing the position size. All positions are closed when the price touches the MA.

This strategy was tailored for the current EURUSD market conditions.

The results:

EURUSD backtest of this year.

The Code:

using System;
using System.Net;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
using cAlgo.Indicators;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class MAGrid : Robot
    {
        [Parameter("MA Periods", DefaultValue = 50)]
        public int MAPeriods { get; set; }

        [Parameter("MA Type", DefaultValue = MovingAverageType.Exponential)]
        public MovingAverageType MAType { get; set; }

        [Parameter("Entry Distance", DefaultValue = 50)]
        public int EntryDistancePips { get; set; }

        [Parameter(DefaultValue = 14)]
        public int AtrPeriods { get; set; }

        [Parameter(DefaultValue = 3)]
        public double GridAtrMultiplier { get; set; }

        [Parameter(DefaultValue = 80)]
        public double MaxEquityDrawdownPercent { get; set; }

        [Parameter(DefaultValue = 0.1, MinValue = 0.01)]
        public double LotsPerThousandBalance { get; set; }

        [Parameter(DefaultValue = 0.1)]
        public double LotIncrement { get; set; }

        private double buylevel = double.MinValue, selllevel = double.MaxValue;
        private int nbuys = 0, nsells = 0;
        private double minprofit = double.MaxValue, maxprofit = double.MinValue;

        private MovingAverage ma;
        private AverageTrueRange atr;

        protected override void OnStart()
        {
            ma = Indicators.MovingAverage(Bars.ClosePrices, MAPeriods, MAType);
            atr = Indicators.AverageTrueRange(AtrPeriods, MovingAverageType.Exponential);

            var buys = Positions.FindAll(ToString(), Symbol.Name, TradeType.Buy);
            var sells = Positions.FindAll(ToString(), Symbol.Name, TradeType.Sell);

            nbuys = buys.Length;
            nsells = sells.Length;
            double lots = Math.Max(Math.Floor(100 * Account.Balance / 1000 * (LotsPerThousandBalance + LotIncrement * (nbuys + nsells))) / 100, Symbol.VolumeInUnitsToQuantity(Symbol.VolumeInUnitsMin));

            if (nbuys > 0)
            {
                double min = double.MaxValue;
                foreach (var position in buys)
                    if (position.EntryPrice < min)
                        min = position.EntryPrice;
                buylevel = min - GridAtrMultiplier * atr.Result.LastValue;
            }

            if (nsells > 0)
            {
                double max = double.MinValue;
                foreach (var position in sells)
                    if (position.EntryPrice > max)
                        max = position.EntryPrice;
                selllevel = max + GridAtrMultiplier * atr.Result.LastValue;
            }

            Print("nbuys: {0}, nsells: {1}, buylevel: {2}, selllevel: {3}, lots: {4}", nbuys, nsells, buylevel > double.MinValue ? buylevel.ToString("0.00000") : "n/a", selllevel < double.MaxValue ? selllevel.ToString("0.00000") : "n/a", lots);
        }

        protected override void OnBar()
        {
            // entry logic
            if (nsells + nbuys == 0)
            {
                if (Symbol.Bid > ma.Result.Last(0) + EntryDistancePips * Symbol.PipSize)
                    OpenOrder(TradeType.Sell);

                else if (Symbol.Bid < ma.Result.Last(0) - EntryDistancePips * Symbol.PipSize)
                    OpenOrder(TradeType.Buy);
            }
        }

        protected override void OnTick()
        {
            double profit = getMyUnrealizedNetProfit();
            minprofit = profit < minprofit ? profit : minprofit;
            maxprofit = profit > maxprofit ? profit : maxprofit;

            // exit logic            
            var pos = Positions.FindAll(ToString(), Symbol.Name);
            if (pos.Length == 0 || (profit > 0 && ((nsells > 0 && Symbol.Bid < ma.Result.Last(0)) || (nbuys > 0 && Symbol.Bid > ma.Result.Last(0)))) || -profit > (MaxEquityDrawdownPercent / 100) * Account.Balance)
            {
                if (nbuys + nsells > 0)
                {
                    Print(string.Format("Closing {0} position(s), maxdd: {1}%, profit: {2}, balance: {3}", nbuys + nsells, (minprofit / Account.Balance * 100).ToString("0.0"), profit, Account.Balance));

                    foreach (var p in pos)
                        ClosePositionAsync(p);
                }

                nbuys = nsells = 0;
                maxprofit = buylevel = double.MinValue;
                minprofit = selllevel = double.MaxValue;
            }

            // grid logic
            if (Symbol.Ask <= buylevel)
                OpenOrder(TradeType.Buy);

            if (Symbol.Bid >= selllevel)
                OpenOrder(TradeType.Sell);
        }

        protected override void OnStop()
        {
            if (IsBacktesting)
            {
                var positions = Positions.FindAll(ToString(), Symbol.Name);
                foreach (var p in positions)
                    ClosePositionAsync(p);
            }
        }

        private void OpenOrder(TradeType tt)
        {
            double lots = Math.Max(Math.Floor(100 * Account.Balance / 1000 * (LotsPerThousandBalance + LotIncrement * (nbuys + nsells))) / 100, Symbol.VolumeInUnitsToQuantity(Symbol.VolumeInUnitsMin));
            TradeResult tr = ExecuteMarketOrder(tt, Symbol.Name, Symbol.QuantityToVolumeInUnits(lots), ToString());

            if (tr.Position != null)
            {
                if (tt == TradeType.Sell)
                {
                    nsells++;
                    selllevel = tr.Position.EntryPrice + GridAtrMultiplier * atr.Result.LastValue;
                }
                else
                {
                    nbuys++;
                    buylevel = tr.Position.EntryPrice - GridAtrMultiplier * atr.Result.LastValue;
                }

                Print(tr.Error.HasValue ? string.Format("Order error: {0}", tr.Error) : string.Format("{0} #{1} {2} {3} b: {4}, s: {5}", tr.Position.TradeType, nbuys + nsells, lots.ToString("0.00"), Symbol.Name, buylevel > double.MinValue ? buylevel.ToString("0.00000") : "n/a", selllevel < double.MaxValue ? selllevel.ToString("0.00000") : "n/a"));
            }
        }

        private double getMyUnrealizedNetProfit()
        {
            double netprofit = 0;
            var positions = Positions.FindAll(ToString(), Symbol.Name);
            foreach (var p in positions)
                netprofit += p.NetProfit;

            return netprofit;
        }
    }
}

4 Replies to “MA Grid cBot”

    1. The ATR is used to calculate the price level at which the next position of the grid will be opened. With this approach, the grid size will be bigger when the market is more volatile, reducing the risk.

Leave a Reply

Your email address will not be published. Required fields are marked *