/// <summary> /// Преобразовать тиковую сделку. /// </summary> /// <param name="message">Тиковая сделка.</param> /// <returns>Поток <see cref="ExecutionMessage"/>.</returns> public IEnumerable <ExecutionMessage> ToExecutionLog(ExecutionMessage message) { if (message == null) { throw new ArgumentNullException("message"); } if (!_stepsUpdated) { _securityDefinition.PriceStep = message.GetTradePrice().GetDecimalInfo().EffectiveScale.GetPriceStep(); _securityDefinition.VolumeStep = message.GetVolume().GetDecimalInfo().EffectiveScale.GetPriceStep(); _stepsUpdated = true; } //if (message.DataType != ExecutionDataTypes.Trade) // throw new ArgumentOutOfRangeException("Тип данных не может быть {0}.".Put(message.DataType), "message"); _lastTradeDate = message.LocalTime.Date; return(ProcessExecution(message)); }
/// <summary> /// To convert the tick trade. /// </summary> /// <param name="tick">Tick trade.</param> /// <returns>Stream <see cref="ExecutionMessage"/>.</returns> public IEnumerable <ExecutionMessage> ToExecutionLog(ExecutionMessage tick) { if (tick == null) { throw new ArgumentNullException(nameof(tick)); } if (!_priceStepUpdated) { _securityDefinition.PriceStep = tick.GetTradePrice().GetDecimalInfo().EffectiveScale.GetPriceStep(); _priceStepUpdated = true; } if (!_volumeStepUpdated) { var tickVolume = tick.TradeVolume; if (tickVolume != null) { _securityDefinition.VolumeStep = tickVolume.Value.GetDecimalInfo().EffectiveScale.GetPriceStep(); _volumeStepUpdated = true; } } //if (tick.ExecutionType != ExecutionTypes.Tick) // throw new ArgumentOutOfRangeException(nameof(tick), tick.ExecutionType, LocalizedStrings.Str1655); //_lastTradeDate = message.LocalTime.Date; var retVal = new List <ExecutionMessage>(); var bestBid = _bids.FirstOrDefault(); var bestAsk = _asks.FirstOrDefault(); var tradePrice = tick.GetTradePrice(); var volume = tick.TradeVolume ?? 1; var time = tick.LocalTime; if (bestBid.Value != null && tradePrice <= bestBid.Key) { // тик попал в биды, значит была крупная заявка по рынку на продажу, // которая возможна исполнила наши заявки ProcessMarketOrder(retVal, _bids, tick.ServerTime, tick.LocalTime, Sides.Sell, tradePrice, volume); // подтягиваем противоположные котировки и снимаем лишние заявки TryCreateOppositeOrder(retVal, _asks, time, tick.ServerTime, tradePrice, volume, Sides.Buy); } else if (bestAsk.Value != null && tradePrice >= bestAsk.Key) { // тик попал в аски, значит была крупная заявка по рынку на покупку, // которая возможна исполнила наши заявки ProcessMarketOrder(retVal, _asks, tick.ServerTime, tick.LocalTime, Sides.Buy, tradePrice, volume); TryCreateOppositeOrder(retVal, _bids, time, tick.ServerTime, tradePrice, volume, Sides.Sell); } else if (bestBid.Value != null && bestAsk.Value != null && bestBid.Key < tradePrice && tradePrice < bestAsk.Key) { // тик попал в спред, значит в спреде до сделки была заявка. // создаем две лимитки с разных сторон, но одинаковой ценой. // если в эмуляторе есть наша заявка на этом уровне, то она исполниться. // если нет, то эмулятор взаимно исполнит эти заявки друг об друга var originSide = GetOrderSide(tick); retVal.Add(CreateMessage(time, tick.ServerTime, originSide, tradePrice, volume + (_securityDefinition.VolumeStep ?? 1 * _settings.VolumeMultiplier), tif: TimeInForce.MatchOrCancel)); var spreadStep = _settings.SpreadSize * GetPriceStep(); // try to fill depth gaps var newBestPrice = tradePrice + spreadStep; var depth = _settings.MaxDepth; while (--depth > 0) { var diff = bestAsk.Key - newBestPrice; if (diff > 0) { retVal.Add(CreateMessage(time, tick.ServerTime, Sides.Sell, newBestPrice, 0)); newBestPrice += spreadStep * _priceRandom.Next(1, _settings.SpreadSize); } else { break; } } newBestPrice = tradePrice - spreadStep; depth = _settings.MaxDepth; while (--depth > 0) { var diff = newBestPrice - bestBid.Key; if (diff > 0) { retVal.Add(CreateMessage(time, tick.ServerTime, Sides.Buy, newBestPrice, 0)); newBestPrice -= spreadStep * _priceRandom.Next(1, _settings.SpreadSize); } else { break; } } retVal.Add(CreateMessage(time, tick.ServerTime, originSide.Invert(), tradePrice, volume, tif: TimeInForce.MatchOrCancel)); } else { // если у нас стакан был полу пустой, то тик формирует некий ценовой уровень в стакана, // так как прошедщая заявка должна была обо что-то удариться. допускаем, что после // прохождения сделки на этом ценовом уровне остался объем равный тиковой сделки var hasOpposite = true; Sides originSide; // определяем направление псевдо-ранее существовавшей заявки, из которой получился тик if (bestBid.Value != null) { originSide = Sides.Sell; } else if (bestAsk.Value != null) { originSide = Sides.Buy; } else { originSide = GetOrderSide(tick); hasOpposite = false; } retVal.Add(CreateMessage(time, tick.ServerTime, originSide, tradePrice, volume)); // если стакан был полностью пустой, то формируем сразу уровень с противоположной стороны if (!hasOpposite) { var oppositePrice = tradePrice + _settings.SpreadSize * GetPriceStep() * (originSide == Sides.Buy ? 1 : -1); if (oppositePrice > 0) { retVal.Add(CreateMessage(time, tick.ServerTime, originSide.Invert(), oppositePrice, volume)); } } } if (!HasDepth(time)) { // если стакан слишком разросся, то удаляем его хвосты (не удаляя пользовательские заявки) CancelWorstQuote(retVal, time, tick.ServerTime, Sides.Buy, _bids); CancelWorstQuote(retVal, time, tick.ServerTime, Sides.Sell, _asks); } _prevTickPrice = tradePrice; return(retVal); }
private IEnumerable <ExecutionMessage> ProcessExecution(ExecutionMessage message) { var retVal = new List <ExecutionMessage>(); var bestBid = _bids.FirstOrDefault(); var bestAsk = _asks.FirstOrDefault(); var tradePrice = message.GetTradePrice(); var volume = message.GetVolume(); if (bestBid.Value != null && tradePrice <= bestBid.Key) { // тик попал в биды, значит была крупная заявка по рынку на продажу, // которая возможна исполнила наши заявки ProcessMarketOrder(retVal, _bids, message, Sides.Sell); // подтягиваем противоположные котировки и снимаем лишние заявки TryCreateOppositeOrder(retVal, _asks, message.LocalTime, tradePrice, volume, Sides.Buy); CancelWorstQuotes(retVal, message.LocalTime); } else if (bestAsk.Value != null && tradePrice >= bestAsk.Key) { // тик попал в аски, значит была крупная заявка по рынку на покупку, // которая возможна исполнила наши заявки ProcessMarketOrder(retVal, _asks, message, Sides.Buy); TryCreateOppositeOrder(retVal, _bids, message.LocalTime, tradePrice, volume, Sides.Sell); CancelWorstQuotes(retVal, message.LocalTime); } else if (bestBid.Value != null && bestAsk.Value != null && bestBid.Key < tradePrice && tradePrice < bestAsk.Key) { // тик попал в спред, значит в спреде до сделки была заявка. // создаем две лимитки с разных сторон, но одинаковой ценой. // если в эмуляторе есть наша заявка на этом уровне, то она исполниться. // если нет, то эмулятор взаимно исполнит эти заявки друг об друга var originSide = GetOrderSide(message); retVal.Add(CreateMessage(message.LocalTime, originSide, tradePrice, volume + (_securityDefinition.VolumeStep ?? 1 * _settings.VolumeMultiplier), tif: TimeInForce.MatchOrCancel)); var spreadStep = _settings.SpreadSize * GetPriceStep(); // try to fill depth gaps var newBestPrice = tradePrice + spreadStep; while (true) { var diff = bestAsk.Key - newBestPrice; if (diff > 0) { retVal.Add(CreateMessage(message.LocalTime, Sides.Sell, newBestPrice, 0)); newBestPrice += spreadStep * _priceRandom.Next(1, _settings.SpreadSize); } else { break; } } newBestPrice = tradePrice - spreadStep; while (true) { var diff = newBestPrice - bestBid.Key; if (diff > 0) { retVal.Add(CreateMessage(message.LocalTime, Sides.Buy, newBestPrice, 0)); newBestPrice -= spreadStep * _priceRandom.Next(1, _settings.SpreadSize); } else { break; } } retVal.Add(CreateMessage(message.LocalTime, originSide.Invert(), tradePrice, volume, tif: TimeInForce.MatchOrCancel)); CancelWorstQuotes(retVal, message.LocalTime); } else { // если у нас стакан был полу пустой, то тик формирует некий ценовой уровень в стакана, // так как прошедщая заявка должна была обо что-то удариться. допускаем, что после // прохождения сделки на этом ценовом уровне остался объем равный тиковой сделки var hasOpposite = true; Sides originSide; // определяем направление псевдо-ранее существовавшей заявки, из которой получился тик if (bestBid.Value != null) { originSide = Sides.Sell; } else if (bestAsk.Value != null) { originSide = Sides.Buy; } else { originSide = GetOrderSide(message); hasOpposite = false; } retVal.Add(CreateMessage(message.LocalTime, originSide, tradePrice, volume)); // если стакан был полностью пустой, то формируем сразу уровень с противоположной стороны if (!hasOpposite) { var oppositePrice = tradePrice + _settings.SpreadSize * GetPriceStep() * (originSide == Sides.Buy ? 1 : -1); if (oppositePrice > 0) { retVal.Add(CreateMessage(message.LocalTime, originSide.Invert(), oppositePrice, volume)); } } } _prevTickPrice = tradePrice; return(retVal); }
/// <summary> /// To calculate trade profitability. If the trade was already processed earlier, previous information returns. /// </summary> /// <param name="trade">Trade.</param> /// <returns>Information on new trade.</returns> public PnLInfo Process(ExecutionMessage trade) { if (trade == null) { throw new ArgumentNullException(nameof(trade)); } var closedVolume = 0m; var pnl = 0m; var volume = trade.SafeGetVolume(); var price = trade.GetTradePrice(); _unrealizedPnL = null; lock (_openedTrades.SyncRoot) { if (_openedTrades.Count > 0) { var currTrade = _openedTrades.Peek(); if (_openedPosSide != trade.Side) { while (volume > 0) { if (currTrade == null) { currTrade = _openedTrades.Peek(); } var diff = currTrade.Second.Min(volume); closedVolume += diff; pnl += GetPnL(currTrade.First, diff, _openedPosSide, price); volume -= diff; currTrade.Second -= diff; if (currTrade.Second != 0) { continue; } currTrade = null; _openedTrades.Pop(); if (_openedTrades.Count == 0) { break; } } } } if (volume > 0) { _openedPosSide = trade.Side; _openedTrades.Push(RefTuple.Create(price, volume)); } RealizedPnL += _multiplier * pnl; } return(new PnLInfo(trade, closedVolume, pnl)); }