GT MultiSymbol Spread Analyzer v1.3 free

by GoldTrader in category Other at 12/03/2021
Description

GT MultiSymbol Spread Analyzer allows you to analyze, evaluate, and compare spreads and trading costs of multiple symbols from the historical Tick data of various brokers.


You can set up the following parameters:

  • Watchlist (commission)
    Name of the Watchlist with symbols charged commission fee
  • Commission per mil
    Commission charged by the broker is converted by the analyzer to the equivalent pips.
    Together with the average symbol spread this represents the total cost of trade of the symbol:
    • total cost = average spread (avg) + commission pips (comm)
  • Watchlist (no commission)
    Name of the Watchlist with commission-free symbols
  • Export to File
    You can choose to export the aggregated results of one broker's account to a .csv file (saved on the Desktop). Multiple files generated from different accounts (brokers) can be merged e.g. in MS Excel or Google Sheets for further cross-broker analysis  (see the below example)
  • By Hours
    Data export can be aggregated by hours instead of the standard daily aggregation
  • Normalize Symbol Names
    When selected the analyzer attempts to convert various symbol names (e.g. EURUSD and EUR/USD) used by brokers to a unified format so that data exported from different brokers can be aggregated and analyzed together (e.g. in a pivot table)
    See the NormalizeSymbolName() function for the mapping rules. In case of need you can add your broker's symbol names
     

Make sure to use the Tick data from Server (accurate) for Backtesting (you can ignore the Starting Capital and Commission fields):


Choose the time period over which you want to analyze the spreads (no more than 1 year is suggested if you use the Create File together with By Hours option)

IMPORTANT NOTE: Should you experience errors during the backtest run, ensure the start date is a working day

​The aggregated results are written to the Log:

  • min - minimum spread identified
  • max - maximum spread identified
  • avg - calculated average spread
  • comm - commission fee converted to the equivalent pips
  • total cost - total cost in pips
  • [new in v1.2] normalized - total cost in "normalized pips" where pip size is determined by the analyzer for each symbol. This ensures the normalized pips to have the same size for selected symbol across all tested brokers, thus allowing for an easier total costs comparison
     

Example of a further analysis of the extracted data in MS Excel PivotTable:
(note the differences between the Ø Total Cost vs. the Ø Normalized Cost on indices and crypto caused by different pip sizes among the tested brokers)


Version history:
1.0 - Initial release
1.1 - Zero commission watchlist added
1.2 - Normalized total cost added
1.3 - Fixed usage of normalized symbol names in the backtesting log

Feel free to let me know about any issues or post your suggestions for improvements in the comments!


Did you like my work?

 

Warning! Executing the following cBot may result in loss of funds. Use it at your own risk.
Notification Publishing copyrighted material is strictly prohibited. If you believe there is copyrighted material in this section you may use the Copyright Infringement Notification form to submit a claim.
Formula / Source Code
Language: C#
Trading Platform: cAlgo
//  GT MultiSymbol Spread Analyzer v1.3 by GoldTrader
//  https://ctrader.com/algos/cbots/show/2625

//  Version history:
//  v1.0    - initial release
//  v1.1    - commission-free watchlist added
//  v1.2    - normalized total pips added to the log output and to the exported file
//  v1.3    - fixed usage of normalized symbol names in the backtesting log 

using System;
using cAlgo.API;
using cAlgo.API.Internals;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace cAlgo
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FullAccess)]
    public class _GT_MultiSymbol_Spread_Analyzer : Robot
    {
        [Parameter("Watchlist (commission)", DefaultValue = "SpreadTest")]
        public string parWatchlistName { get; set; }

        [Parameter("Commission per mil", DefaultValue = 25, MinValue = 0)]
        public double parCommission { get; set; }

        [Parameter("Watchlist (no commission)", DefaultValue = "SpreadTestZero")]
        public string parWatchlistNameZero { get; set; }

        [Parameter("Export to File", DefaultValue = false)]
        public bool parCreateFile { get; set; }

        [Parameter("By Hours", DefaultValue = false)]
        public bool parByHours { get; set; }

        [Parameter("Normalize Symbol Names", DefaultValue = false)]
        public bool parNormalize { get; set; }

        private Functions Fn = new Functions();

        private const int hrShift = 0;
        private const string csvSeparator = ",";

        private bool failure = false;
        private string FileName;
        private StringBuilder csv;
        private long csvLines;

        public class HourItem
        {
            public double minSpread;
            public double maxSpread;
            public double sumPrice;
            public double sumSpread;
            public long count;

            public HourItem()
            {
                this.minSpread = double.PositiveInfinity;
                this.maxSpread = double.NegativeInfinity;
                this.sumPrice = 0;
                this.sumSpread = 0;
                this.count = 0;
            }
        }

        public List<Bot> bots = new List<Bot>();
        public class Bot
        {
            public Symbol symbol;
            public double sumSpread, minSpread, maxSpread, avgSpread, comSpread, sumPrice, avgPrice, sumAccCurrRate, normCoef;
            public long count;
            public int precision;
            public HourItem[] hourData;
            public DateTime prevTickDate;
            public bool isFirstTick;
            public bool isZeroCommission;
            public Ticks ticks;

            public Bot(Robot robot, Symbol symbol, bool iszero)
            {
                this.symbol = symbol;
                this.sumSpread = 0;
                this.minSpread = double.PositiveInfinity;
                this.maxSpread = double.NegativeInfinity;
                this.count = 0;
                this.precision = 2;
                this.sumPrice = 0;
                this.sumAccCurrRate = 0;
                this.prevTickDate = DateTime.MinValue;
                this.isFirstTick = true;
                this.isZeroCommission = iszero;
                this.ticks = robot.MarketData.GetTicks(symbol.Name);
                this.normCoef = Math.Pow(10, Math.Round(Math.Log10(symbol.Bid / symbol.PipSize) - 4, 0));
                this.hourData = new HourItem[24];
                for (int h = 0; h < 24; h++)
                    this.hourData[h] = new HourItem();
            }
        }

        protected override void OnStart()
        {
            Symbol[] MySymbols = null, MySymbolsZero = null;
            bool WLfound = true, WLzerofound = true;

            try
            {
                MySymbols = Symbols.GetSymbols(Watchlists.FirstOrDefault(wl => wl.Name == parWatchlistName).SymbolNames.ToArray());
            } catch (Exception)
            {
                WLfound = false;
            }
            try
            {
                MySymbolsZero = Symbols.GetSymbols(Watchlists.FirstOrDefault(wl => wl.Name == parWatchlistNameZero).SymbolNames.ToArray());
            } catch (Exception)
            {
                WLzerofound = false;
            }

            if (!WLfound && !WLzerofound)
            {
                failure = true;
                Print("Watchlist not found");
                Stop();
                return;
            }

            try
            {
                if (WLfound)
                    foreach (Symbol s in MySymbols)
                        bots.Add(new Bot(this, s, false));
                if (WLzerofound)
                    foreach (Symbol s in MySymbolsZero)
                        bots.Add(new Bot(this, s, true));
            } catch (Exception)
            {
                failure = true;
                Print("Failed to get the Tick data");
                Print("Make sure to use the \"Tick data from Server (accurate)\" Backtesting options and a working day for the start date");
//                or delete the \"BacktestingCache\" folder at \"{0}\"", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\" + Account.BrokerName + " cTrader");
                Stop();
                return;
            }

            foreach (Bot bot in bots)
                bot.ticks.Tick += OnBotticksTick;

            // Set up the string writer for CSV:
            if (parCreateFile)
            {
                FileName = Account.BrokerName.Replace(' ', '_') + "_Spread_Analyzer_Output";
                csv = new StringBuilder();
                var headerLine = string.Format("Broker{0}Symbol{0}Date{0}Week day{0}Hour{0}Ticks{0}Min spread{0}Max spread{0}Sum spread{0}Sum cost spread{0}Sum total cost{0}Sum normalized cost", csvSeparator);
                csv.AppendLine(headerLine);
                csvLines = 1;
            }
        }

        void OnBotticksTick(TicksTickEventArgs obj)
        {
            double actSpread;
            foreach (Bot bot in bots)
                if (obj.Ticks.SymbolName == bot.symbol.Name)
                {
                    bot.count++;
                    bot.sumAccCurrRate += bot.symbol.PipValue / bot.symbol.PipSize;
                    actSpread = (obj.Ticks.LastTick.Ask - obj.Ticks.LastTick.Bid);
                    bot.sumSpread += actSpread;
                    bot.sumPrice += (obj.Ticks.LastTick.Ask + obj.Ticks.LastTick.Bid) / 2.0;
                    if (actSpread < bot.minSpread)
                        bot.minSpread = actSpread;
                    if (actSpread > bot.maxSpread)
                        bot.maxSpread = actSpread;

                    if (parCreateFile && obj.Ticks.LastTick.Time.Date > bot.prevTickDate.Date && !bot.isFirstTick)
                        WriteHourData(bot);

                    int hour = (parByHours ? obj.Ticks.LastTick.Time.AddHours(hrShift).Hour : 0);
                    bot.hourData[hour].minSpread = (actSpread / bot.symbol.PipSize < bot.hourData[hour].minSpread ? actSpread / bot.symbol.PipSize : bot.hourData[hour].minSpread);
                    bot.hourData[hour].maxSpread = (actSpread / bot.symbol.PipSize > bot.hourData[hour].maxSpread ? actSpread / bot.symbol.PipSize : bot.hourData[hour].maxSpread);
                    bot.hourData[hour].sumPrice += (bot.symbol.Ask + bot.symbol.Bid) / 2.0;
                    bot.hourData[hour].sumSpread += (actSpread < 0 ? 0 : actSpread / bot.symbol.PipSize);
                    bot.hourData[hour].count++;

                    bot.prevTickDate = obj.Ticks.LastTick.Time.Date;
                    bot.isFirstTick = false;
                }
        }

        private void WriteHourData(Bot b)
        {
            string symbolname = parNormalize ? Fn.NormalizeSymbolName(b.symbol.Name) : b.symbol.Name;
            HourItem[] hdata = b.hourData;


            for (int h = 0; h < (parByHours ? 24 : 1); h++)
            {
                double sumCommSpread = hdata[h].sumPrice * (double)parCommission * (b.isZeroCommission ? 0 : 2.0 / 1000000.0) / b.symbol.PipSize;
                double sumTotalCost = hdata[h].sumSpread + sumCommSpread;
                double sumNormalizedTotal = sumTotalCost / b.normCoef;
                var newLine = string.Format("{1}{0}{2}{0}{3:yyyy/MM/dd}{0}{3:ddd}{0}{4}{0}{5}{0}{6}{0}{7}{0}{8}{0}{9}{0}{10}{0}{11}", csvSeparator, Account.BrokerName, symbolname, b.prevTickDate.Date, h, hdata[h].count, hdata[h].count > 0 ? Math.Round(hdata[h].minSpread, 4).ToString() : "", hdata[h].count > 0 ? Math.Round(hdata[h].maxSpread, 4).ToString() : "", hdata[h].count > 0 ? Math.Round(hdata[h].sumSpread, 4).ToString() : "",
                hdata[h].count > 0 ? Math.Round(sumCommSpread, 4).ToString() : "", hdata[h].count > 0 ? Math.Round(sumTotalCost, 4).ToString() : "", hdata[h].count > 0 ? Math.Round(sumNormalizedTotal, 4).ToString() : "");
                csv.AppendLine(newLine);
                csvLines++;
                hdata[h] = new HourItem();
            }

        }

        protected override void OnStop()
        {
            if (failure)
                return;

            long totalTicks = 0;

            foreach (Bot bot in bots)
            {
                bot.avgPrice = bot.sumPrice / bot.count;
                bot.avgSpread = (double)bot.sumSpread / (double)bot.count;
                bot.comSpread = bot.avgPrice * (double)parCommission * (bot.isZeroCommission ? 0 : 2.0 / 1000000.0);
                totalTicks += bot.count;
                double totalCost = (bot.avgSpread + bot.comSpread) / bot.symbol.PipSize;
                double normCost = totalCost / bot.normCoef;

                Print("{0} (pip size:{6}): (min: {1} - max: {2}) avg: {3} + comm: {4} = total cost: {5} pips | {7} normalized", parNormalize ? Fn.NormalizeSymbolName(bot.symbol.Name) : bot.symbol.Name, Fn.ToRoundedString(bot.minSpread / bot.symbol.PipSize, bot.precision), Fn.ToRoundedString(bot.maxSpread / bot.symbol.PipSize, bot.precision), Fn.ToRoundedString(bot.avgSpread / bot.symbol.PipSize, bot.precision), Fn.ToRoundedString(bot.comSpread / bot.symbol.PipSize, bot.precision), Fn.ToRoundedString(totalCost, bot.precision), bot.symbol.PipSize, Fn.ToRoundedString(normCost, 2));
            }

            if (parCreateFile)
            {
                string fullPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\" + FileName + "_" + DateTime.Now.ToString("yyyyMMddhhmmss") + ".csv";
                File.WriteAllText(fullPath, csv.ToString());
                Print("{0} lines written into {1}", csvLines, fullPath);
            }
        }

        public class Functions
        {
            public string ToRoundedString(double num, int type, bool fix = false)
            {
                if (fix && (Math.Abs(num) < 1.0))
                    return num.ToString("#,##0." + new string('0', type));
                else
                {
                    if (Math.Abs(num) < 1E-05)
                        num = 0;

                    int shift = 0;

                    if ((type != 0) && (num != 0))
                        shift = (int)Math.Floor(Math.Log10(Math.Abs(num))) + ((Math.Abs(num) < 1) ? 1 : 0);

                    if (type - shift > 0)
                        return num.ToString("#,##0." + new string('0', type - shift));
                    else
                        return num.ToString("#,##0");
                }
            }

            public string NormalizeSymbolName(string code)
            {
                if ("|XNG|XNG/USD|NAT.GAS||".Contains("|" + code + "|"))
                    return "XNGUSD";
                else if ("|CL|WTI||".Contains("|" + code + "|"))
                    return "XTIUSD";
                else if ("|BRENT|||".Contains("|" + code + "|"))
                    return "XBRUSD";
                else if ("|GERMANY 30|GER30|DAX|Germany 30 (Mini)||".Contains("|" + code + "|"))
                    return "DE30";
                else if ("|USTECH100|US TECH 100|NSDQ|US Tech 100 (Mini)||".Contains("|" + code + "|"))
                    return "USTEC";
                else if ("|US 30|DOW||".Contains("|" + code + "|"))
                    return "US30";
                else if ("|US 500|SP|US SPX 500 (Mini)||".Contains("|" + code + "|"))
                    return "US500";
                else if ("|UK 100|FTSE||".Contains("|" + code + "|"))
                    return "UK100";
                else if ("|JAPAN 225|NIKKEI||".Contains("|" + code + "|"))
                    return "JP225";
                else if ("|HONG KONG 50||".Contains("|" + code + "|"))
                    return "HK50";
                else if ("|XBN/USD||".Contains("|" + code + "|"))
                    return "BCHUSD";
                else if ((code.Length == 7) && (code.Substring(3, 1) == "/"))
                    return code.Substring(0, 3) + code.Substring(4, 3);
                else
                    return code;
            }
        }
    }
}
Comments

evgrinaus - March 12, 2021 @ 23:59

Nice

evgrinaus - March 13, 2021 @ 01:04

not fining my source file (saved in backtesting)

GoldTrader - March 13, 2021 @ 10:26

Hi evgrinaus - the file is saved on your desktop

0