/// <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="compress">Whether to compress the log files</param> /// <param name="exchangeNamesAndSymbols">Exchange names and symbols to log</param> public static void LogExchanges(string path, float intervalSeconds, out Action terminateAction, 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(ExchangeAPI.GetExchangeAPI(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(); } }; terminateAction = terminator; // 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))); }
/// <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) { if (string.IsNullOrEmpty(api.Name)) { continue; } Apis[api.Name] = 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; }
/// <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="api"></param> /// <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 <List <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"); } if (priceThreshold <= 0m) { priceThreshold = 1m; } else if (isBuy && priceThreshold > 0m) { priceThreshold = 1.0m / priceThreshold; } ExchangeDepth book = null; //todo await api.GetDepthAsync(symbol, orderBookCount); if (book == null || isBuy && book.Asks.Count == 0 || !isBuy && book.Bids.Count == 0) { throw new APIException($"Error getting order book for {symbol}"); } var counter = 0m; var highPrice = decimal.MinValue; var lowPrice = decimal.MaxValue; if (isBuy) { foreach (var 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 (var 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."); } 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."); } var request = new ExchangeOrderRequest { Amount = amount, OrderType = OrderType.Limit, Price = CryptoUtility.RoundAmount((isBuy ? highPrice : lowPrice) * priceThreshold), ShouldRoundAmount = true, Symbol = null //todo symbol }; var result = await api.PlaceOrdersAsync(request); // wait about 10 seconds until the order is fulfilled var i = 0; const int maxTries = 20; // 500 ms for each try for (; i < maxTries; i++) { await Task.Delay(500); } //TODO //result = await api.GetCanceledOrdersAsync(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); }