Problems using indicator in bot

Created at 08 Aug 2018, 07:58
How’s your experience with the cTrader Platform?
Your feedback is crucial to cTrader's development. Please take a few seconds to share your opinion and help us improve your trading experience. Thanks!
bishbashbosh's avatar

bishbashbosh

Joined 08.08.2018

Problems using indicator in bot
08 Aug 2018, 07:58


I have developed an indicator that is working exactly as planned.

I now want to use this to power a bot, but I am having difficulty in using the output from the indicator correctly.

The indicator has two output series (sells omitted for brevity):


        [Output("Buys", PlotType = PlotType.Points, Color = Colors.Green, Thickness = 4)]
        public IndicatorDataSeries BuyDots { get; set; }
        public override void Calculate(int index)
        {
            // logic..

            if (buy)
            {
                BuyDots[index] = DotUpY(low); // place dot below low of bar
            }
        }

Buy and sell dots are placed correctly on the chart.

However, when I tried to use these output series in the bot:

        protected override void OnBar()
        {
            if (_indicator.BuyDots.Last(1) > 0)
                PlaceEntryOrder(TradeType.Buy, MarketSeries.High.Last(1));
        }

...the output is garbage - it seems as if BuyDots and MarketSeries do not refer to the same bar.

So I try a different tack; in the indicator:

        public override void Calculate(int index)
        {
            // logic..

            if (buy)
            {
                BuyDots[index] = DotUpY(low); // place dot below low of bar
                LastBuyTime = MarketSeries.OpenTime.LastValue;
            }
        }

And in the bot:

        protected override void OnBar()
        {
            if (_indicator.LastBuyTime == MarketSeries.OpenTime.Last(1)) // never matches
                PlaceEntryOrder(TradeType.Buy, MarketSeries.High.Last(1));
        }

After stepping through in VS 2017, the reason that it never matches now is that OnBar is called for the entire back-test before Calculate even hits once (so LastBuyTime is always DateTime.MinValue).

Please advise what I should be doing.


@bishbashbosh
Replies

PanagiotisCharalampous
08 Aug 2018, 10:47

Hi bishbashbosh,

Thank you for posting in our forum. Could you please send us the full source code of the cBot and indicator so that we can reproduce and advise accordingly?

Best Regards,

Panagiotis


@PanagiotisCharalampous

bishbashbosh
09 Aug 2018, 08:08

Hi Panagiotis

Here is some code that demonstrates the problem:

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

namespace cAlgo
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class TestIndicator : Indicator
    {
        private CrossoverEvent _lastEvent;
        private ExponentialMovingAverage _longEma;
        private ExponentialMovingAverage _shortEma;

        [Parameter("EMA periods (fast)", DefaultValue = 8)]
        public int FastEmaPeriods { get; set; }

        [Parameter("EMA periods (slow)", DefaultValue = 13)]
        public int SlowEmaPeriods { get; set; }

        [Output("Buys", PlotType = PlotType.Points, Color = Colors.Green, Thickness = 4)]
        public IndicatorDataSeries BuyDots { get; set; }

        public DateTime LastBuyTime { get; set; }

        protected override void Initialize()
        {
            _lastEvent = CrossoverEvent.None;
            _shortEma = Indicators.ExponentialMovingAverage(MarketSeries.Close, FastEmaPeriods);
            _longEma = Indicators.ExponentialMovingAverage(MarketSeries.Close, SlowEmaPeriods);
        }

        public override void Calculate(int index)
        {
            var shortEma = _shortEma.Result[index];
            var longEma = _longEma.Result[index];
            var low = MarketSeries.Low[index];
            var high = MarketSeries.High[index];

            if (shortEma > longEma && _lastEvent != CrossoverEvent.CrossUp)
            {
                _lastEvent = CrossoverEvent.CrossUp;
            }

            if (shortEma < longEma && _lastEvent != CrossoverEvent.CrossDown)
            {
                _lastEvent = CrossoverEvent.CrossDown;
            }

            if (low > shortEma && _lastEvent == CrossoverEvent.CrossUp)
            {
                _lastEvent = CrossoverEvent.Above;
                BuyDots[index] = DotUpY(low);
                LastBuyTime = MarketSeries.OpenTime.LastValue;
            }
        }

        private double DotUpY(double low, double offset = 1)
        {
            return low - offset * Symbol.PipSize;
        }

        private enum CrossoverEvent
        {
            None = 0,
            CrossUp,
            CrossDown,
            Above,
            Below
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo;
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 TestBot : Robot
    {
        private readonly Dictionary<string, CampaignInfo> _trades;
        private int _bar;
        private TestIndicator _indicator;

        public TestBot()
        {
            _trades = new Dictionary<string, CampaignInfo>();
        }

        [Parameter("EMA periods (fast)", DefaultValue = 8)]
        public int FastEmaPeriods { get; set; }

        [Parameter("EMA periods (slow)", DefaultValue = 13)]
        public int SlowEmaPeriods { get; set; }

        [Parameter("Trade risk %", DefaultValue = 1.0)]
        public double TradeRiskPercent { get; set; }

        [Parameter("Entry order high/low offset (pips)", DefaultValue = 1.0)]
        public double EntryOrderPipOffset { get; set; }

        [Parameter("Entry order stop limit range (pips)", DefaultValue = 1.0)]
        public double StopLimitRangePips { get; set; }

        protected override void OnStart()
        {
            _indicator = Indicators.GetIndicator<TestIndicator>(FastEmaPeriods, SlowEmaPeriods);
            Positions.Opened += OnPositionsOpened;
        }

        private void OnPositionsOpened(PositionOpenedEventArgs args)
        {
            var info = _trades[args.Position.Label];
            info.PositionOpenedBar = _bar;
        }

        protected override void OnBar()
        {
            foreach (var position in Positions)
            {
                FixedLifetime(position, _trades[position.Label]);
            }

            if (_indicator.BuyDots.Last(1) > 0 && Positions.Count == 0)
                PlaceEntryOrder(TradeType.Buy, MarketSeries.High.Last(1));

            _bar++;
        }

        protected override void OnStop()
        {
            foreach (var position in Positions)
            {
                ClosePosition(position);
            }
        }

        private void FixedLifetime(Position position, CampaignInfo info, int lifetime = 3)
        {
            if (!(info.PositionOpenedBar - _bar >= lifetime))
                return;
            Print("Closing position {0} ({1}) after {3} bar{4}", position.Id, position.Label, lifetime, lifetime > 1 ? "s" : string.Empty);
            ClosePosition(position);
        }

        private TradeResult PlaceEntryOrder(TradeType side, double targetPrice, int expiryInBars = 1)
        {
            var label = side + "-" + MarketSeries.OpenTime.LastValue.ToString("yyyyMMddhhmm");

            CampaignInfo info;
            if (_trades.TryGetValue(label, out info) && info.TradeResult.IsSuccessful)
                return info.TradeResult;

            var rangePips = (MarketSeries.High.Last(1) - MarketSeries.Low.Last(1)) / Symbol.PipSize;
            var stopLossPips = rangePips + 2 * (StopLimitRangePips + EntryOrderPipOffset);
            var valueAtRisk = Account.Balance * TradeRiskPercent / 100;
            const double scalingFactor = 10000;
            var volume = RoundDown(valueAtRisk * scalingFactor / stopLossPips);
            double? takeProfitPips = null;
            var interval = MarketSeries.OpenTime[1] - MarketSeries.OpenTime[0];
            var expiration = MarketSeries.OpenTime.LastValue.Add(TimeSpan.FromTicks(interval.Ticks * expiryInBars));
            const bool hasTrailingStop = true;
            var entryPrice = targetPrice + EntryOrderPipOffset * Symbol.PipSize;
            var result = PlaceStopLimitOrder(side, Symbol, volume, entryPrice, StopLimitRangePips, label, stopLossPips, takeProfitPips, expiration, "automated order",
            hasTrailingStop);

            _trades[label] = new CampaignInfo 
            {
                IslPips = stopLossPips,
                TradeResult = result
            };

            Print("Placed {0} at bar {1} ({2} {3}, entry={4}, stopLossPips={5}, expiration={6})", label, _bar, volume, Symbol, entryPrice, stopLossPips, expiration);

            return result;
        }

        private static double RoundDown(double n)
        {
            const double unit = 1000.0;
            return Math.Floor(n / unit) * unit;
        }

        internal struct CampaignInfo
        {
            public double IslPips { get; set; }
            public int PositionOpenedBar { get; set; }
            public TradeResult TradeResult { get; set; }
        }
    }
}

Cheers


@bishbashbosh

PanagiotisCharalampous
09 Aug 2018, 09:56

Hi bishbashbosh,

I had a look at the cBot and indicator but I cannot see any issue. Can you please explain to me why you think the output is garbage? What does the cBot do and what would you expect it to do instead?

Best Regards,

Panagiotis


@PanagiotisCharalampous

bishbashbosh
09 Aug 2018, 22:58 ( Updated at: 21 Dec 2023, 09:20 )

Hi Pangiotis

Here's an example of what I am talking about - the bot is trading when/where there is clearly no green dot:

The intention of the code (and please correct me if you do not agree that this is what it is doing) is to place a buy order that is valid for one bar just above the high of an immediately preceding green dot bar, with an initial stop loss just below its low.

Cheers


@bishbashbosh

PanagiotisCharalampous
10 Aug 2018, 09:39

Hi bishbashbosh,

Your cBot places stop limit orders after the green dot. The stop limit orders will be executed when the target price is reached. This might happen some bars after the green dot. If there is a position that you cannot understand why it is there, you can send me backtesting parameters and a trade that you think is wrong and I will explain you why it is placed. 

Best Regards,

Panagiotis


@PanagiotisCharalampous

bishbashbosh
10 Aug 2018, 12:53

RE:

Hi Panagiotis

The backtest above was EURUSD hourly, as I recall. You can see the dates on the chart pic above - it’s the last day or two.

Like I say, the orders should be cancelling if they are not hit in one bar, as is hopefully clear from the code. So there should be no time when a trade enters on a bar that doesn’t follow a green dot. If the problem is in the order code, great - please point out where. But having stepped through it quite a few times trying to figure this out myself, it wasn’t my impression.

Thanks for your assistance.

Panagiotis Charalampous said:

Hi bishbashbosh,

Your cBot places stop limit orders after the green dot. The stop limit orders will be executed when the target price is reached. This might happen some bars after the green dot. If there is a position that you cannot understand why it is there, you can send me backtesting parameters and a trade that you think is wrong and I will explain you why it is placed. 

Best Regards,

 

 


@bishbashbosh

bishbashbosh
20 Aug 2018, 10:10 ( Updated at: 21 Dec 2023, 09:20 )

RE: RE:

Further testing...

I appears that attempting to use BuyDots plain does not work, perhaps because it only intermittently contains a value that is not double.NaN - IndicatorDataSeries.LastValue etc. seem to do an !IsNaN on the series before calculating any look-back. Is this a correct diagnosis?

Moving on, I have developed the code further and am now using a custom method on the indicator to retrieve values from a Dictionary instead.

I notice that Calculate on my indicator is being called from the bot for a given bar/index sometimes correctly at the at the start of the next bar, sometimes at the start of the bar in question (so OHLC are all equal) and sometimes both, so it gets it right in the end.

What is the logic behind this? It works perfectly when I just place the indicator on a chart, but from the bot... not.

        protected override void OnBar()
        {
            AdjustStops();

            // needed or else GetResult returns 0 every time
            var buyLast = _indicator.BuyDots.LastValue;

            var openTimeLast = MarketSeries.OpenTime.Last(1);
            var resultLast = _indicator.GetResult(openTimeLast);

            if (resultLast > 0)
                Print("openTime: {0}; result: {1}", openTimeLast, resultLast);

            if (resultLast.IsBuy())
                PlaceEntryOrder(TradeType.Buy, MarketSeries.High.Last(1));

            if (resultLast.IsSell())
                PlaceEntryOrder(TradeType.Sell, MarketSeries.Low.Last(1));

            _bar++;
        }

Indicator:


        private readonly Dictionary<DateTime, Patterns> _result;

        public Patterns GetResult(DateTime time)
        {
            Patterns value;
            return _result.TryGetValue(time, out value) ? value : Patterns.None;
        }

 

        private bool CalledAtStartOfBar(int index)
        {
            return Math.Abs(MarketSeries.Open[index] - MarketSeries.High[index]) < double.Epsilon &&
                   Math.Abs(MarketSeries.Low[index] - MarketSeries.Close[index]) < double.Epsilon &&
                   Math.Abs(MarketSeries.Open[index] - MarketSeries.Close[index]) < double.Epsilon;
        }

        public override void Calculate(int index)
        {
            var time = MarketSeries.OpenTime[index];

            if (CalledAtStartOfBar(index))
            {
                Print("[{0}] {1}: incomplete bar ({2}, {3}, {4}, {5})", index, time, MarketSeries.Open[index], MarketSeries.High[index], MarketSeries.Low[index], MarketSeries.Close[index]);
                return;
            }

            var matches = Matches(index);
            if (matches > Patterns.None)
            {
                _result[time] = matches;
                Print("[{0}] {1}: {2} ({3}, {4}, {5}, {6})", index, time, matches, MarketSeries.Open[index], MarketSeries.High[index], MarketSeries.Low[index], MarketSeries.Close[index]);

            }
        }


What I see in the log - the log output is always the same, suggesting that it is a logical problem and not e.g. a timing issue:

Hard to diagnose this without understanding cTrader internal logic fully - is my pattern of trying to access the output of the indicator via a method that is not an [Output] property inherently doomed to failure?

I want to be able to dual-use the indicator - it works fine as is placed on a chart, but I don't want to add any more [Output] properties that will mess that up, hence adding a separate method that gets the output in a form more usable by the bot.


@bishbashbosh

bishbashbosh
20 Aug 2018, 13:12

Ok, to answer my own question here - it looks like the above approach works, but only if you have at least one [Output] parameter that has a value set for every period.

Btw, I tested this by creating a modified copy of the indicator with a single [Output] parameter that sets a value for every value of index - and even when I did this, I still needed the Dictionary, as the values I was getting back from it using .Last(1) did not correspond to the correct bar!

:laughing emoji:


@bishbashbosh

bishbashbosh
20 Aug 2018, 14:14 ( Updated at: 21 Dec 2023, 09:20 )

Actually, spoke too soon - still seeing lots of "incomplete bar" in the logs.


@bishbashbosh