private IEnumerable <Arbitrage> GetArbitrages(IReadOnlyCollection <OrderBook> orderBooks) { orderBooks = orderBooks.Where(x => x.BestBid != null || x.BestAsk != null) .OrderBy(x => x.AssetPair.Name).ToList(); var result = new List <Arbitrage>(); var watch = Stopwatch.StartNew(); var synthsCount = 0; // O( (n^2)/2 ) // TODO: cache should be implemented to avoid iterating over all asset pairs every time for (var i = 0; i < orderBooks.Count; i++) { if (i == orderBooks.Count - 1) { break; } var target = orderBooks.ElementAt(i); for (var j = i + 1; j < orderBooks.Count; j++) { var source = orderBooks.ElementAt(j); if (target.ToString() == source.ToString()) { continue; } // Calculate all synthetic order books between source order book and target order book var synthOrderBooks = SynthOrderBook.GetSynthsFromAll(target.AssetPair, source, orderBooks); synthsCount += synthOrderBooks.Count; // Compare each synthetic with target foreach (var synthOrderBook in synthOrderBooks) { decimal spread = 0; decimal volume = 0; decimal pnL = 0; string targetSide = null; IReadOnlyList <string> marketMakers = new List <string>(); if (target.BestBid?.Price > synthOrderBook.BestAsk?.Price) { spread = Arbitrage.GetSpread(target.BestBid.Price, synthOrderBook.BestAsk.Price); var volumePnL = Arbitrage.GetArbitrageVolumeAndPnL(target.Bids, synthOrderBook.Asks); Debug.Assert(volumePnL?.Volume != null); Debug.Assert(volumePnL?.PnL != null); targetSide = Bid; marketMakers = GetMarketMakers(target.BestBid, synthOrderBook.GetLimitOrdersOfBestAsk()); volume = volumePnL.Value.Volume; pnL = volumePnL.Value.PnL; } if (synthOrderBook.BestBid?.Price > target.BestAsk?.Price) { spread = Arbitrage.GetSpread(synthOrderBook.BestBid.Price, target.BestAsk.Price); var volumePnL = Arbitrage.GetArbitrageVolumeAndPnL(synthOrderBook.Bids, target.Asks); Debug.Assert(volumePnL?.Volume != null); Debug.Assert(volumePnL?.PnL != null); targetSide = Ask; marketMakers = GetMarketMakers(target.BestAsk, synthOrderBook.GetLimitOrdersOfBestBid()); volume = volumePnL.Value.Volume; pnL = volumePnL.Value.PnL; } if (targetSide == null) // no arbitrages { continue; } var volumeInUsd = _orderBooksService.ConvertToUsd(target.AssetPair.Base.Id, volume); var pnLInUsd = _orderBooksService.ConvertToUsd(target.AssetPair.Quote.Id, pnL); var lykkeArbitrage = new Arbitrage( target.AssetPair, source.AssetPair, spread, targetSide, synthOrderBook.ConversionPath, volume, volumeInUsd, pnL, pnLInUsd, target.BestBid?.Price, target.BestAsk?.Price, synthOrderBook.BestBid?.Price, synthOrderBook.BestAsk?.Price, marketMakers, DateTime.UtcNow ); result.Add(lykkeArbitrage); } } } watch.Stop(); if (watch.ElapsedMilliseconds > 1000) { _log.Info($"Performance issue - {watch.ElapsedMilliseconds} ms, {result.Count} arbitrages, {orderBooks.Count} order books," + $"{synthsCount} synthetic order books created."); } return(result.ToList()); }
public Matrix GetMatrix(string assetPair, bool isPublic = false, bool depositFee = false, bool tradingFee = false) { if (string.IsNullOrWhiteSpace(assetPair)) { return(null); } var result = new Matrix(assetPair); var s = Settings(); // Filter by asset pair var orderBooks = _orderBooksService.GetAll().Where(x => string.Equals(x.AssetPair.Name, assetPair.Replace("/", ""), StringComparison.OrdinalIgnoreCase)).ToList(); // Filter by exchanges if (isPublic && s.PublicMatrixExchanges.Any()) { orderBooks = orderBooks.Where(x => s.PublicMatrixExchanges.Keys.Contains(x.Source)).ToList(); } // Order by exchange name orderBooks = orderBooks.OrderBy(x => x.Source).ToList(); // Fees var exchangesFees = new List <ExchangeFees>(); foreach (var orderBook in orderBooks) { var exchangeFees = s.ExchangesFees.SingleOrDefault(x => x.ExchangeName.Equals(orderBook.Source, StringComparison.OrdinalIgnoreCase)) ?? new ExchangeFees(orderBook.Source, 0, 0); // deposit and trading fees = 0 by default exchangesFees.Add(exchangeFees); } // Order books with fees var useFees = depositFee || tradingFee; var orderBooksWithFees = useFees ? new List <OrderBook>() : null; if (useFees) { // Put fees into prices foreach (var orderBook in orderBooks) { var exchangeFees = exchangesFees.Single(x => x.ExchangeName == orderBook.Source); var totalFee = (depositFee ? exchangeFees.DepositFee : 0) + (tradingFee ? exchangeFees.TradingFee : 0); var orderBookWithFees = orderBook.DeepClone(totalFee); orderBooksWithFees.Add(orderBookWithFees); } orderBooks = orderBooksWithFees; } var exchangesNames = orderBooks.Select(x => x.Source).ToList(); // Raplace exchange names if (isPublic && s.PublicMatrixExchanges.Any()) { exchangesNames = exchangesNames.Select(x => x.Replace(x, s.PublicMatrixExchanges[x])).ToList(); } var matrixSide = exchangesNames.Count; for (var row = 0; row < matrixSide; row++) { var orderBookRow = orderBooks[row]; var cellsRow = new List <MatrixCell>(); var isActual = (DateTime.UtcNow - orderBookRow.Timestamp).TotalSeconds < s.ExpirationTimeInSeconds; var assetPairObj = orderBookRow.AssetPair; // Add ask and exchange var exchangeName = orderBookRow.Source; var exchangeFees = exchangesFees.Single(x => x.ExchangeName == exchangeName); result.Exchanges.Add(new Exchange(exchangeName, isActual, exchangeFees)); result.Asks.Add(GetPriceWithAccuracy(orderBookRow.BestAsk?.Price, assetPairObj)); for (var col = 0; col < matrixSide; col++) { var orderBookCol = orderBooks[col]; // Add bid if (row == 0) { result.Bids.Add(GetPriceWithAccuracy(orderBookCol.BestBid?.Price, assetPairObj)); } // If the same exchanges than cell = null MatrixCell cell; if (row == col) { cellsRow.Add(null); continue; } // If current cell doesn't have prices on one or both sides. if (orderBookCol.BestBid == null || orderBookRow.BestAsk == null) { cell = new MatrixCell(null, null); cellsRow.Add(cell); continue; } var spread = Arbitrage.GetSpread(orderBookCol.BestBid.Value.Price, orderBookRow.BestAsk.Value.Price); spread = Math.Round(spread, 2); decimal?volume = null; if (spread < 0) { volume = Arbitrage.GetArbitrageVolumePnL(orderBookCol.Bids, orderBookRow.Asks)?.Volume; volume = GetVolumeWithAccuracy(volume, assetPairObj); } cell = new MatrixCell(spread, volume); cellsRow.Add(cell); } // row ends result.Cells.Add(cellsRow); } result.DateTime = DateTime.UtcNow; return(result); }
private Task <IReadOnlyList <LykkeArbitrageRow> > GetArbitragesAsync(IReadOnlyList <OrderBook> orderBooks) { orderBooks = orderBooks.Where(x => x.BestBid.HasValue || x.BestAsk.HasValue).OrderBy(x => x.AssetPair.Name).ToList(); var result = new List <LykkeArbitrageRow>(); var watch = Stopwatch.StartNew(); var synthsCount = 0; // O( (n^2)/2 ) for (var i = 0; i < orderBooks.Count; i++) { if (i == orderBooks.Count - 1) { break; } var target = orderBooks.ElementAt(i); for (var j = i + 1; j < orderBooks.Count; j++) { var source = orderBooks.ElementAt(j); if (target.ToString() == source.ToString()) { continue; } // Calculate all synthetic order books between a source order book and a target order book var synthOrderBooks = SynthOrderBook.GetSynthsFromAll(target.AssetPair, source, orderBooks); synthsCount += synthOrderBooks.Count; // Compare each synthetic with current target asset pair foreach (var synthOrderBook in synthOrderBooks) { decimal spread = 0; decimal volume = 0; decimal pnL = 0; string targetSide = null; // Bid side if (target.BestBid?.Price > synthOrderBook.BestAsk?.Price) { spread = Arbitrage.GetSpread(target.BestBid.Value.Price, synthOrderBook.BestAsk.Value.Price); var volumePnL = Arbitrage.GetArbitrageVolumePnL(target.Bids, synthOrderBook.Asks); Debug.Assert(volumePnL?.Volume != null); Debug.Assert(volumePnL?.PnL != null); targetSide = "Bid"; volume = volumePnL.Value.Volume; pnL = volumePnL.Value.PnL; } // Ask side if (synthOrderBook.BestBid?.Price > target.BestAsk?.Price) { spread = Arbitrage.GetSpread(synthOrderBook.BestBid.Value.Price, target.BestAsk.Value.Price); var volumePnL = Arbitrage.GetArbitrageVolumePnL(synthOrderBook.Bids, target.Asks); Debug.Assert(volumePnL?.Volume != null); Debug.Assert(volumePnL?.PnL != null); targetSide = "Ask"; volume = volumePnL.Value.Volume; pnL = volumePnL.Value.PnL; } if (string.IsNullOrWhiteSpace(targetSide)) // no arbitrage { continue; } var baseToUsdRate = Convert(target.AssetPair.Base, Usd, orderBooks); var quoteToUsdRate = Convert(target.AssetPair.Quote, Usd, orderBooks); var volumeInUsd = volume * baseToUsdRate; volumeInUsd = volumeInUsd.HasValue ? Math.Round(volumeInUsd.Value) : (decimal?)null; var pnLInUsd = pnL * quoteToUsdRate; pnLInUsd = pnLInUsd.HasValue ? Math.Round(pnLInUsd.Value) : (decimal?)null; var lykkeArbitrage = new LykkeArbitrageRow(target.AssetPair, source.AssetPair, spread, targetSide, synthOrderBook.ConversionPath, volume, target.BestBid?.Price, target.BestAsk?.Price, synthOrderBook.BestBid?.Price, synthOrderBook.BestAsk?.Price, volumeInUsd, pnL, pnLInUsd); result.Add(lykkeArbitrage); } } } watch.Stop(); if (watch.ElapsedMilliseconds > 1000) { _log.Info($"{watch.ElapsedMilliseconds} ms, {result.Count} arbitrages, {orderBooks.Count} order books, {synthsCount} synthetic order books."); } return(Task.FromResult(result.OrderBy(x => x.Target).ThenBy(x => x.Source).ToList() as IReadOnlyList <LykkeArbitrageRow>)); }
public Task <Dictionary <string, Arbitrage> > CalculateArbitrages() { var newArbitrages = new Dictionary <string, Arbitrage>(); var actualSynthOrderBooks = GetActualSynthOrderBooks(); var settings = Settings(); // For each asset pair var uniqueAssetPairs = actualSynthOrderBooks.Select(x => x.AssetPair).Distinct().ToList(); foreach (var assetPair in uniqueAssetPairs) { var watch = Stopwatch.StartNew(); var assetPairSynthOrderBooks = actualSynthOrderBooks.Where(x => x.AssetPair.Equals(assetPair)).ToList(); // For each synthetic order book make a line for every ask and every bid var bidsAndAsks = CalculateSynthOrderBookLines(assetPairSynthOrderBooks); var bidsAndAsksMs = watch.ElapsedMilliseconds; if (!bidsAndAsks.HasValue) { return(Task.FromResult(newArbitrages)); } var totalItarations = 0; var possibleArbitrages = 0; var bids = bidsAndAsks.Value.bids; var bidsCount = bids.Count; var asks = bidsAndAsks.Value.asks; var asksCount = asks.Count; // Calculate arbitrage for every ask and every higher bid for (var b = 0; b < bidsCount; b++) { var bid = bids[b]; var bidPrice = bid.Price; for (var a = 0; a < asksCount; a++) { totalItarations++; var ask = asks[a]; var askPrice = ask.Price; if (askPrice >= bidPrice) { continue; } possibleArbitrages++; // Filtering by spread var spread = Arbitrage.GetSpread(bidPrice, askPrice); if (settings.MinSpread < 0 && spread < settings.MinSpread) { continue; } var bidVolume = bid.Volume; var askVolume = ask.Volume; var volume = askVolume < bidVolume ? askVolume : bidVolume; // Filtering by PnL var pnL = Arbitrage.GetPnL(bidPrice, askPrice, volume); if (settings.MinimumPnL > 0 && pnL < settings.MinimumPnL) { continue; } var key = Arbitrage.FormatConversionPath(bid.SynthOrderBook.ConversionPath, ask.SynthOrderBook.ConversionPath); if (newArbitrages.TryGetValue(key, out var existed)) { var newpnL = Arbitrage.GetPnL(bidPrice, askPrice, volume); // Best PnL if (newpnL <= existed.PnL) { continue; } var arbitrage = new Arbitrage(assetPair, bid.SynthOrderBook, new VolumePrice(bid.Price, bid.Volume), ask.SynthOrderBook, new VolumePrice(ask.Price, ask.Volume)); newArbitrages[key] = arbitrage; } else { var arbitrage = new Arbitrage(assetPair, bid.SynthOrderBook, new VolumePrice(bid.Price, bid.Volume), ask.SynthOrderBook, new VolumePrice(ask.Price, ask.Volume)); newArbitrages.Add(key, arbitrage); } } } watch.Stop(); if (watch.ElapsedMilliseconds > 1000) { _log.Info($"{watch.ElapsedMilliseconds} ms, {newArbitrages.Count} arbitrages, {actualSynthOrderBooks.Count} actual synthetic order books, {bidsAndAsksMs} ms for bids and asks, {bids.Count} bids, {asks.Count} asks, {totalItarations} iterations, {possibleArbitrages} possible arbitrages."); } } return(Task.FromResult(newArbitrages)); }