/// <summary> /// Рассчитать комиссию. /// </summary> /// <param name="message">Сообщение, содержащее информацию по заявке или собственной сделке.</param> /// <returns>Комиссия. Если комиссию рассчитать невозможно, то будет возвращено <see langword="null"/>.</returns> protected override decimal?OnProcessExecution(ExecutionMessage message) { if (message.ExecutionType != ExecutionTypes.Trade) { return(null); } _currentTurnOver += message.GetTradePrice() * message.GetVolume(); if (_currentTurnOver < TurnOver) { return(null); } return((decimal)Value); }
/// <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)); }
private void ProcessMarketOrder(List <ExecutionMessage> retVal, SortedDictionary <decimal, RefPair <List <ExecutionMessage>, QuoteChange> > quotes, ExecutionMessage tradeMessage, Sides orderSide) { // вычисляем объем заявки по рынку, который смог бы пробить текущие котировки. var tradePrice = tradeMessage.GetTradePrice(); // bigOrder - это наша большая рыночная заявка, которая способствовала появлению tradeMessage var bigOrder = CreateMessage(tradeMessage.LocalTime, orderSide, tradePrice, 0, tif: TimeInForce.MatchOrCancel); var sign = orderSide == Sides.Buy ? -1 : 1; var hasQuotes = false; foreach (var pair in quotes) { var quote = pair.Value.Second; if (quote.Price * sign > tradeMessage.TradePrice * sign) { bigOrder.Volume += quote.Volume; } else { if (quote.Price == tradeMessage.TradePrice) { bigOrder.Volume += tradeMessage.Volume; //var diff = tradeMessage.Volume - quote.Volume; //// если объем котиовки был меньше объема сделки //if (diff > 0) // retVal.Add(CreateMessage(tradeMessage.LocalTime, quote.Side, quote.Price, diff)); } else { if ((tradePrice - quote.Price).Abs() == _securityDefinition.PriceStep) { // если на один шаг цены выше/ниже есть котировка, то не выполняем никаких действий // иначе добавляем новый уровень в стакан, чтобы не было большого расхождения цен. hasQuotes = true; } break; } //// если котировки с ценой сделки вообще не было в стакане //else if (quote.Price * sign < tradeMessage.TradePrice * sign) //{ // retVal.Add(CreateMessage(tradeMessage.LocalTime, quote.Side, tradeMessage.Price, tradeMessage.Volume)); //} } } retVal.Add(bigOrder); // если собрали все котировки, то оставляем заявку в стакане по цене сделки if (!hasQuotes) { retVal.Add(CreateMessage(tradeMessage.LocalTime, orderSide.Invert(), tradePrice, tradeMessage.GetVolume())); } }
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> /// Рассчитать прибыльность сделки. Если сделка уже ранее была обработана, то возвращается предыдущая информация. /// </summary> /// <param name="trade">Сделка.</param> /// <returns>Информация о новой сделке.</returns> public PnLInfo Process(ExecutionMessage trade) { if (trade == null) { throw new ArgumentNullException("trade"); } var closedVolume = 0m; var pnl = 0m; var volume = trade.GetVolume(); 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 += TraderHelper.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)); }
/// <summary> /// Добавить новую строчку из лога заявок к стакану. /// </summary> /// <param name="item">Строчка лога заявок.</param> /// <returns>Был ли изменен стакан.</returns> public bool Update(ExecutionMessage item) { if (item == null) { throw new ArgumentNullException("item"); } if (item.ExecutionType != ExecutionTypes.OrderLog) { throw new ArgumentException("item"); } var volume = item.GetVolume(); var changed = false; try { // Очистить стакан в вечерний клиринг if (item.ServerTime.TimeOfDay >= _clearingBeginTime) { // Garic - переделал // Очищаем только в рабочие дни поскольку в субботу/воскресенье допустима отмена заявок if (_lastUpdateTime != null && _lastUpdateTime.Value.TimeOfDay < _clearingBeginTime && _exchange.WorkingTime.IsTradeDate(item.ServerTime.LocalDateTime, true)) { _depth.ServerTime = item.ServerTime; _depth.Bids = Enumerable.Empty <QuoteChange>(); _depth.Asks = Enumerable.Empty <QuoteChange>(); _matchingOrder = null; changed = true; } } _lastUpdateTime = item.ServerTime.LocalDateTime; if (item.IsSystem == false || item.TradePrice != null || item.Price == 0 /* нулевая цена может появится при поставке опционов */) { return(changed); } if (item.IsOrderLogRegistered()) { changed = TryApplyTrades(null); if ( (item.Side == Sides.Buy && (_depth.Asks.IsEmpty() || item.Price < _depth.Asks.First().Price)) || (item.Side == Sides.Sell && (_depth.Bids.IsEmpty() || item.Price > _depth.Bids.First().Price)) ) { if (item.TimeInForce == TimeInForce.PutInQueue) { var quotes = (item.Side == Sides.Buy ? _bids : _asks); var quote = quotes.TryGetValue(item.Price); if (quote == null) { quote = new QuoteChange { Side = item.Side, Price = item.Price, Volume = volume, }; quotes.Add(item.Price, quote); if (item.Side == Sides.Buy) { _depth.Bids = GetArray(quotes); } else { _depth.Asks = GetArray(quotes); } } else { quote.Volume += volume; } changed = true; } } else { _matchingOrder = (ExecutionMessage)item.Clone(); // mika // из-за того, что могут быть кросс-сделки, матчинг только по заявкам невозможен // (сначала идет регистрация вглубь стакана, затем отмена по причине кросс-сделки) // http://forum.rts.ru/viewtopic.asp?t=24197 // } } else if (item.IsOrderLogCanceled()) { var isSame = _matchingOrder != null && _matchingOrder.OrderId == item.OrderId; changed = TryApplyTrades(item); if (!isSame && item.TimeInForce == TimeInForce.PutInQueue) { // http://forum.rts.ru/viewtopic.asp?t=24197 if (item.GetOrderLogCancelReason() != OrderLogCancelReasons.CrossTrade) { var quotes = (item.Side == Sides.Buy ? _bids : _asks); var quote = quotes.TryGetValue(item.Price); if (quote != null) { quote.Volume -= volume; if (quote.Volume <= 0) { quotes.Remove(item.Price); if (item.Side == Sides.Buy) { _depth.Bids = GetArray(quotes); } else { _depth.Asks = GetArray(quotes); } } } } changed = true; } } else { throw new ArgumentException(LocalizedStrings.Str943Params.Put(item), "item"); // для одной сделки соответствуют две строчки в ОЛ //_trades[item.Trade.Id] = item; } } finally { if (changed) { _depth.ServerTime = item.ServerTime; } } return(changed); }
private bool TryApplyTrades(ExecutionMessage item) { if (_matchingOrder == null) { return(false); } try { var volume = _matchingOrder.GetVolume(); if (item != null && _matchingOrder.OrderId == item.OrderId) { volume -= item.GetVolume(); } // если заявка была вся отменена. например, по причине http://forum.rts.ru/viewtopic.asp?t=24197 if (volume == 0) { return(false); } var removingQuotes = new List <Tuple <QuoteChange, decimal> >(); foreach (var quote in (_matchingOrder.Side == Sides.Buy ? _depth.Asks : _depth.Bids)) { if ((_matchingOrder.Side == Sides.Buy && _matchingOrder.Price < quote.Price) || (_matchingOrder.Side == Sides.Sell && _matchingOrder.Price > quote.Price)) { break; } if (volume >= quote.Volume) { removingQuotes.Add(Tuple.Create(quote, quote.Volume)); volume -= quote.Volume; if (volume == 0) { break; } } else { removingQuotes.Add(Tuple.Create(quote, volume)); volume = 0; break; } } // в текущей момент Плаза не транслирует признак MatchOrCancel через ОЛ, поэтому сделано на будущее if (_matchingOrder.TimeInForce == TimeInForce.MatchOrCancel && volume > 0) { return(false); } foreach (var removingQuote in removingQuotes) { var quotes = (_matchingOrder.Side.Invert() == Sides.Buy ? _bids : _asks); var quote = quotes.TryGetValue(removingQuote.Item1.Price); if (quote != null) { quote.Volume -= removingQuote.Item2; if (quote.Volume <= 0) { quotes.Remove(removingQuote.Item1.Price); } } } if (volume > 0) { if (_matchingOrder.TimeInForce == TimeInForce.PutInQueue) { var quote = new QuoteChange { Side = _matchingOrder.Side, Price = _matchingOrder.Price, Volume = volume, }; (quote.Side == Sides.Buy ? _bids : _asks).Add(quote.Price, quote); } } _depth.Bids = GetArray(_bids); _depth.Asks = GetArray(_asks); return(true); } finally { _matchingOrder = null; } }