private async Task CreateEvent(LimitQueueItem.LimitOrderWithTrades limitOrderWithTrades, OrderStatus status) { var order = limitOrderWithTrades.Order; var type = order.Volume > 0 ? OrderType.Buy : OrderType.Sell; var assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(order.AssetPairId); var date = status == OrderStatus.InOrderBook ? limitOrderWithTrades.Order.CreatedAt : DateTime.UtcNow; var insertRequest = new LimitTradeEventInsertRequest { Volume = order.Volume, Type = type, OrderId = order.Id, Status = status, AssetId = assetPair?.BaseAssetId, ClientId = order.ClientId, Price = order.Price, AssetPair = order.AssetPairId, DateTime = date }; await _limitTradeEventsRepositoryClient.CreateAsync(insertRequest); _log.Info(nameof(OperationHistoryProjection), $"Client {order.ClientId}. Limit trade {order.Id}. State has changed -> {status}", JsonConvert.SerializeObject(insertRequest, Formatting.Indented)); }
public async Task <decimal?> ConvertPriceAsync(string assetPairId, decimal price) { AssetPair assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(assetPairId); if (assetPair.QuotingAssetId == UsdAssetId) { return(price); } IReadOnlyCollection <CrossRateInstrument> crossInstruments = await GetAllAsync(); CrossRateInstrument crossInstrument = crossInstruments.SingleOrDefault(o => o.AssetPairId == assetPairId); if (crossInstrument == null) { _log.WarningWithDetails("Cross instrument not found", assetPairId); return(null); } Quote quote = await _quoteService .GetAsync(crossInstrument.QuoteSource, crossInstrument.ExternalAssetPairId); if (quote == null) { _log.WarningWithDetails("No quote for cross instrument", crossInstrument); return(null); } (decimal crossPrice, decimal _) = Calculator.CalculateCrossMidPrice(price, quote, crossInstrument.IsInverse); return(crossPrice); }
public Task <AssetPair> TryGetAssetPairAsync(string assetPairId) { return(Policy .Handle <Exception>() .WaitAndRetryForeverAsync( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timespan) => _log.Error(exception, context: new { assetPairId = assetPairId })) .ExecuteAsync(() => _apiService.TryGetAssetPairAsync(assetPairId))); }
public Task <AssetPair> TryGetAssetPairAsync(string assetPairId) { return(Policy .Handle <Exception>() .WaitAndRetryForeverAsync( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timespan) => _log.WriteErrorAsync("Get asset pair with retry", assetPairId, exception)) .ExecuteAsync(() => _apiService.TryGetAssetPairAsync(assetPairId))); }
private async Task <ClientTrade[]> CreateTrades(LimitQueueItem.LimitOrderWithTrades limitOrderWithTrades) { if (limitOrderWithTrades.Trades == null || limitOrderWithTrades.Trades.Count == 0) { return(new ClientTrade[0]); } var assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(limitOrderWithTrades.Order.AssetPairId); var trades = limitOrderWithTrades.ToDomain(assetPair); return(trades.ToArray()); }
private async Task <bool> ValidateBalanceAsync(InternalOrder internalOrder) { Instrument instrument = await _instrumentService.TryGetByAssetPairIdAsync(internalOrder.AssetPairId); AssetPair assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(instrument.AssetPairId); Asset asset; decimal amount; if (internalOrder.Type == LimitOrderType.Sell) { asset = await _assetsServiceWithCache.TryGetAssetAsync(assetPair.QuotingAssetId); amount = (internalOrder.Volume * internalOrder.Price).TruncateDecimalPlaces(asset.Accuracy, true); } else { asset = await _assetsServiceWithCache.TryGetAssetAsync(assetPair.BaseAssetId); amount = internalOrder.Volume; } Balance balance = await _balanceService.GetByAssetIdAsync(ExchangeNames.Lykke, asset.Id); if (balance.Amount < amount) { internalOrder.Status = InternalOrderStatus.Rejected; internalOrder.RejectReason = Errors.NotEnoughLiquidity; await _internalOrderRepository.UpdateAsync(internalOrder); return(false); } return(true); }
public async Task CalculateAsync(LykkeTrade lykkeTrade) { WalletSettings walletSettings = await _walletSettingsService.GetWalletAsync(lykkeTrade.ClientId); if (walletSettings == null || !walletSettings.Enabled) { return; } AssetPair assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(lykkeTrade.AssetPairId); string[] assets = walletSettings.Assets .Intersect(new[] { assetPair.BaseAssetId, assetPair.QuotingAssetId }) .ToArray(); if (!assets.Any()) { return; } var tradeData = new TradeData { Id = lykkeTrade.Id, Exchange = ExchangeNames.Lykke, AssetPair = assetPair.Id, BaseAsset = assetPair.BaseAssetId, QuoteAsset = assetPair.QuotingAssetId, Price = lykkeTrade.Price, Volume = lykkeTrade.Volume, Type = lykkeTrade.Type, Time = lykkeTrade.Time, LimitOrderId = lykkeTrade.LimitOrderId, OppositeClientId = lykkeTrade.OppositeClientId, OppositeLimitOrderId = lykkeTrade.OppositeLimitOrderId }; foreach (string assetId in assets) { AssetRealisedPnL assetRealisedPnL = await CalculateAsync(tradeData, walletSettings.Id, assetId); await _assetRealisedPnLRepository.InsertAsync(assetRealisedPnL); _cache.Set(assetRealisedPnL); } _log.InfoWithDetails("Lykke trade handled", tradeData); }
public async Task <ClientTrade[]> Create(TradeQueueItem.MarketOrder order, List <TradeQueueItem.TradeInfo> trades, string clientId) { var assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(order.AssetPairId); var trade = trades[0]; var price = ConvertExtensions.CalcEffectivePrice(trades, assetPair, order.Volume > 0); var marketVolume = trades.Sum(x => x.MarketVolume); var limitVolume = trades.Sum(x => x.LimitVolume); var marketAssetRecord = CreateCommonPartForTradeRecord(trade, order.Id, price, order.AssetPairId); var limitAssetRecord = CreateCommonPartForTradeRecord(trade, order.Id, price, order.AssetPairId); marketAssetRecord.ClientId = limitAssetRecord.ClientId = clientId; marketAssetRecord.Amount = marketVolume; if (Math.Sign(marketVolume) == Math.Sign(limitVolume)) { marketAssetRecord.Amount *= -1; } marketAssetRecord.AssetId = trade.MarketAsset; limitAssetRecord.Amount = limitVolume; limitAssetRecord.AssetId = trade.LimitAsset; foreach (var t in trades) { var transfer = t.Fees?.FirstOrDefault()?.Transfer; if (transfer != null) { if (marketAssetRecord.AssetId == transfer.Asset) { marketAssetRecord.FeeSize += (double)transfer.Volume; marketAssetRecord.FeeType = FeeType.Absolute; } else { limitAssetRecord.FeeSize += (double)transfer.Volume; limitAssetRecord.FeeType = FeeType.Absolute; } } } marketAssetRecord.Id = Core.Domain.CashOperations.Utils.GenerateRecordId(marketAssetRecord.DateTime); limitAssetRecord.Id = Core.Domain.CashOperations.Utils.GenerateRecordId(limitAssetRecord.DateTime); return(new[] { marketAssetRecord, limitAssetRecord }); }
public async Task <decimal> GetFiatEquityMarkup(string assetPairId) { AssetPair lykkeAssetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(assetPairId); Domain.AssetSettings quoteAssetSettings = await _assetSettingsService.GetByLykkeIdAsync(lykkeAssetPair.QuotingAssetId); if (quoteAssetSettings == null) { _log.WarningWithDetails("Can't find asset settings while calculating fiat equity.", lykkeAssetPair.QuotingAssetId); return(0); } if (quoteAssetSettings.IsCrypto) { return(0); } decimal fiatEquity = GetFiatEquity(); if (fiatEquity >= 0) { return(0); } MarketMakerSettings marketMakerSettings = await _marketMakerSettingsService.GetAsync(); decimal thresholdFrom = marketMakerSettings.FiatEquityThresholdFrom; decimal thresholdTo = marketMakerSettings.FiatEquityThresholdTo; decimal markupFrom = marketMakerSettings.FiatEquityMarkupFrom; decimal markupTo = marketMakerSettings.FiatEquityMarkupTo; if (thresholdFrom >= thresholdTo) { return(0); } decimal markup = CalculateMarkup(fiatEquity, thresholdFrom, thresholdTo, markupFrom, markupTo); return(markup); }
public async Task <Order> CreateAsync(string walletId, string priceId, decimal quotingVolume) { var price = await _pricesService.GetAsync(priceId); if (price == null) { throw new EntityNotFoundException(); } var instrument = await _instrumentsAccessService.GetByAssetPairIdAsync(price.AssetPair); if (instrument.State != InstrumentState.Active) { throw new FailedOperationException($"Instrument {instrument.AssetPair} isn't active."); } var defaultSettings = await _settingsService.GetDefaultSettingsAsync(); if (DateTime.UtcNow > price.ValidTo + (instrument.OverlapTime ?? defaultSettings.OverlapTime)) { throw new FailedOperationException("Given price too old."); } if (quotingVolume > price.QuotingVolume) { throw new FailedOperationException("Requested volume higher than initial."); } var pair = await _assetsService.TryGetAssetPairAsync(price.AssetPair); var baseAsset = await _assetsService.TryGetAssetAsync(pair.BaseAssetId); var baseVolume = quotingVolume == price.QuotingVolume ? price.BaseVolume : (quotingVolume / price.Value).TruncateDecimalPlaces(baseAsset.Accuracy, price.Type == OrderType.Sell); var order = new Order { Id = Guid.NewGuid().ToString(), WalletId = walletId, Type = price.Type, AssetPair = price.AssetPair, QuotingVolume = quotingVolume, BaseVolume = baseVolume, PriceId = priceId, CreatedTime = DateTime.UtcNow, Status = OrderStatus.New, ReserveTransferId = Guid.NewGuid().ToString(), SettlementTransferId = Guid.NewGuid().ToString() }; await _ordersRepository.InsertAsync(order); _log.Info("Order was created.", order); try { await _internalTransfersService.TransferAsync( order.ReserveTransferId, walletId, await _settingsService.GetWalletIdAsync(), order.Type == OrderType.Buy ?pair.QuotingAssetId : pair.BaseAssetId, order.Type == OrderType.Buy ?quotingVolume : baseVolume); } catch (MeNotEnoughFundsException) { var rejectReason = "Client doesn't have enough funds."; await PersistWithStatusAsync(order, OrderStatus.Cancelled, rejectReason); throw new FailedOperationException(rejectReason); } catch (MeOperationException e) { var rejectReason = "ME call failed."; await PersistWithStatusAsync(order, OrderStatus.Cancelled, rejectReason); _log.Warning(rejectReason, priceId, e); throw new FailedOperationException(rejectReason); } catch (FailedOperationException e) { var rejectReason = "ME call failed."; await PersistWithStatusAsync(order, OrderStatus.Cancelled, rejectReason); _log.Warning(rejectReason, priceId, e); throw; } await PersistWithStatusAsync(order, OrderStatus.Reserved); return(order); }
public async Task <ValidationResult> ValidateLimitOrderAsync(string walletId, string assetPairId, OrderAction side, decimal price, decimal volume) { if (price <= 0) { return(new ValidationResult { Message = ErrorMessages.LessThanZero(nameof(price)), FieldName = nameof(price) }); } if (volume <= 0) { return(new ValidationResult { Message = ErrorMessages.LessThanZero(nameof(volume)), FieldName = nameof(volume) }); } var assetPair = await _assetsService.TryGetAssetPairAsync(assetPairId); if (assetPair == null) { return(new ValidationResult { Message = ErrorMessages.AssetPairNotFound, FieldName = nameof(assetPairId) }); } if (volume < (decimal)assetPair.MinVolume) { return(new ValidationResult { Message = ErrorMessages.MustBeGreaterThan(nameof(volume), assetPair.MinVolume.ToString(CultureInfo.InvariantCulture)), FieldName = nameof(volume) }); } decimal totalVolume; string asset; if (side == OrderAction.Buy) { asset = assetPair.QuotingAssetId; totalVolume = price * volume; } else { asset = assetPair.BaseAssetId; totalVolume = volume; } var assetBalance = await _balancesClient.GetClientBalanceByAssetId( new ClientBalanceByAssetIdModel { ClientId = walletId, AssetId = asset }); if (assetBalance == null || assetBalance.Balance - assetBalance.Reserved < totalVolume) { return(new ValidationResult { Message = ErrorMessages.NotEnoughFunds, FieldName = nameof(volume) }); } return(null); }
private async Task CheckApi(string key, ILiquidityEngineClient client) { if (_lastTime == default(DateTime)) { _lastTime = DateTime.UtcNow.Subtract(Interval); } var fromDate = _lastTime; var toDate = DateTime.UtcNow; Log.Info($"Check api Start. summary. Api: {key}. LastTime: {_lastTime:yyyy-MM-dd HH:mm:ss}"); var sb = new StringBuilder(); sb.AppendLine($"=== {fromDate:yyyy/MM/dd HH:mm:ss} ==="); sb.AppendLine($"=== {toDate:yyyy/MM/dd HH:mm:ss} ==="); sb.Append(Environment.NewLine); sb.AppendLine("Liquidity Engine Summary Statistics"); sb.Append(Environment.NewLine); var countTrade = 0; try { var data = await client.Reports.GetPositionsReportAsync(fromDate, toDate, int.MaxValue); var report = data .Where(x => x.IsClosed) .Where(x => x.CloseDate > fromDate) .Where(x => x.CloseDate <= toDate) .ToList(); var summary = await client.Reports.GetBalanceIndicatorsReportAsync(); var markups = await client.InstrumentMarkupsApi.GetAllAsync(); var grouped = report.GroupBy(x => x.AssetPairId).OrderBy(x => x.Key); var result = new List <string>(); var totalPl = 0m; var totalTurnover = 0m; foreach (var assetPairTrades in grouped) { var assetPairId = assetPairTrades.Key; var count = assetPairTrades.Count(); var buyVolume = assetPairTrades.Where(x => x.Type == PositionType.Long).Sum(x => x.Volume); var sellVolume = assetPairTrades.Where(x => x.Type == PositionType.Short).Sum(x => x.Volume); var isPnLInUsd = assetPairTrades.All(x => x.PnLUsd.HasValue); var pnLInUsd = isPnLInUsd ? assetPairTrades.Sum(x => x.PnLUsd ?? 0) : (decimal?)null; var pnL = assetPairTrades.Sum(x => x.PnL ?? 0); var assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(assetPairId); var quoteAssetId = assetPair?.QuotingAssetId; var asset = await _assetsServiceWithCache.TryGetAssetAsync(quoteAssetId); var quoteAssetDisplayId = quoteAssetId == null ? null : asset.Id; var quoteAssetStr = string.IsNullOrWhiteSpace(quoteAssetDisplayId) ? "[quote asset]" : quoteAssetDisplayId; var pnLStr = pnLInUsd.HasValue ? $"{Math.Round(pnLInUsd.Value, 4)}$" : $"{Math.Round(pnL, 4)} {quoteAssetStr}"; var lastTrade = assetPairTrades.Where(e => e.PnLUsd.HasValue && e.PnLUsd.Value > 0).OrderByDescending(e => e.ClosePrice).FirstOrDefault(); var latsUsdPrice = (lastTrade != null && lastTrade.PnLUsd.HasValue && lastTrade.PnL.HasValue) ? lastTrade.PnLUsd.Value / lastTrade.PnL.Value : 0m; decimal turnover = assetPairTrades.Sum(e => e.Volume * e.ClosePrice) ?? 0m; var turnoverUsd = turnover * latsUsdPrice; var assetPairMessage = $"{assetPairId}; " + $"PL={pnLStr}; " + $"Turnover: {Math.Round(turnoverUsd, 0)} $; "; result.Add(assetPairMessage); assetPairMessage = $". Count: {count}; " + $"Sell: {Math.Round(sellVolume, 6)}; " + $"Buy: {Math.Round(buyVolume, 6)}; "; var markup = markups.SingleOrDefault(x => x.AssetPairId == assetPairId); if (markup != null) { var bidMarkup = $"{markup.TotalBidMarkup*100:0.##}%"; var askMarkup = markup.TotalAskMarkup == -1 ? "stopped" : $"{markup.TotalAskMarkup*100:0.##}%"; assetPairMessage += $"BidMarkup: {bidMarkup}, AskMarkup: {askMarkup}"; } result.Add(assetPairMessage); totalPl += pnLInUsd ?? 0m; totalTurnover += turnoverUsd; } sb.AppendLine(string.Join(Environment.NewLine, result)); sb.Append($""); var fee = totalTurnover > 0 ? totalPl / totalTurnover : 0m; sb.Append($"Total PL: {Math.Round(totalPl, 2)} $, Turnover: {Math.Round(totalTurnover, 2)} $, fee: {Math.Round(fee * 100, 2)} %"); _lastTime = DateTime.UtcNow; await SendMessage(sb.ToString()); var summaryMessage = $"Equity: {(int) summary.Equity} $. RiskExposure: {(int) summary.RiskExposure} $"; await SendMessage(summaryMessage); var fiatSettlement = await client.Trades.GetSettlementTradesAsync(); if (fiatSettlement.Any(e => !e.IsCompleted)) { sb.Clear(); sb.AppendLine("Detect fiat trades without settlement in lykke: "); foreach (var model in fiatSettlement.Where(e => !e.IsCompleted)) { sb.AppendLine($"{model.AssetPair}, {model.Type}, price: {model.Price}, volume: {model.Volume}"); } await SendMessage(sb.ToString()); } } catch (Exception ex) { Log.Error(ex); } Log.Info($"Check api complete. Found: {countTrade} asset pairs. Api: {key}. LastTime: {_lastTime:yyyy-MM-dd HH:mm:ss}"); }
private async Task <OrderBook> CalculateDirectOrderBookAsync(Instrument instrument) { Quote[] quotes = _b2C2OrderBookService.GetQuotes(instrument.AssetPairId); if (quotes == null || quotes.Length != 2) { _log.WarningWithDetails("No quotes for instrument", instrument.AssetPairId); return(null); } Balance baseAssetBalance = null; Balance quoteAssetBalance = null; TimeSpan timeSinceLastTrade = TimeSpan.Zero; if (instrument.AllowSmartMarkup) { AssetPairLink assetPairLink = await _assetPairLinkService.GetByInternalAssetPairIdAsync(instrument.AssetPairId); if (assetPairLink != null && !assetPairLink.IsEmpty()) { baseAssetBalance = await _balanceService.GetByAssetIdAsync(ExchangeNames.B2C2, assetPairLink.ExternalBaseAssetId); quoteAssetBalance = await _balanceService.GetByAssetIdAsync(ExchangeNames.B2C2, assetPairLink.ExternalQuoteAssetId); timeSinceLastTrade = DateTime.UtcNow - _tradeService.GetLastInternalTradeTime(instrument.AssetPairId); } else { _log.WarningWithDetails("The asset pair link does not configured", new { instrument.AssetPairId }); } } AssetPair assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(instrument.AssetPairId); Asset baseAsset = await _assetsServiceWithCache.TryGetAssetAsync(assetPair.BaseAssetId); MarketMakerSettings marketMakerSettings = await _marketMakerSettingsService.GetAsync(); decimal pnLStopLossMarkup = await _pnLStopLossEngineService.GetTotalMarkupByAssetPairIdAsync(assetPair.Id); decimal fiatEquityStopLossMarkup = await _fiatEquityStopLossService.GetFiatEquityMarkup(assetPair.Id); decimal stopLossMarkup = await _noFreshQuotesStopLossService.GetNoFreshQuotesMarkup(assetPair.Id); IReadOnlyCollection <LimitOrder> limitOrders = Calculator.CalculateLimitOrders( quotes[0], quotes[1], instrument.Levels.ToArray(), baseAssetBalance?.Amount ?? 0, quoteAssetBalance?.Amount ?? 0, (int)timeSinceLastTrade.TotalSeconds, instrument.HalfLifePeriod, instrument.AllowSmartMarkup, marketMakerSettings.LimitOrderPriceMarkup, pnLStopLossMarkup, fiatEquityStopLossMarkup, stopLossMarkup, assetPair.Accuracy, baseAsset.Accuracy); await ValidateQuoteTimeoutAsync(limitOrders, quotes[0]); await ValidateQuoteTimeoutAsync(limitOrders, quotes[1]); ValidateMinVolume(limitOrders, (decimal)assetPair.MinVolume); await ValidatePriceAsync(limitOrders); await ValidateBalanceAsync(limitOrders, assetPair); WriteInfoLog(instrument.AssetPairId, quotes, timeSinceLastTrade, limitOrders, "Direct limit orders calculated"); return(new OrderBook { AssetPairId = instrument.AssetPairId, Time = DateTime.UtcNow, LimitOrders = limitOrders, IsDirect = true }); }
public async Task <IActionResult> PlaceMarketOrder([FromBody] MarketOrderRequest request) { var id = Guid.NewGuid(); var asset = await _assetsServiceWithCache.TryGetAssetAsync(request.AssetId); var pair = await _assetsServiceWithCache.TryGetAssetPairAsync(request.AssetPairId); if (asset == null) { return(NotFound($"Asset '{request.AssetId}' not found.")); } if (pair == null) { return(NotFound($"Asset pair '{request.AssetPairId}' not found.")); } if (pair.IsDisabled) { return(BadRequest($"Asset pair '{request.AssetPairId}' disabled.")); } if (request.AssetId != pair.BaseAssetId && request.AssetId != pair.QuotingAssetId) { return(BadRequest()); } var baseAsset = await _assetsServiceWithCache.TryGetAssetAsync(pair.BaseAssetId); var quotingAsset = await _assetsServiceWithCache.TryGetAssetAsync(pair.QuotingAssetId); var tradingSession = await _clientSessionsClient.GetTradingSession(_lykkePrincipal.GetToken()); var confirmationRequired = _baseSettings.EnableSessionValidation && !(tradingSession?.Confirmed ?? false); if (confirmationRequired) { return(BadRequest("Session confirmation is required")); } var command = new CreateMarketOrderCommand { AssetId = request.AssetId, AssetPair = new AssetPairModel { Id = request.AssetPairId, BaseAsset = ConvertAssetToAssetModel(baseAsset), QuotingAsset = ConvertAssetToAssetModel(quotingAsset), MinVolume = pair.MinVolume, MinInvertedVolume = pair.MinInvertedVolume }, Volume = Math.Abs(request.Volume), OrderAction = request.OrderAction == Models.Orders.OrderAction.Buy ? Lykke.Service.Operations.Contracts.OrderAction.Buy : Lykke.Service.Operations.Contracts.OrderAction.Sell, Client = await GetClientModel(), GlobalSettings = GetGlobalSettings() }; try { await _operationsClient.PlaceMarketOrder(id, command); } catch (HttpOperationException e) { if (e.Response.StatusCode == HttpStatusCode.BadRequest) { return(BadRequest(JObject.Parse(e.Response.Content))); } throw; } return(Created(Url.Action("Get", "Operations", new { id }), id)); }
private async Task CheckApi(string key, IReportsApi reportsApi) { if (_lastTime == default(DateTime)) { _lastTime = DateTime.UtcNow; return; } var fromDate = _lastTime; var toDate = DateTime.UtcNow; Log.Info($"Check api Start. summary. Api: {key}. LastTime: {_lastTime:yyyy-MM-dd HH:mm:ss}"); var sb = new StringBuilder(); sb.AppendLine($"=== {fromDate:yyyy/MM/dd HH:mm:ss} ==="); sb.AppendLine($"=== {toDate:yyyy/MM/dd HH:mm:ss} ==="); sb.Append(Environment.NewLine); sb.AppendLine("Liquidity Engine Summary Statistics"); sb.Append(Environment.NewLine); var countTrade = 0; try { var report = (await reportsApi.GetPositionsReportAsync(fromDate, toDate, int.MaxValue)) .Where(x => x.IsClosed) .Where(x => x.CloseDate > fromDate) .Where(x => x.CloseDate <= toDate) .ToList(); if (report.Count == 0) { return; } var grouped = report.GroupBy(x => x.AssetPairId).OrderBy(x => x.Key); var result = new List <string>(); foreach (var assetPairTrades in grouped) { var assetPairId = assetPairTrades.Key; var count = assetPairTrades.Count(); var buyVolume = assetPairTrades.Where(x => x.Type == PositionType.Long).Sum(x => x.Volume); var sellVolume = assetPairTrades.Where(x => x.Type == PositionType.Short).Sum(x => x.Volume); var isPnLInUsd = assetPairTrades.All(x => x.PnLUsd.HasValue); var pnLInUsd = isPnLInUsd ? assetPairTrades.Sum(x => x.PnLUsd.Value) : (decimal?)null; var pnL = assetPairTrades.Sum(x => x.PnL.Value); var assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(assetPairId); var quoteAssetId = assetPair?.QuotingAssetId; var asset = await _assetsServiceWithCache.TryGetAssetAsync(quoteAssetId); var quoteAssetDisplayId = quoteAssetId == null ? null : asset.Id; var quoteAssetStr = string.IsNullOrWhiteSpace(quoteAssetDisplayId) ? "[quote asset]" : quoteAssetDisplayId; var pnLStr = pnLInUsd.HasValue ? $"{Math.Round(pnLInUsd.Value, 4)}$" : $"{Math.Round(pnL, 4)} {quoteAssetStr}"; var assetPairMessage = $"{assetPairId}; " + $"PL={pnLStr}; " + $"Count: {count}; " + $"Sell: {Math.Round(sellVolume, 6)}; " + $"Buy: {Math.Round(buyVolume, 6)}; "; result.Add(assetPairMessage); } sb.AppendLine(string.Join(Environment.NewLine, result)); _lastTime = DateTime.UtcNow; await TelegramSender.SendTextMessageAsync(PublisherSettings.ChatId, sb.ToString()); } catch (Exception ex) { Log.Error(ex); } Log.Info($"Check api complete. Found: {countTrade} asset pairs. Api: {key}. LastTime: {_lastTime:yyyy-MM-dd HH:mm:ss}"); }
public async Task <Price> CreateAsync(string assetPair, OrderType type, decimal quotingVolume, DateTime validFrom) { var instrument = await _instrumentsAccessService.GetByAssetPairIdAsync(assetPair); if (instrument == null || instrument.State != InstrumentState.Active) { throw new FailedOperationException($"No active instrument {assetPair} was found."); } var orderBook = _orderBookService.GetByAssetPairId(instrument.Exchange, assetPair); if (type == OrderType.None) { throw new FailedOperationException($"Invalid order type {OrderType.None}"); } var pair = await _assetsService.TryGetAssetPairAsync(assetPair); if (pair == null) { throw new FailedOperationException($"Pair {assetPair} not found."); } var baseAsset = await _assetsService.TryGetAssetAsync(pair.BaseAssetId); var quotingAsset = await _assetsService.TryGetAssetAsync(pair.QuotingAssetId); if (baseAsset == null) { throw new FailedOperationException($"Base asset {pair.BaseAssetId} not found."); } if (quotingAsset == null) { throw new FailedOperationException($"Quoting asset {pair.QuotingAssetId} not found."); } var orders = type == OrderType.Buy ? orderBook.SellLimitOrders.OrderBy(x => x.Price).ToList() : orderBook.BuyLimitOrders.OrderByDescending(x => x.Price).ToList(); if (!orders.Any() || orders.Sum(x => x.Volume * x.Price) < quotingVolume) { throw new FailedOperationException("Not enough liquidity."); } var defaultSettings = await _settingsService.GetDefaultSettingsAsync(); var markupCoefficient = instrument.Markup ?? defaultSettings.Markup; var midPrice = orderBook.GetMidPrice(); if (!midPrice.HasValue) { throw new FailedOperationException("Not enough liquidity."); } var markupAbsolute = midPrice.Value * markupCoefficient; var volumes = new List <decimal>(); var remainingVolume = quotingVolume; for (var i = 0; i < orders.Count; i++) { var orderVolumeInUsd = orders[i].Volume * orders[i].Price; if (remainingVolume == 0m || remainingVolume <= (decimal)pair.MinVolume) { break; } if (orderVolumeInUsd <= remainingVolume) { volumes.Add(orders[i].Volume); remainingVolume -= orderVolumeInUsd; } else { volumes.Add(remainingVolume / orders[i].Price); break; } } var volumeBase = Math.Round(volumes.Sum(), baseAsset.Accuracy); var originalPrice = (quotingVolume / volumes.Sum()).TruncateDecimalPlaces(pair.Accuracy, type == OrderType.Buy); var overallPrice = (originalPrice + markupAbsolute * (type == OrderType.Buy ? 1m : -1m)) .TruncateDecimalPlaces(pair.Accuracy, type == OrderType.Buy); var validTo = validFrom + (instrument.PriceLifetime ?? defaultSettings.PriceLifetime); var allowedOverlap = instrument.OverlapTime ?? defaultSettings.OverlapTime; var price = new Price { Id = Guid.NewGuid().ToString(), Value = overallPrice, AssetPair = assetPair, Exchange = instrument.Exchange, BaseVolume = volumeBase, QuotingVolume = quotingVolume, Markup = markupCoefficient, OriginalPrice = originalPrice, Type = type, ValidFrom = validFrom, ValidTo = validTo, AllowedOverlap = allowedOverlap }; await _pricesRepository.InsertAsync(price); await UpdateIfLatestAsync(price); return(price); }
private async Task CheckApi(string key, ILiquidityEngineClient client) { var fromDate = DateTime.UtcNow.Date.AddDays(-1); var toDate = DateTime.UtcNow.Date.AddDays(+1); if (!_lastClose.TryGetValue(key, out var lastClose)) { lastClose = DateTime.UtcNow; _lastClose[key] = lastClose; } Log.Info($"Check api started. Trades. Api: {key}. LastTime: {lastClose:yyyy-MM-dd HH:mm:ss}"); var countTrade = 0; try { var data = await client.Reports.GetPositionsReportAsync(fromDate, toDate, 5000); var markups = await client.InstrumentMarkupsApi.GetAllAsync(); var positions = data.Where(e => e.CloseDate > lastClose).ToList(); foreach (var position in positions.OrderBy(e => e.CloseDate)) { var assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(position.AssetPairId); var quoteAssetId = assetPair?.QuotingAssetId; var asset = await _assetsServiceWithCache.TryGetAssetAsync(quoteAssetId); var quoteAssetDisplayId = quoteAssetId == null ? null : asset.Id; var quoteAssetStr = string.IsNullOrWhiteSpace(quoteAssetDisplayId) ? "[quote asset]" : quoteAssetDisplayId; var markupModel = markups.Single(x => x.AssetPairId == position.AssetPairId); var markupValue = position.Type == PositionType.Short ? markupModel.TotalAskMarkup : markupModel.TotalBidMarkup; var markup = markupValue == -1 ? "stopped" : (markupValue * 100).ToString("0.##") + "%"; var pnL = position.PnL ?? 0; var closePrice = position.ClosePrice ?? 0; var pnLStr = position.PnLUsd.HasValue ? $"{Math.Round(position.PnLUsd.Value, 4)}$" : $"{Math.Round(pnL, 4)} {quoteAssetStr}"; var message = $"{position.AssetPairId}; " + $"PL={pnLStr}; " + $"{(position.Type == PositionType.Short ? "Sell" : "Buy")}; " + $"Volume: {Math.Round(position.Volume, 6)}; " + $"OpenPrice: {Math.Round(position.Price, 6)}; " + $"ClosePrice: {Math.Round(closePrice, 6)}; " + $"Close: {position.CloseDate:MM-dd HH:mm:ss}; " + $"Markup: {markup}"; if (positions.Count >= 4800) { message += "; !!!max count of position in day, please add limit!!!"; } await TelegramSender.SendTextMessageAsync(PublisherSettings.ChatId, message); if (position.CloseDate.HasValue) { _lastClose[key] = position.CloseDate.Value; } countTrade++; } } catch (Exception ex) { Log.Error(ex); _lastClose[key] = DateTime.UtcNow; } Log.Info($"Check api completed. Found: {countTrade} trades. Api: {key}. LastTime: {lastClose:yyyy-MM-dd HH:mm:ss}"); }
private async Task <OrderBook> CalculateDirectOrderBookAsync(Instrument instrument, DateTime iterationDateTime) { Quote[] quotes = _b2C2OrderBookService.GetQuotes(instrument.AssetPairId); if (quotes == null || quotes.Length != 2) { _log.WarningWithDetails("No quotes for instrument", instrument.AssetPairId); return(null); } Balance baseAssetBalance = null; Balance quoteAssetBalance = null; TimeSpan timeSinceLastTrade = TimeSpan.Zero; if (instrument.AllowSmartMarkup) { AssetPairLink assetPairLink = await _assetPairLinkService.GetByInternalAssetPairIdAsync(instrument.AssetPairId); if (assetPairLink != null && !assetPairLink.IsEmpty()) { baseAssetBalance = await _balanceService.GetByAssetIdAsync(ExchangeNames.B2C2, assetPairLink.ExternalBaseAssetId); quoteAssetBalance = await _balanceService.GetByAssetIdAsync(ExchangeNames.B2C2, assetPairLink.ExternalQuoteAssetId); timeSinceLastTrade = DateTime.UtcNow - _tradeService.GetLastInternalTradeTime(instrument.AssetPairId); } else { _log.WarningWithDetails("The asset pair link does not configured", new { instrument.AssetPairId }); } } AssetPair assetPair = await _assetsServiceWithCache.TryGetAssetPairAsync(instrument.AssetPairId); Asset baseAsset = await _assetsServiceWithCache.TryGetAssetAsync(assetPair.BaseAssetId); MarketMakerSettings marketMakerSettings = await _marketMakerSettingsService.GetAsync(); decimal globalMarkup = marketMakerSettings.LimitOrderPriceMarkup; decimal noQuotesMarkup = await _noFreshQuotesStopLossService.GetNoFreshQuotesMarkup(assetPair.Id); decimal pnLStopLossMarkup = await _pnLStopLossEngineService.GetTotalMarkupByAssetPairIdAsync(assetPair.Id); decimal fiatEquityStopLossMarkup = await _fiatEquityStopLossService.GetFiatEquityMarkup(assetPair.Id); _log.InfoWithDetails("Arguments for Calculator.CalculateLimitOrders(...).", new { instrument.AssetPairId, quotes, levels = instrument.Levels.ToArray(), baseAmountBalance = baseAssetBalance?.Amount ?? 0, quoteAmountBalance = quoteAssetBalance?.Amount ?? 0, timeSinceLastTradeTotalSeconds = (int)timeSinceLastTrade.TotalSeconds, instrumentHalfLifePeriod = instrument.HalfLifePeriod, instrumentAllowSmartMarkup = instrument.AllowSmartMarkup, marketMakerSettingsLimitOrderPriceMarkup = globalMarkup, pnLStopLossMarkup, fiatEquityStopLossMarkup, noQuotesMarkup, assetPairAccuracy = assetPair.Accuracy, baseAssetAccuracy = baseAsset.Accuracy, instrument }); OrderBookUpdateReport orderBookUpdateReport = null; if (_isOrderBooksUpdateReportEnabled) { orderBookUpdateReport = new OrderBookUpdateReport(iterationDateTime); orderBookUpdateReport.AssetPair = instrument.AssetPairId; orderBookUpdateReport.FirstQuoteAsk = quotes[0].Ask; orderBookUpdateReport.FirstQuoteBid = quotes[0].Bid; orderBookUpdateReport.SecondQuoteAsk = quotes[1].Ask; orderBookUpdateReport.SecondQuoteBid = quotes[1].Bid; orderBookUpdateReport.QuoteDateTime = quotes[0].Time; orderBookUpdateReport.GlobalMarkup = globalMarkup; orderBookUpdateReport.NoFreshQuoteMarkup = noQuotesMarkup; orderBookUpdateReport.PnLStopLossMarkup = pnLStopLossMarkup; orderBookUpdateReport.FiatEquityMarkup = fiatEquityStopLossMarkup; } IReadOnlyCollection <LimitOrder> limitOrders = Calculator.CalculateLimitOrders( quotes[0], quotes[1], instrument.Levels.ToArray(), baseAssetBalance?.Amount ?? 0, quoteAssetBalance?.Amount ?? 0, (int)timeSinceLastTrade.TotalSeconds, instrument.HalfLifePeriod, instrument.AllowSmartMarkup, globalMarkup, pnLStopLossMarkup, fiatEquityStopLossMarkup, noQuotesMarkup, assetPair.Accuracy, baseAsset.Accuracy, orderBookUpdateReport); await ValidateQuoteTimeoutAsync(limitOrders, quotes[0]); await ValidateQuoteTimeoutAsync(limitOrders, quotes[1]); ValidateMinVolume(limitOrders, (decimal)assetPair.MinVolume); await ValidatePriceAsync(limitOrders); await ValidateBalanceAsync(limitOrders, assetPair); WriteInfoLog(instrument.AssetPairId, quotes, timeSinceLastTrade, limitOrders, "Direct limit orders calculated"); if (orderBookUpdateReport != null) { await _orderBooksUpdatesReportPublisher.PublishAsync(orderBookUpdateReport); } return(new OrderBook { AssetPairId = instrument.AssetPairId, Time = DateTime.UtcNow, LimitOrders = limitOrders, IsDirect = true }); }
public async Task <IActionResult> PlaceMarketOrder([FromBody] MarketOrderRequest request) { var clientId = _requestContext.ClientId; var pair = await _assetsServiceWithCache.TryGetAssetPairAsync(request.AssetPairId); if (pair == null) { return(NotFound()); } if (pair.IsDisabled) { return(BadRequest()); } var baseAsset = await _assetsServiceWithCache.TryGetAssetAsync(pair.BaseAssetId); if (baseAsset == null) { return(NotFound()); } var quotingAsset = await _assetsServiceWithCache.TryGetAssetAsync(pair.QuotingAssetId); if (quotingAsset == null) { return(BadRequest()); } if (request.AssetId != baseAsset.Id && request.AssetId != baseAsset.Name && request.AssetId != quotingAsset.Id && request.AssetId != quotingAsset.Name) { return(BadRequest()); } var straight = request.AssetId == baseAsset.Id || request.AssetId == baseAsset.Name; var volume = request.Volume.TruncateDecimalPlaces(straight ? baseAsset.Accuracy : quotingAsset.Accuracy); if (Math.Abs(volume) < double.Epsilon) { return(BadRequest(CreateErrorMessage("Required volume is less than asset accuracy"))); } var order = new MarketOrderModel { Id = Guid.NewGuid().ToString(), AssetPairId = pair.Id, ClientId = clientId, ReservedLimitVolume = null, Straight = straight, Volume = Math.Abs(volume), OrderAction = ToMeOrderAction(request.OrderAction) }; if (_baseSettings.EnableFees) { order.Fees = new[] { await GetMarketOrderFee(clientId, request.AssetPairId, request.AssetId, request.OrderAction) } } ; var response = await _matchingEngineClient.HandleMarketOrderAsync(order); if (response == null) { throw new Exception("ME unavailable"); } if (response.Status != MeStatusCodes.Ok) { return(BadRequest(CreateErrorMessage($"ME responded: {response.Status}"))); } return(Ok(order.Id)); }
public Task <AssetPair> GetAssetPairAsync(string assetPairId) { return(_assetsServiceWithCache.TryGetAssetPairAsync(assetPairId)); }