예제 #1
0
        public BaseExchange(ExchangeOptions options)
        {
            _exchange = options.Exchange;

            switch (_exchange)
            {
            case Exchange.Binance:
                _api = new ExchangeSharp.ExchangeBinanceAPI();
                break;

            case Exchange.Bitfinex:
                _api = new ExchangeSharp.ExchangeBitfinexAPI();
                break;

            case Exchange.Bittrex:
                _api = new ExchangeSharp.ExchangeBittrexAPI();
                break;

            case Exchange.Poloniex:
                _api = new ExchangeSharp.ExchangePoloniexAPI();
                break;
            }

            _api.LoadAPIKeysUnsecure(options.ApiKey, options.ApiSecret, options.PassPhrase);
        }
예제 #2
0
        public static void RunPerformTests(Dictionary <string, string> dict)
        {
            IExchangeAPI[] apis = ExchangeAPI.GetExchangeAPIDictionary().Values.ToArray();
            foreach (IExchangeAPI api in apis)
            {
                // test all public API for each exchange
                try
                {
                    string symbol = GetSymbol(api);

                    IReadOnlyCollection <string> symbols = api.GetSymbols();
                    Assert(symbols != null && symbols.Count != 0 && symbols.Contains(symbol, StringComparer.OrdinalIgnoreCase));

                    ExchangeTrade[] trades = api.GetHistoricalTrades(symbol).ToArray();
                    Assert(trades.Length != 0 && trades[0].Price > 0m && trades[0].Amount > 0m);

                    var book = api.GetOrderBook(symbol);
                    Assert(book.Asks.Count != 0 && book.Bids.Count != 0 && book.Asks[0].Amount > 0m &&
                           book.Asks[0].Price > 0m && book.Bids[0].Amount > 0m && book.Bids[0].Price > 0m);

                    trades = api.GetRecentTrades(symbol).ToArray();
                    Assert(trades.Length != 0 && trades[0].Price > 0m && trades[0].Amount > 0m);

                    var ticker = api.GetTicker(symbol);
                    Assert(ticker != null && ticker.Ask > 0m && ticker.Bid > 0m && ticker.Last > 0m &&
                           ticker.Volume != null && ticker.Volume.PriceAmount > 0m && ticker.Volume.QuantityAmount > 0m);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Request failed, api: {0}, error: {1}", api, ex.Message);
                }
            }
        }
예제 #3
0
 /// <summary>
 /// Static constructor
 /// </summary>
 static ExchangeAPI()
 {
     foreach (Type type in typeof(ExchangeAPI).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ExchangeAPI))))
     {
         ExchangeAPI api = Activator.CreateInstance(type) as ExchangeAPI;
         apis[api.Name] = api;
     }
 }
예제 #4
0
        /// <summary>
        /// Begins logging exchanges - writes errors to console. You should block the app using Console.ReadLine.
        /// </summary>
        /// <param name="path">Path to write files to</param>
        /// <param name="intervalSeconds">Interval in seconds in between each log calls for each exchange</param>
        /// <param name="compress">Whether to compress the log files</param>
        /// <param name="exchangeNamesAndSymbols">Exchange names and symbols to log</param>
        /// <returns">Call this when the process is about to exit, like a WM_CLOSE message on Windows.</returns>
        public static async Task <Action> LogExchangesAsync(string path, float intervalSeconds, bool compress, params string[] exchangeNamesAndSymbols)
        {
            bool   terminating = false;
            Action terminator  = null;

            path = (string.IsNullOrWhiteSpace(path) ? "./" : path);
            Dictionary <ExchangeLogger, int> errors  = new Dictionary <ExchangeLogger, int>();
            List <ExchangeLogger>            loggers = new List <ExchangeLogger>();

            for (int i = 0; i < exchangeNamesAndSymbols.Length;)
            {
                loggers.Add(new ExchangeLogger(await ExchangeAPI.GetExchangeAPIAsync(exchangeNamesAndSymbols[i++]), exchangeNamesAndSymbols[i++], intervalSeconds, path, compress));
            }
            ;
            foreach (ExchangeLogger logger in loggers)
            {
                logger.Start();
                logger.Error += (log, ex) =>
                {
                    int errorCount;
                    lock (errors)
                    {
                        if (!errors.TryGetValue(log, out errorCount))
                        {
                            errorCount = 0;
                        }
                        errors[log] = ++errorCount;
                    }
                    Logger.Info("Errors for {0}: {1}", log.API.Name, errorCount);
                };
            }
            terminator = () =>
            {
                if (!terminating)
                {
                    terminating = true;
                    foreach (ExchangeLogger logger in loggers.ToArray())
                    {
                        logger.Stop();
                        logger.Dispose();
                    }
                    loggers.Clear();
                }
            };

            // make sure to close properly
            Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e)
            {
                terminator();
            };
            AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) =>
            {
                terminator();
            };
            Logger.Info("Loggers \"{0}\" started, press ENTER or CTRL-C to terminate.", string.Join(", ", loggers.Select(l => l.API.Name)));

            return(terminator);
        }
예제 #5
0
 /// <summary>
 /// Static constructor
 /// </summary>
 static ExchangeAPI()
 {
     foreach (Type type in typeof(ExchangeAPI).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ExchangeAPI))))
     {
         ExchangeAPI api = Activator.CreateInstance(type) as ExchangeAPI;
         apis[api.Name] = api;
         ExchangeGlobalCurrencyReplacements[type] = new KeyValuePair <string, string> [0];
     }
 }
예제 #6
0
        /// <summary>
        /// Get a dictionary of exchange APIs for all exchanges
        /// </summary>
        /// <returns>Dictionary of string exchange name and value exchange api</returns>
        public static Dictionary <string, IExchangeAPI> GetExchangeAPIDictionary()
        {
            Dictionary <string, IExchangeAPI> apis = new Dictionary <string, IExchangeAPI>(StringComparer.OrdinalIgnoreCase);

            foreach (Type type in typeof(ExchangeAPI).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ExchangeAPI))))
            {
                ExchangeAPI api = Activator.CreateInstance(type) as ExchangeAPI;
                apis[api.Name] = api;
            }
            return(apis);
        }
        public static void RunExportData(Dictionary <string, string> dict)
        {
            RequireArgs(dict, "exchange", "symbol", "path", "sinceDateTime");
            string exchange = dict["exchange"];
            long   total    = 0;

            TraderExchangeExport.ExportExchangeTrades(ExchangeAPI.GetExchangeAPI(exchange), dict["symbol"], dict["path"], DateTime.Parse(dict["sinceDateTime"]), (long count) =>
            {
                total = count;
                Console.Write("Exporting {0}: {1}     \r", exchange, total);
            });
            Console.WriteLine("{0}Finished Exporting {1}: {2}     \r", Environment.NewLine, exchange, total);
        }
        public static void RunPerformTests(Dictionary <string, string> dict)
        {
            TestEncryption();

            IExchangeAPI[] apis = ExchangeAPI.GetExchangeAPIDictionary().Values.ToArray();
            foreach (IExchangeAPI api in apis)
            {
                // test all public API for each exchange
                try
                {
                    string symbol = GetSymbol(api);

                    IReadOnlyCollection <string> symbols = api.GetSymbols().ToArray();
                    Assert(symbols != null && symbols.Count != 0 && symbols.Contains(symbol, StringComparer.OrdinalIgnoreCase));

                    ExchangeTrade[] trades = api.GetHistoricalTrades(symbol).ToArray();
                    Assert(trades.Length != 0 && trades[0].Price > 0m && trades[0].Amount > 0m);

                    var book = api.GetOrderBook(symbol);
                    Assert(book.Asks.Count != 0 && book.Bids.Count != 0 && book.Asks[0].Amount > 0m &&
                           book.Asks[0].Price > 0m && book.Bids[0].Amount > 0m && book.Bids[0].Price > 0m);

                    trades = api.GetRecentTrades(symbol).ToArray();
                    Assert(trades.Length != 0 && trades[0].Price > 0m && trades[0].Amount > 0m);

                    var ticker = api.GetTicker(symbol);
                    Assert(ticker != null && ticker.Ask > 0m && ticker.Bid > 0m && ticker.Last > 0m &&
                           ticker.Volume != null && ticker.Volume.PriceAmount > 0m && ticker.Volume.QuantityAmount > 0m);

                    try
                    {
                        var candles = api.GetCandles(symbol, 86400, DateTime.UtcNow.Subtract(TimeSpan.FromDays(7.0)), null).ToArray();
                        Assert(candles.Length != 0 && candles[0].ClosePrice > 0m && candles[0].HighPrice > 0m && candles[0].LowPrice > 0m && candles[0].OpenPrice > 0m &&
                               candles[0].HighPrice >= candles[0].LowPrice && candles[0].HighPrice >= candles[0].ClosePrice && candles[0].HighPrice >= candles[0].OpenPrice &&
                               !string.IsNullOrWhiteSpace(candles[0].Name) && candles[0].ExchangeName == api.Name && candles[0].PeriodSeconds == 86400 && candles[0].VolumePrice > 0.0 &&
                               candles[0].VolumeQuantity > 0.0 && candles[0].WeightedAverage >= 0m);
                    }
                    catch (NotSupportedException)
                    {
                    }
                    catch (NotImplementedException)
                    {
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Request failed, api: {0}, error: {1}", api, ex.Message);
                }
            }
        }
예제 #9
0
 /// <summary>
 /// Static constructor
 /// </summary>
 static ExchangeAPI()
 {
     foreach (Type type in typeof(ExchangeAPI).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ExchangeAPI)) && !type.IsAbstract))
     {
         // lazy create, we just create an instance to get the name, nothing more
         // we don't want to pro-actively create all of these becanse an API
         // may be running a timer or other house-keeping which we don't want
         // the overhead of if a user is only using one or a handful of the apis
         using (ExchangeAPI api = Activator.CreateInstance(type) as ExchangeAPI)
         {
             apis[api.Name] = null;
         }
         ExchangeGlobalCurrencyReplacements[type] = new KeyValuePair <string, string> [0];
     }
 }
        /// <summary>
        /// Parse a JToken into a ticker
        /// </summary>
        /// <param name="api">ExchangeAPI</param>
        /// <param name="token">Token</param>
        /// <param name="marketSymbol">Symbol</param>
        /// <param name="askKey">Ask key</param>
        /// <param name="bidKey">Bid key</param>
        /// <param name="lastKey">Last key</param>
        /// <param name="baseVolumeKey">Base currency volume key</param>
        /// <param name="quoteVolumeKey">Quote currency volume key</param>
        /// <param name="timestampKey">Timestamp key</param>
        /// <param name="timestampType">Timestamp type</param>
        /// <param name="baseCurrencyKey">Base currency key</param>
        /// <param name="quoteCurrencyKey">Quote currency key</param>
        /// <param name="idKey">Id key</param>
        /// <returns>ExchangeTicker</returns>
        internal static ExchangeTicker ParseTicker(this ExchangeAPI api, JToken token, string marketSymbol,
                                                   object askKey, object bidKey, object lastKey, object baseVolumeKey,
                                                   object quoteVolumeKey  = null, object timestampKey     = null, TimestampType timestampType = TimestampType.None,
                                                   object baseCurrencyKey = null, object quoteCurrencyKey = null, object idKey                = null)
        {
            if (token == null || !token.HasValues)
            {
                return(null);
            }
            decimal last = token[lastKey].ConvertInvariant <decimal>();

            // parse out volumes, handle cases where one or both do not exist
            token.ParseVolumes(baseVolumeKey, quoteVolumeKey, last, out decimal baseCurrencyVolume, out decimal quoteCurrencyVolume);

            // pull out timestamp
            DateTime timestamp = (timestampKey == null ? CryptoUtility.UtcNow : CryptoUtility.ParseTimestamp(token[timestampKey], timestampType));

            // split apart the symbol if we have a separator, otherwise just put the symbol for base and convert symbol
            string baseCurrency;
            string quoteCurrency;

            if (baseCurrencyKey != null && quoteCurrencyKey != null)
            {
                baseCurrency  = token[baseCurrencyKey].ToStringInvariant();
                quoteCurrency = token[quoteCurrencyKey].ToStringInvariant();
            }
            else if (string.IsNullOrWhiteSpace(marketSymbol))
            {
                throw new ArgumentNullException(nameof(marketSymbol));
            }
            else
            {
                (baseCurrency, quoteCurrency) = api.ExchangeMarketSymbolToCurrencies(marketSymbol);
            }

            // create the ticker and return it
            JToken askValue = token[askKey];
            JToken bidValue = token[bidKey];

            if (askValue is JArray)
            {
                askValue = askValue[0];
            }
            if (bidValue is JArray)
            {
                bidValue = bidValue[0];
            }
            ExchangeTicker ticker = new ExchangeTicker
            {
                MarketSymbol = marketSymbol,
                Ask          = askValue.ConvertInvariant <decimal>(),
                Bid          = bidValue.ConvertInvariant <decimal>(),
                Id           = (idKey == null ? null : token[idKey].ToStringInvariant()),
                Last         = last,
                Volume       = new ExchangeVolume
                {
                    BaseCurrencyVolume  = baseCurrencyVolume,
                    BaseCurrency        = baseCurrency,
                    QuoteCurrencyVolume = quoteCurrencyVolume,
                    QuoteCurrency       = quoteCurrency,
                    Timestamp           = timestamp
                }
            };

            return(ticker);
        }
        /// <summary>
        /// Place a limit order by first querying the order book and then placing the order for a threshold below the bid or above the ask that would fully fulfill the amount.
        /// The order book is scanned until an amount of bids or asks that will fulfill the order is found and then the order is placed at the lowest bid or highest ask price multiplied
        /// by priceThreshold.
        /// </summary>
        /// <param name="symbol">Symbol to sell</param>
        /// <param name="amount">Amount to sell</param>
        /// <param name="isBuy">True for buy, false for sell</param>
        /// <param name="orderBookCount">Amount of bids/asks to request in the order book</param>
        /// <param name="priceThreshold">Threshold below the lowest bid or above the highest ask to set the limit order price at. For buys, this is converted to 1 / priceThreshold.
        /// This can be set to 0 if you want to set the price like a market order.</param>
        /// <param name="thresholdToAbort">If the lowest bid/highest ask price divided by the highest bid/lowest ask price is below this threshold, throw an exception.
        /// This ensures that your order does not buy or sell at an extreme margin.</param>
        /// <param name="abortIfOrderBookTooSmall">Whether to abort if the order book does not have enough bids or ask amounts to fulfill the order.</param>
        /// <returns>Order result</returns>
        public static async Task <ExchangeOrderResult> PlaceSafeMarketOrderAsync(this ExchangeAPI api, string symbol, decimal amount, bool isBuy, int orderBookCount = 100, decimal priceThreshold = 0.9m,
                                                                                 decimal thresholdToAbort = 0.75m, bool abortIfOrderBookTooSmall = false)
        {
            if (priceThreshold > 0.9m)
            {
                throw new APIException("You cannot specify a price threshold above 0.9m, otherwise there is a chance your order will never be fulfilled. For buys, this is " +
                                       "converted to 1.0m / priceThreshold, so always specify the value below 0.9m");
            }
            else if (priceThreshold <= 0m)
            {
                priceThreshold = 1m;
            }
            else if (isBuy && priceThreshold > 0m)
            {
                priceThreshold = 1.0m / priceThreshold;
            }
            ExchangeOrderBook book = await api.GetOrderBookAsync(symbol, orderBookCount);

            if (book == null || (isBuy && book.Asks.Count == 0) || (!isBuy && book.Bids.Count == 0))
            {
                throw new APIException($"Error getting order book for {symbol}");
            }
            decimal counter   = 0m;
            decimal highPrice = decimal.MinValue;
            decimal lowPrice  = decimal.MaxValue;

            if (isBuy)
            {
                foreach (ExchangeOrderPrice ask in book.Asks.Values)
                {
                    counter  += ask.Amount;
                    highPrice = Math.Max(highPrice, ask.Price);
                    lowPrice  = Math.Min(lowPrice, ask.Price);
                    if (counter >= amount)
                    {
                        break;
                    }
                }
            }
            else
            {
                foreach (ExchangeOrderPrice bid in book.Bids.Values)
                {
                    counter  += bid.Amount;
                    highPrice = Math.Max(highPrice, bid.Price);
                    lowPrice  = Math.Min(lowPrice, bid.Price);
                    if (counter >= amount)
                    {
                        break;
                    }
                }
            }
            if (abortIfOrderBookTooSmall && counter < amount)
            {
                throw new APIException($"{(isBuy ? "Buy" : "Sell") } order for {symbol} and amount {amount} cannot be fulfilled because the order book is too thin.");
            }
            else if (lowPrice / highPrice < thresholdToAbort)
            {
                throw new APIException($"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} would place for a price below threshold of {thresholdToAbort}, aborting.");
            }
            ExchangeOrderRequest request = new ExchangeOrderRequest
            {
                Amount            = amount,
                OrderType         = OrderType.Limit,
                Price             = CryptoUtility.RoundAmount((isBuy ? highPrice : lowPrice) * priceThreshold),
                ShouldRoundAmount = true,
                MarketSymbol      = symbol
            };
            ExchangeOrderResult result = await api.PlaceOrderAsync(request);

            // wait about 10 seconds until the order is fulfilled
            int       i        = 0;
            const int maxTries = 20; // 500 ms for each try

            for (; i < maxTries; i++)
            {
                await System.Threading.Tasks.Task.Delay(500);

                result = await api.GetOrderDetailsAsync(result.OrderId, symbol);

                switch (result.Result)
                {
                case ExchangeAPIOrderResult.Filled:
                case ExchangeAPIOrderResult.Canceled:
                case ExchangeAPIOrderResult.Error:
                    break;
                }
            }

            if (i == maxTries)
            {
                throw new APIException($"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} timed out and may not have been fulfilled");
            }

            return(result);
        }
예제 #12
0
        /// <summary>
        /// Parse a JToken into a ticker
        /// </summary>
        /// <param name="api">ExchangeAPI</param>
        /// <param name="token">Token</param>
        /// <param name="symbol">Symbol</param>
        /// <param name="askKey">Ask key</param>
        /// <param name="bidKey">Bid key</param>
        /// <param name="lastKey">Last key</param>
        /// <param name="baseVolumeKey">Base volume key</param>
        /// <param name="convertVolumeKey">Convert volume key</param>
        /// <param name="timestampKey">Timestamp key</param>
        /// <param name="timestampType">Timestamp type</param>
        /// <param name="baseCurrencyKey">Base currency key</param>
        /// <param name="convertCurrencyKey">Convert currency key</param>
        /// <param name="idKey">Id key</param>
        /// <returns>ExchangeTicker</returns>
        internal static ExchangeTicker ParseTicker(this ExchangeAPI api, JToken token, string symbol,
                                                   object askKey, object bidKey, object lastKey, object baseVolumeKey,
                                                   object convertVolumeKey = null, object timestampKey       = null, TimestampType timestampType = TimestampType.None,
                                                   object baseCurrencyKey  = null, object convertCurrencyKey = null, object idKey                = null)
        {
            if (token == null || !token.HasValues)
            {
                return(null);
            }
            decimal last = token[lastKey].ConvertInvariant <decimal>();

            // parse out volumes, handle cases where one or both do not exist
            token.ParseVolumes(baseVolumeKey, convertVolumeKey, last, out decimal baseVolume, out decimal convertVolume);

            // pull out timestamp
            DateTime timestamp = (timestampKey == null ? DateTime.UtcNow : CryptoUtility.ParseTimestamp(token[timestampKey], timestampType));

            // split apart the symbol if we have a separator, otherwise just put the symbol for base and convert symbol
            string baseSymbol;
            string convertSymbol;

            if (baseCurrencyKey != null && convertCurrencyKey != null)
            {
                baseSymbol    = token[baseCurrencyKey].ToStringInvariant();
                convertSymbol = token[convertCurrencyKey].ToStringInvariant();
            }
            else if (string.IsNullOrWhiteSpace(symbol))
            {
                throw new ArgumentNullException(nameof(symbol));
            }
            else if (api.SymbolSeparator.Length != 0)
            {
                string[] pieces = symbol.Split(api.SymbolSeparator[0]);
                if (pieces.Length != 2)
                {
                    throw new ArgumentException($"Symbol does not have the correct symbol separator of '{api.SymbolSeparator}'");
                }
                baseSymbol    = pieces[0];
                convertSymbol = pieces[1];
            }
            else
            {
                baseSymbol = convertSymbol = symbol;
            }

            // create the ticker and return it
            ExchangeTicker ticker = new ExchangeTicker
            {
                Ask    = token[askKey].ConvertInvariant <decimal>(),
                Bid    = token[bidKey].ConvertInvariant <decimal>(),
                Id     = (idKey == null ? null : token[idKey].ToStringInvariant()),
                Last   = last,
                Volume = new ExchangeVolume
                {
                    BaseVolume      = baseVolume,
                    BaseSymbol      = baseSymbol,
                    ConvertedVolume = convertVolume,
                    ConvertedSymbol = convertSymbol,
                    Timestamp       = timestamp
                }
            };

            return(ticker);
        }
예제 #13
0
        /// <summary>
        /// Get cache of symbols metadata and put into a dictionary. This method looks in the cache first, and if found, returns immediately, otherwise makes a network request and puts it in the cache
        /// </summary>
        /// <param name="api">Exchange API</param>
        /// <returns>Dictionary of symbol name and market, or null if there was an error</returns>
        public static async Task <Dictionary <string, ExchangeMarket> > GetExchangeMarketDictionaryFromCacheAsync(this ExchangeAPI api)
        {
            await new SynchronizationContextRemover();
            CachedItem <Dictionary <string, ExchangeMarket> > cacheResult = await api.Cache.Get <Dictionary <string, ExchangeMarket> >(nameof(GetExchangeMarketDictionaryFromCacheAsync), async() =>
            {
                try
                {
                    Dictionary <string, ExchangeMarket> symbolsMetadataDictionary = new Dictionary <string, ExchangeMarket>(StringComparer.OrdinalIgnoreCase);
                    IEnumerable <ExchangeMarket> symbolsMetadata = await api.GetMarketSymbolsMetadataAsync();

                    // build a new lookup dictionary
                    foreach (ExchangeMarket symbolMetadata in symbolsMetadata)
                    {
                        symbolsMetadataDictionary[symbolMetadata.MarketSymbol] = symbolMetadata;
                    }

                    // return the cached dictionary for 4 hours
                    return(new CachedItem <Dictionary <string, ExchangeMarket> >(symbolsMetadataDictionary, CryptoUtility.UtcNow.AddHours(4.0)));
                }
                catch// (Exception ex)
                {
                    // if the network goes down this could log quite a lot of exceptions...
                    //Logger.Error(ex);
                    return(new CachedItem <Dictionary <string, ExchangeMarket> >());
                }
            });

            if (cacheResult.Found)
            {
                return(cacheResult.Value);
            }
            return(null);
        }
            } = true;                                              // some exchanges support going from most recent to oldest, but others, like Gemini must go from oldest to newest

            public ExchangeHistoricalTradeHelper(ExchangeAPI api)
            {
                this.api = api;
            }
예제 #15
0
        /// <summary>
        /// Begins logging exchanges - writes errors to console. You should block the app using Console.ReadLine.
        /// </summary>
        /// <param name="path">Path to write files to</param>
        /// <param name="intervalSeconds">Interval in seconds in between each log calls for each exchange</param>
        /// <param name="terminateAction">Call this when the process is about to exit, like a WM_CLOSE message on Windows.</param>
        /// <param name="exchangeNamesAndSymbols">Exchange names and symbols to log</param>
        public static void LogExchanges(string path, float intervalSeconds, out System.Action terminateAction, params string[] exchangeNamesAndSymbols)
        {
            bool terminating = false;

            System.Action terminator = null;
            path = (string.IsNullOrWhiteSpace(path) ? "./" : path);
            Dictionary <ExchangeLogger, int> errors  = new Dictionary <ExchangeLogger, int>();
            List <ExchangeLogger>            loggers = new List <ExchangeLogger>();

            for (int i = 0; i < exchangeNamesAndSymbols.Length;)
            {
                loggers.Add(new ExchangeLogger(ExchangeAPI.GetExchangeAPI(exchangeNamesAndSymbols[i++]), exchangeNamesAndSymbols[i++], intervalSeconds, path));
            }
            ;
            StreamWriter errorLog = File.CreateText(Path.Combine(path, "errors.txt"));

            foreach (ExchangeLogger logger in loggers)
            {
                logger.Start();
                logger.Error += (log, ex) =>
                {
                    int errorCount;
                    lock (errors)
                    {
                        if (!errors.TryGetValue(log, out errorCount))
                        {
                            errorCount = 0;
                        }
                        errors[log] = ++errorCount;
                    }
                    Console.WriteLine("Errors for {0}: {1}", log.API.Name, errorCount);
                    errorLog.WriteLine("Errors for {0}: {1}", log.API.Name, errorCount);
                };
            }
            terminator = () =>
            {
                if (!terminating)
                {
                    terminating = true;
                    foreach (ExchangeLogger logger in loggers.ToArray())
                    {
                        logger.Stop();
                        logger.Dispose();
                    }
                    loggers.Clear();
                    errorLog.Close();
                }
            };
            terminateAction = terminator;

            // make sure to close properly
            Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e)
            {
                terminator();
            };
            AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) =>
            {
                terminator();
            };

            Console.WriteLine("Loggers \"{0}\" started, press ENTER or CTRL-C to terminate.", string.Join(", ", loggers.Select(l => l.API.Name)));
            errorLog.WriteLine("Loggers \"{0}\" started, press ENTER or CTRL-C to terminate.", string.Join(", ", loggers.Select(l => l.API.Name)));
        }