//TraderHelper.cs //Copyright (c) 2013 StockSharp LLC, all rights reserved. //This code module is part of StockSharp library. //This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3. //See the file License.txt for the license details. //More info on: http://stocksharp.com using System.Linq.Expressions; namespace StockSharp.Algo { using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.ServiceModel; using System.Threading; using Ecng.Net; using Ecng.Common; using Ecng.ComponentModel; using Ecng.Collections; using StockSharp.Algo.PnL; using StockSharp.Algo.Positions; using StockSharp.Algo.Storages; using StockSharp.Algo.Strategies; using StockSharp.Algo.Testing; using StockSharp.BusinessEntities; using StockSharp.Logging; using Wintellect.PowerCollections; /// /// Правила округления цены. /// public enum ShrinkRules { /// /// Автоматически определять, к меньшему или большему значению округлять. /// Auto, /// /// Округлять к меньшему значению. /// Less, /// /// Округлять к большему значению. /// More, } /// /// Вспомогательный класс для предоставления различной алгоритмической функциональности. /// public static class TraderHelper { /// /// Получить стакан котировок для заданного инструмента. /// /// Инструмент, по которому необходимо получить стакан. /// Cтакан. public static MarketDepth GetMarketDepth(this Security security) { return security.CheckTrader().GetMarketDepth(security); } /// /// Получить отфильтрованный стакан инструмента от всех активных заявок. /// /// Инструмент, по которому необходимо получить стакан. /// Заявки, которые необходимо игнорировать. /// Отфильтрованный стакан. public static IEnumerable GetFilteredQuotes(this Security security, IEnumerable orders) { return security.GetMarketDepth().SyncGet(c => c.GetFilteredQuotes(orders)); } /// /// Получить отфильтрованный стакан от всех активных заявок. /// /// Исходный стакан, который необходимо отфильтровать. /// Заявки, которые необходимо игнорировать. /// Отфильтрованный стакан. public static IEnumerable GetFilteredQuotes(this IEnumerable quotes, IEnumerable orders) { if (quotes == null) throw new ArgumentNullException("quotes"); if (orders == null) throw new ArgumentNullException("orders"); var firstQuote = quotes.FirstOrDefault(); if (firstQuote == null) return new Quote[0]; var trader = firstQuote.Security.Trader; if (trader == null) throw new ArgumentException("Котировка не имеет информацию о шлюзе."); // pyh GetFilteredQuotes для EmulationTrader пропускает процедуру фильтрации т.к. наш симуляционный ордер в стакан не попадает if (trader is IEmulationTrader) return quotes; return quotes.GetFilteredQuotes(trader.Orders.Where(o => o.State == OrderStates.Active || o.State == OrderStates.None), orders); } /// /// Отфильтровать стакан от собственных заявок. /// /// Исходный стакан, который необходимо отфильтровать. /// Активные заявки по данному инструменту. /// Заявки, которые необходимо игнорировать. /// Отфильтрованный стакан. public static IEnumerable GetFilteredQuotes(this IEnumerable quotes, IEnumerable ownOrders, IEnumerable orders) { if (quotes == null) throw new ArgumentNullException("quotes"); if (ownOrders == null) throw new ArgumentNullException("ownOrders"); if (orders == null) throw new ArgumentNullException("orders"); var dict = new MultiDictionary, Order>(false); foreach (var order in ownOrders) { dict.Add(new Tuple(order.Direction, order.Price), order); } var retVal = new List(quotes.Select(q => q.Clone())); foreach (var quote in retVal.ToArray()) { var o = dict.TryGetValue(new Tuple(quote.OrderDirection, quote.Price)); if (o != null) { foreach (var order in o) { if (!orders.Contains(order)) quote.Volume -= order.Balance; } if (quote.Volume <= 0) retVal.Remove(quote); } } return retVal; } /// /// Получить рыночную цену для инструмента. /// /// Инструмент, по которому вычисляется рыночная цена. /// Направление заявки. /// Рыночная цена. Если по инструменту возможна регистрация заявок типа , будет возвращено 0. public static decimal GetMarketPrice(this Security security, OrderDirections direction) { var board = security.CheckExchangeBoard(); if (board.IsSupportMarketOrders) return 0; if (direction == OrderDirections.Buy && security.MaxPrice != 0) return security.MaxPrice; else if (direction == OrderDirections.Sell && security.MinPrice != 0) return security.MinPrice; else throw new ArgumentException("У инструмента {0} отсутствует информация о планках.".Put(security), "security"); } /// /// Высчитать текущую цену по инструменту в зависимости от направления заявки. /// /// Инструмент, по которому вычисляется текущая цена. /// Направление заявки. /// Тип рыночной цены. /// Заявки, которые необходимо игнорировать. /// Текущая цена. Если информации в стакане недостаточно, будет возвращено 0. public static Unit GetCurrentPrice(this Security security, OrderDirections? direction = null, MarketPriceTypes priceType = MarketPriceTypes.Following, IEnumerable orders = null) { var depth = security.GetMarketDepth(); decimal? currentPrice = null; if (direction != null) { var result = depth.GetCurrentPrice((OrderDirections)direction, priceType, orders); if (result != 0) return result; if (direction == OrderDirections.Buy && security.BestAsk != null) currentPrice = security.BestAsk.Price; else if (direction == OrderDirections.Sell && security.BestBid != null) currentPrice = security.BestBid.Price; } if (currentPrice == null && security.LastTrade != null) currentPrice = security.LastTrade.Price; if (currentPrice == null) currentPrice = 0; return new Unit((decimal)currentPrice) { Security = security }; } /// /// Высчитать текущую цену по стакану в зависимости от направления заявки. /// /// Для корректной работы метода необходимо запустить экспорт стакана. /// Стакан, по которому нужно высчитать текущую цену. /// Направление заявки. Если это покупка, то будет использоваться /// значение , иначе . /// Тип текущей цены. /// Заявки, которые необходимо игнорировать. /// Текущая цена. Если информации в стакане недостаточно, будет возвращено 0. public static Unit GetCurrentPrice(this MarketDepth depth, OrderDirections direction, MarketPriceTypes priceType = MarketPriceTypes.Following, IEnumerable orders = null) { if (depth == null) throw new ArgumentNullException("depth"); if (orders != null) { var quotes = depth.GetFilteredQuotes(orders); depth = new MarketDepth(depth.Security).Update(quotes, depth.LastChangeTime); } return depth.BestPair.GetCurrentPrice(direction, priceType); } /// /// Высчитать текущую цену по лучшей паре котировок в зависимости от направления заявки. /// /// Для корректной работы метода необходимо запустить экспорт стакана. /// Лучшая пара котировок, по которой вычисляется текущая цена. /// Направление заявки. Если это покупка, то будет использоваться /// значение , иначе . /// Тип текущей цены. /// Текущая цена. Если информации в стакане недостаточно, будет возвращено 0. public static Unit GetCurrentPrice(this MarketDepthPair bestPair, OrderDirections direction, MarketPriceTypes priceType = MarketPriceTypes.Following) { if (bestPair == null) throw new ArgumentNullException("bestPair"); Quote quote; decimal currentPrice; switch (priceType) { case MarketPriceTypes.Opposite: quote = (direction == OrderDirections.Buy ? bestPair.Ask : bestPair.Bid); currentPrice = quote == null ? 0 : quote.Price; break; case MarketPriceTypes.Following: quote = (direction == OrderDirections.Buy ? bestPair.Bid : bestPair.Ask); currentPrice = quote == null ? 0 : quote.Price; break; case MarketPriceTypes.Middle: if (bestPair.Bid == null || bestPair.Ask == null) currentPrice = 0; else currentPrice = bestPair.Bid.Price + bestPair.SpreadPrice / 2; break; default: throw new ArgumentOutOfRangeException("priceType"); } return new Unit(currentPrice) { Security = bestPair.Security }; } /// /// Применить для цены сдвиг в зависимости от направления . /// /// Цена. /// Направление заявки, котороые использует в качестве направления для сдвига (для покупки сдвиг прибавляется, для продажи - вычитается). /// Сдвиг цены. /// Новая цена. public static decimal ApplyOffset(this Unit price, OrderDirections direction, Unit offset) { if (price == null) throw new ArgumentNullException("price"); return price.Security.ShrinkPrice((decimal)(direction == OrderDirections.Buy ? price + offset : price - offset)); } /// /// Обрезать цену для заявки, чтобы она стала кратной минимальному шагу, а так же ограничить количество знаков после запятой. /// /// Заявка, для которой будет обрезана цена . /// Правило округления цены. public static void ShrinkPrice(this Order order, ShrinkRules rule = ShrinkRules.Auto) { if (order == null) throw new ArgumentNullException("order"); order.Price = order.Security.ShrinkPrice(order.Price, rule); } /// /// Обрезать цену, чтобы она стала кратной минимальному шагу, а так же ограничить количество знаков после запятой. /// /// Инструмент, из которого берется значения и . /// Цена, которую нужно сделать кратной. /// Правило округления цены. /// Кратная цена. public static decimal ShrinkPrice(this Security security, decimal price, ShrinkRules rule = ShrinkRules.Auto) { security.CheckMinStepSize(); return price.Round(security.MinStepSize, security.Decimals, rule == ShrinkRules.Auto ? (MidpointRounding?)null : (rule == ShrinkRules.Less ? MidpointRounding.AwayFromZero : MidpointRounding.ToEven)).RemoveTrailingZeros(); } /// /// Получить позицию по Моей сделке. /// /// Моя сделка, по которой рассчитывается позиция. При покупке объем сделки /// берется с положительным знаком, при продаже - с отрицательным. /// Позиция. public static decimal GetPosition(this MyTrade trade) { if (trade == null) throw new ArgumentNullException("trade"); return trade.Order.Direction == OrderDirections.Buy ? trade.Trade.Volume : -trade.Trade.Volume; } /// /// Получить позицию по заявке. /// /// Заявка, по которой рассчитывается позиция. При покупке позиция берется с положительным знаком, при продаже - с отрицательным. /// Позиция. public static decimal GetPosition(this Order order) { if (order == null) throw new ArgumentNullException("order"); var volume = order.GetMatchedVolume(); return order.Direction == OrderDirections.Buy ? volume : -volume; } /// /// Получить позицию по портфелю. /// /// Портфель, для которого необходимо получить позицию. /// Позиция по портфелю. public static decimal GetPosition(this Portfolio portfolio) { if (portfolio == null) throw new ArgumentNullException("portfolio"); if (portfolio.Trader == null) throw new ArgumentException("Портфель не имеет информацию о шлюзе."); return portfolio.Trader.Positions.Filter(portfolio).Sum(p => p.CurrentValue); } /// /// Получить позицию по Моим сделкам. /// /// Мои сделки, по которым рассчитывается позиция через метод . /// Позиция. public static decimal GetPosition(this IEnumerable trades) { return trades.Sum(t => t.GetPosition()); } /// /// Получить объем заявки, сопоставимый с размером позиции. /// /// Позиция по инструменту. /// Объем заявки. public static decimal GetOrderVolume(this Position position) { if (position == null) throw new ArgumentNullException("position"); return (position.CurrentValue / position.Security.MinLotSize).Abs(); } /// /// Сгруппировать заявки по инструменту и портфелю. /// /// Рекомендуется использовать для уменьшения транзакционных издержек. /// Исходные заявки. /// Сгруппированные заявки. public static IEnumerable Join(this IEnumerable orders) { if (orders == null) throw new ArgumentNullException("orders"); return orders.GroupBy(o => new Tuple(o.Security, o.Portfolio)).Select(g => { Order firstOrder = null; foreach (var order in g) { if (firstOrder == null) { firstOrder = order; } else { var sameDir = firstOrder.Direction == order.Direction; firstOrder.Volume += (sameDir ? 1 : -1) * order.Volume; if (firstOrder.Volume < 0) { firstOrder.Direction = firstOrder.Direction.Invert(); firstOrder.Volume = firstOrder.Volume.Abs(); } firstOrder.Price = sameDir ? firstOrder.Price.GetMiddle(order.Price) : order.Price; } } if (firstOrder == null) throw new InvalidOperationException("Заявки отсутствуют."); if (firstOrder.Volume == 0) return null; firstOrder.ShrinkPrice(); return firstOrder; }) .Where(o => o != null); } /// /// Рассчитать прибыль-убыток на основе сделок. /// /// Сделки, по которым необходимо рассчитывать прибыль-убыток. /// Прибыль-убыток. public static decimal GetPnL(this IEnumerable trades) { return trades.GroupBy(t => t.Trade.Security).Sum(g => { var queue = new PnLQueue(g.Key); g.OrderBy(t => t.Trade.Time).ForEach(t => queue.Process(t, false)); return queue.RealizedPnL + queue.UnrealizedPnL; }); } /// /// Рассчитать прибыль-убыток для сделки. /// /// Сделка, для которой необходимо рассчитывать прибыль-убыток. /// Прибыль-убыток. public static decimal GetPnL(this MyTrade trade) { if (trade == null) throw new ArgumentNullException("trade"); return GetPnL(trade.Trade.Price, trade.Trade.Volume, trade.Order.Direction, (decimal)trade.Order.Security.GetCurrentPrice(trade.Order.Direction.Invert())); } internal static decimal GetPnL(decimal price, decimal volume, OrderDirections direction, decimal marketPrice) { return (price - marketPrice) * volume * (direction == OrderDirections.Sell ? 1 : -1); } /// /// Рассчитать прибыль-убыток на основе портфеля. /// /// Портфель, для которого необходимо расcчитать прибыль-убыток. /// Прибыль-убыток. public static decimal GetPnL(this Portfolio portfolio) { if (portfolio == null) throw new ArgumentNullException("portfolio"); return portfolio.CurrentValue - portfolio.BeginValue; } /// /// Рассчитать стоимость позиции. /// /// Позиция. /// Стоимость позиции. public static decimal GetPrice(this Position position) { if (position == null) throw new ArgumentNullException("position"); var security = position.Security; return (decimal)security.GetCurrentPrice(position.GetDirection()) * position.CurrentValue * security.MinStepPrice / security.MinStepSize; } private static decimal GetPrice(decimal money, CurrencyTypes currency, IEnumerable positions) { foreach (var position in positions) { var price = position.GetPrice(); if (currency != position.Portfolio.Currency) price *= Convert(currency, position.Portfolio.Currency); money += price; } return money; } /// /// Рассчитать стоимость портфеля. /// /// Портфель. /// Стоимость портфеля. public static decimal GetPrice(this Portfolio portfolio) { if (portfolio == null) throw new ArgumentNullException("portfolio"); var basket = portfolio as BasketPortfolio; return basket == null ? GetPrice(portfolio.GetFreeMoney(), portfolio.Currency, portfolio.Trader.Positions.Filter(portfolio)) : GetPrice(basket.CurrentValue, basket.Currency, basket.InnerPositions); } /// /// Рассчитать проскальзывание по сделке. В качестве ожидаемой цены берется . /// /// Моя сделка, для которой необходимо рассчитать проскальзывание. /// Считать ли отрицательное проскальзывание. Если false, то отрицательное значение проскальзывания заменяется нулем. /// Значение проскальзывания. public static decimal GetSlippage(this MyTrade trade, bool calcNegative = false) { if (trade == null) throw new ArgumentNullException("trade"); return trade.GetSlippage(trade.Order.Price, calcNegative); } /// /// Рассчитать проскальзывание по сделке. /// /// Моя сделка, для которой необходимо рассчитать проскальзывание. /// Ожидаемая цена заявки. /// Считать ли отрицательное проскальзывание. Если false, то отрицательное значение проскальзывания заменяется нулем. /// Значение проскальзывания. public static decimal GetSlippage(this MyTrade trade, decimal estimatedPrice, bool calcNegative = false) { if (trade == null) throw new ArgumentNullException("trade"); if (trade.Order.Type == OrderTypes.Market) return 0; if (estimatedPrice <= 0) throw new ArgumentOutOfRangeException("estimatedPrice", estimatedPrice, "Неправильное значение для ожидаемой цены."); var slippage = (trade.Order.Direction == OrderDirections.Buy) ? trade.Trade.Price - estimatedPrice : estimatedPrice - trade.Trade.Price; // // заявка была кинута глубоко в рынок // http://stocksharp.com/forum/default.aspx?g=posts&m=3739#post3739 // if (!calcNegative && slippage < 0) slippage = 0; return slippage * trade.Trade.Volume; } /// /// Проверить, является ли переданное время торгуемым (началась ли сессия, не закончилась ли, нет ти клиринга). /// /// Временные интервалы работы биржи. Например, для FORTS можно передавать значения 10:00-13:59, 14:03-18:49 и 19:00-23:49. /// Передаваемое время, которое нужно проверить. /// True, если торгуемое время, иначе, неторгуемое. public static bool IsTradeTime(this IEnumerable> timeBounds, DateTime dateTime) { if (timeBounds == null) throw new ArgumentNullException("timeBounds"); return new WorkingTime { Times = timeBounds.ToArray() }.IsTradeTime(dateTime); } /// /// Проверить, является ли текущее время торгуемым (началась ли сессия, не закончилась ли, нет ти клиринга). /// /// Инструмент, хранящий информацию о бирже . Если она равна null, то инструмент торгуется /// круглосуточно. Иначе, информация передается в метод . /// Передаваемое время, которое нужно проверить. /// True, если торгуемое время, иначе, неторгуемое. public static bool IsTradeTime(this Security security, DateTime dateTime) { if (security == null) throw new ArgumentNullException("security"); return security.ExchangeBoard == null || security.ExchangeBoard.IsTradeTime(dateTime); } /// /// Проверить, является ли текущее время торгуемым (началась ли сессия, не закончилась ли, нет ти клиринга). /// /// Информация о площадке, хранящая временные интервалы работы биржи . /// Шлюз с торговой системой. В качестве текущего времени берется результат выполнения метода . /// True, если торгуемое время, иначе, неторгуемое. public static bool IsTradeTime(this ExchangeBoard board, ITrader trader) { if (trader == null) throw new ArgumentNullException("trader"); return board.IsTradeTime(trader.GetMarketTime(board.Exchange)); } /// /// Проверить, является ли текущее время торгуемым (началась ли сессия, не закончилась ли, нет ти клиринга). /// /// Информация о площадке, хранящая временные интервалы работы биржи . /// Например, для FORTS будут значения 10:00-13:59, 14:03-18:49 и 19:00-23:49. /// Передаваемое время, которое нужно проверить. /// True, если торгуемое время, иначе, неторгуемое. public static bool IsTradeTime(this ExchangeBoard board, DateTime dateTime) { if (board == null) throw new ArgumentNullException("board"); return board.WorkingTime.IsTradeTime(dateTime); } /// /// Проверить, является ли время торгуемым (началась ли сессия, не закончилась ли, нет ли клиринга). /// /// Информация о режиме работы биржи. /// Передаваемое время, которое нужно проверить. /// True, если торгуемое время, иначе, неторгуемое. public static bool IsTradeTime(this WorkingTime workingTime, DateTime dateTime) { var isWorkingDay = workingTime.IsTradeDate(dateTime.Date); if (!isWorkingDay) return false; var time = dateTime.TimeOfDay; var timeBounds = workingTime.Times; return timeBounds.IsEmpty() || timeBounds.Any(r => r.Contains(time)); } /// /// Проверить, является ли дата торгуемой. /// /// Информация о режиме работы биржи. /// Передаваемая дата, которую необходимо проверить. /// Проверять ли переданную дату на день недели (суббота и воскресенье являются выходными и для них будет возвращено false). /// True, если торгуемая дата, иначе, неторгуемая. public static bool IsTradeDate(this WorkingTime workingTime, DateTime date, bool checkHolidays = false) { if (workingTime == null) throw new ArgumentNullException("workingTime"); if (workingTime.Times.Length == 0 && workingTime.SpecialWorkingDays.Length == 0 && workingTime.SpecialHolidays.Length == 0) return true; bool isWorkingDay; if (checkHolidays && (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)) isWorkingDay = workingTime.SpecialWorkingDays.Contains(date.Date); else isWorkingDay = !workingTime.SpecialHolidays.Contains(date.Date); return isWorkingDay; } /// /// Заполнить праздниками по-умолчанию. /// /// Площадка. /// Начальный год. /// Конечный год. public static void ApplyDefaultHolidays(this ExchangeBoard board, DateTime startYear, DateTime endYear) { if (board == null) throw new ArgumentNullException("board"); if (startYear >= endYear) throw new ArgumentOutOfRangeException("endYear"); var holidays = new List(); for (var year = startYear.Year; year <= endYear.Year; year++) { for (var i = 1; i <= 10; i++) holidays.Add(new DateTime(year, 1, i)); holidays.Add(new DateTime(year, 2, 23)); holidays.Add(new DateTime(year, 3, 8)); holidays.Add(new DateTime(year, 5, 1)); holidays.Add(new DateTime(year, 5, 2)); holidays.Add(new DateTime(year, 5, 9)); holidays.Add(new DateTime(year, 6, 13)); holidays.Add(new DateTime(year, 11, 4)); } board.WorkingTime.SpecialHolidays = holidays.ToArray(); } /// /// Получить биржевое время. /// /// Инструмент, из которого будет взята информация о бирже . /// Биржевое время. public static DateTime GetMarketTime(this Security security) { if (security == null) throw new ArgumentNullException("security"); return security.CheckTrader().GetMarketTime(security.CheckExchangeBoard().Exchange); } /// /// Создать копию заявки для перерегистрации с новым объемом, равным заявки . /// /// Оригинальная заявка. /// Цена новой заявки. /// Новая заявка. public static Order ReRegisterClone(this Order oldOrder, decimal newPrice) { return oldOrder.ReRegisterClone(newPrice, oldOrder.Balance); } /// /// Создать копию заявки для перерегистрации. /// /// Оригинальная заявка. /// Цена новой заявки. /// Объем новой заявки. /// Новая заявка. public static Order ReRegisterClone(this Order oldOrder, decimal newPrice, decimal newVolume) { if (oldOrder == null) throw new ArgumentNullException("oldOrder"); var newOrder = oldOrder.Clone(); newOrder.Price = newPrice; newOrder.Volume = newVolume; return newOrder; } private static readonly ChannelFactory _dailyInfoFactory = new ChannelFactory(new BasicHttpBinding(), new EndpointAddress("http://www.cbr.ru/dailyinfowebserv/dailyinfo.asmx")); private static readonly Dictionary> _rateInfo = new Dictionary>(); /// /// Сконвертировать одну валюту в другую. /// /// Валюта, из которой нужно произвести конвертацию. /// Код валюты, в которую нужно произвести конвертацию. /// Сконвертированная валюта. public static Currency Convert(this Currency currencyFrom, CurrencyTypes currencyTypeTo) { if (currencyFrom == null) throw new ArgumentNullException("currencyFrom"); return new Currency { Type = currencyTypeTo, Value = currencyFrom.Value * currencyFrom.Type.Convert(currencyTypeTo) }; } /// /// Получить курс конвертации одной валюту в другую. /// /// Код валюты, из которой нужно произвести конвертацию. /// Код валюты, в которую нужно произвести конвертацию. /// Курс. public static decimal Convert(this CurrencyTypes from, CurrencyTypes to) { return from.Convert(to, DateTime.Today); } /// /// Получить курс конвертации одной валюту в другую на определенную дату. /// /// Код валюты, из которой нужно произвести конвертацию. /// Код валюты, в которую нужно произвести конвертацию. /// Дата курса. /// Курс. public static decimal Convert(this CurrencyTypes from, CurrencyTypes to, DateTime date) { if (from == to) return 1; var info = _rateInfo.SafeAdd(date, key => { var i = _dailyInfoFactory.Invoke(c => c.GetCursOnDate(key)); return i.Tables[0].Rows.Cast().ToDictionary(r => r[4].To(), r => r[2].To()); }); if (from != CurrencyTypes.RUB && !info.ContainsKey(from)) throw new ArgumentException("Валюта '{0}' не поддерживается.".Put(from), "from"); if (to != CurrencyTypes.RUB && !info.ContainsKey(to)) throw new ArgumentException("Валюта '{0}' не поддерживается.".Put(to), "to"); if (from == CurrencyTypes.RUB) return 1 / info[to]; else if (to == CurrencyTypes.RUB) return info[from]; else return info[from] / info[to]; } /// /// Создать из обычного стакана разреженный с минимальным шагом цены равный . /// /// В разреженном стакане показаны котировки на те цены, по которым не выставлены заявки. Объем таких котировок равен 0. /// /// /// Обычный стакан. /// Разреженный стакан. public static MarketDepth Sparse(this MarketDepth depth) { if (depth == null) throw new ArgumentNullException("depth"); return depth.Sparse(depth.Security.MinStepSize); } /// /// Создать из обычного стакана разреженный. /// /// В разреженном стакане показаны котировки на те цены, по которым не выставлены заявки. Объем таких котировок равен 0. /// /// /// Обычный стакан. /// Минимальный шаг цены. /// Разреженный стакан. public static MarketDepth Sparse(this MarketDepth depth, decimal minStepSize) { if (depth == null) throw new ArgumentNullException("depth"); var bids = depth.Bids.Sparse(minStepSize); var asks = depth.Asks.Sparse(minStepSize); var spreadQuotes = depth.BestPair.Sparse(minStepSize); return new MarketDepth(depth.Security).Update( bids.Concat(spreadQuotes.Where(q => q.OrderDirection == OrderDirections.Buy)), asks.Concat(spreadQuotes.Where(q => q.OrderDirection == OrderDirections.Sell)), false, depth.LastChangeTime); } /// /// Создать из пары котировок разреженную коллекцию котировок, которая будет входить в диапазон между парой. /// /// В разреженной коллекции показаны котировки на те цены, по которым не выставлены заявки. Объем таких котировок равен 0. /// /// /// Пара обычных котировок. /// Минимальный шаг цены. /// Разреженная коллекция котировок. public static IEnumerable Sparse(this MarketDepthPair pair, decimal minStepSize) { if (pair == null) throw new ArgumentNullException("pair"); if (minStepSize <= 0) throw new ArgumentOutOfRangeException("minStepSize", minStepSize, "Размер шага цены меньше допустимого."); if (pair.SpreadPrice == 0 && pair.SpreadPrice <= minStepSize) return new Quote[0]; var security = pair.Bid.Security; var retVal = new List(); var bidPrice = pair.Bid.Price; var askPrice = pair.Ask.Price; var minStepDecimal = minStepSize; while (true) { bidPrice += minStepDecimal; askPrice -= minStepDecimal; if (bidPrice > askPrice) break; retVal.Add(new Quote { Security = security, Price = bidPrice, OrderDirection = OrderDirections.Buy, }); if (bidPrice == askPrice) break; retVal.Add(new Quote { Security = security, Price = askPrice, OrderDirection = OrderDirections.Sell, }); } return retVal.OrderBy(q => q.Price); } /// /// Создать из обычных котировок разреженную коллекцию котировок. /// /// В разреженной коллекции показаны котировки на те цены, по которым не выставлены заявки. Объем таких котировок равен 0. /// /// /// Обычные котировки. Коллекция должна содержать одинаково направленные котировки (только биды или только оффера). /// Минимальный шаг цены. /// Разреженная коллекция котировок. public static IEnumerable Sparse(this IEnumerable quotes, decimal minStepSize) { if (quotes == null) throw new ArgumentNullException("quotes"); if (minStepSize <= 0) throw new ArgumentOutOfRangeException("minStepSize", minStepSize, "Размер шага цены меньше допустимого."); var list = quotes.OrderBy(q => q.Price).ToList(); if (list.Count < 2) return new Quote[0]; var firstQuote = list[0]; var retVal = new List(); for (var i = 0; i < (list.Count - 1); i++) { var from = list[i]; if (from.OrderDirection != firstQuote.OrderDirection) throw new ArgumentException("Коллекция котировок не одинаково направлена.", "quotes"); var toPrice = list[i + 1].Price; for (var price = (from.Price + minStepSize); price < toPrice; price += minStepSize) { retVal.Add(new Quote { Security = firstQuote.Security, Price = price, OrderDirection = firstQuote.OrderDirection, }); } } if (firstQuote.OrderDirection == OrderDirections.Buy) return retVal.OrderByDescending(q => q.Price); else return retVal; } /// /// Объединить первоначальный стакан, и его разреженное представление. /// /// Первоначальный стакан. /// Разреженный стакан. /// Объединенный стакан. public static MarketDepth Join(this MarketDepth original, MarketDepth rare) { if (original == null) throw new ArgumentNullException("original"); if (rare == null) throw new ArgumentNullException("rare"); return new MarketDepth(original.Security).Update(original.Concat(rare), original.LastChangeTime); } /// /// Сгруппировать стакан по ценовому диапазону. /// /// Стакан, который необходимо сгруппировать. /// Ценовой диапазон, по которому необходимо произвести группировку. /// Сгруппированный стакан. public static MarketDepth Group(this MarketDepth depth, Unit priceRange) { return new MarketDepth(depth.Security).Update(depth.Bids.Group(priceRange), depth.Asks.Group(priceRange), true, depth.LastChangeTime); } /// /// Разгруппировать стакан, сгруппированный через метод . /// /// Сгруппированный стакан. /// Разгруппированный стакан. public static MarketDepth UnGroup(this MarketDepth depth) { return new MarketDepth(depth.Security).Update( depth.Bids.Cast().SelectMany(gq => gq.InnerQuotes), depth.Asks.Cast().SelectMany(gq => gq.InnerQuotes), false, depth.LastChangeTime); } /// /// Удалить в стакане те уровни, которые должны исчезнуть в случае появления сделок . /// /// Стакан, который необходимо очистить. /// Сделки. public static void EmulateTrades(this MarketDepth depth, IEnumerable trades) { if (depth == null) throw new ArgumentNullException("depth"); if (trades == null) throw new ArgumentNullException("trades"); var changedVolume = new Dictionary(); var maxTradePrice = decimal.MinValue; var minTradePrice = decimal.MaxValue; foreach (var trade in trades) { minTradePrice = minTradePrice.Min(trade.Price); maxTradePrice = maxTradePrice.Max(trade.Price); var quote = depth.GetQuote(trade.Price); if (null == quote) continue; decimal vol; if (!changedVolume.TryGetValue(trade.Price, out vol)) vol = quote.Volume; vol -= trade.Volume; changedVolume[quote.Price] = vol; } var bids = new Quote[depth.Bids.Length]; Action a1 = () => { var i = 0; var count = 0; for (; i < depth.Bids.Length; i++) { var quote = depth.Bids[i]; var price = quote.Price; if (price > minTradePrice) continue; if (price == minTradePrice) { decimal vol; if (changedVolume.TryGetValue(price, out vol)) { if (vol <= 0) continue; quote = quote.Clone(); quote.Volume = vol; } } bids[count++] = quote; i++; break; } Array.Copy(depth.Bids, i, bids, count, depth.Bids.Length - i); Array.Resize(ref bids, count + (depth.Bids.Length - i)); }; a1(); var asks = new Quote[depth.Asks.Length]; Action a2 = () => { var i = 0; var count = 0; for (; i < depth.Asks.Length; i++) { var quote = depth.Asks[i]; var price = quote.Price; if (price < maxTradePrice) continue; if (price == maxTradePrice) { decimal vol; if (changedVolume.TryGetValue(price, out vol)) { if (vol <= 0) continue; quote = quote.Clone(); quote.Volume = vol; } } asks[count++] = quote; i++; break; } Array.Copy(depth.Asks, i, asks, count, depth.Asks.Length - i); Array.Resize(ref asks, count + (depth.Asks.Length - i)); }; a2(); depth.Update(bids, asks, depth.LastChangeTime); } /// /// Сгруппировать котировки по ценовому диапазону. /// /// Котировки, которые необходимо сгруппировать. /// Ценовой диапазон, по которому необходимо произвести группировку. /// Сгруппированные котировки. public static IEnumerable Group(this IEnumerable quotes, Unit priceRange) { if (quotes == null) throw new ArgumentNullException("quotes"); if (priceRange == null) throw new ArgumentNullException("priceRange"); //if (priceRange.Value < double.Epsilon) // throw new ArgumentOutOfRangeException("priceRange", priceRange, "Размер группировки меньше допустимого."); if (quotes.Count() < 2) return new AggregatedQuote[0]; var firstQuote = quotes.First(); var retVal = quotes.GroupBy(q => priceRange.AlignPrice(firstQuote.Price, q.Price)).Select(g => { var aggQuote = new AggregatedQuote { Price = g.Key }; aggQuote.InnerQuotes.AddRange(g); return aggQuote; }); retVal = firstQuote.OrderDirection == OrderDirections.Buy ? retVal.OrderBy(q => q.Price) : retVal.OrderByDescending(q => q.Price); return retVal; } internal static decimal AlignPrice(this Unit priceRange, decimal firstPrice, decimal price) { if (priceRange == null) throw new ArgumentNullException("priceRange"); decimal priceLevel; if (priceRange.Type == UnitTypes.Percent) priceLevel = (decimal)(firstPrice + MathHelper.Floor((((price - firstPrice) * 100) / firstPrice), priceRange.Value).Percents()); else priceLevel = MathHelper.Floor(price, (decimal)priceRange); return priceLevel; } /// /// Вычислить изменение между стаканами. /// /// Первый стакан. /// Второй стакан. /// Стакан, хранищий только изменения. public static MarketDepth GetDelta(this MarketDepth from, MarketDepth to) { if (from == null) throw new ArgumentNullException("from"); if (to == null) throw new ArgumentNullException("to"); var delta = new MarketDepth(from.Security); delta.Update(GetDelta(from.Bids, to.Bids), GetDelta(from.Asks, to.Asks), false, to.LastChangeTime); return delta; } /// /// Вычислить изменение между котировками. /// /// Первые котировки. /// Вторые котировки. /// Изменения. public static Quote[] GetDelta(IEnumerable from, IEnumerable to) { var mapTo = to.ToDictionary(q => q.Price); var mapFrom = from.ToDictionary(q => q.Price); foreach (var pair in mapFrom) { var price = pair.Key; var quoteFrom = pair.Value; var quoteTo = mapTo.TryGetValue(price); if (quoteTo != null) { if (quoteTo.Volume == quoteFrom.Volume) mapTo.Remove(price); // то же самое } else { var empty = quoteFrom.Clone(); empty.Volume = 0; // была а теперь нет mapTo[price] = empty; } } return mapTo.Values.ToArray(); } /// /// Вычислить приращение между стаканами. /// /// Первый стакан. /// Второй стакан. /// Стакан, хранищий только приращения. public static MarketDepth GetDiff(this MarketDepth from, MarketDepth to) { if (from == null) throw new ArgumentNullException("from"); if (to == null) throw new ArgumentNullException("to"); var diff = new MarketDepth(from.Security); diff.Update(GetDiff(from.Bids, to.Bids), GetDiff(from.Asks, to.Asks), false, to.LastChangeTime); return diff; } /// /// Вычислить приращение между котировками. /// /// Первые котировки. /// Вторые котировки. /// Изменения. public static Quote[] GetDiff(IEnumerable from, IEnumerable to) { var mapTo = to.ToDictionary(q => q.Price); var mapFrom = from.ToDictionary(q => q.Price); foreach (var pair in mapFrom) { var price = pair.Key; var quoteFrom = pair.Value; var quoteTo = mapTo.TryGetValue(price); if (quoteTo != null) { if (quoteTo.Volume == quoteFrom.Volume) mapTo.Remove(price); // то же самое else { var d = quoteTo.Clone(); d.Volume -= quoteFrom.Volume; mapTo[price] = d; } } else { var d = quoteFrom.Clone(); d.Volume = - quoteFrom.Volume; // была а теперь нет mapTo[price] = d; } } return mapTo.Values.ToArray(); } /// /// Прибавить изменение к первому стакану. /// /// Первый стакан. /// Изменение. /// Измененный стакан. public static MarketDepth AddDelta(this MarketDepth from, MarketDepth delta) { if (from == null) throw new ArgumentNullException("from"); if (delta == null) throw new ArgumentNullException("delta"); var depth = new MarketDepth(from.Security); depth.Update( AddDelta(from.Bids, delta.Bids, true), AddDelta(from.Asks, delta.Asks, false), delta.LastChangeTime); return depth; } /// /// Прибавить изменение к котировки. /// /// Котировки. /// Изменения. /// Признак направления котировок. /// Измененные котировки. public static Quote[] AddDelta(Quote[] from, Quote[] delta, bool isBids) { if (from.Length == 0) return delta; if (delta.Length == 0) return from; var result = new Quote[from.Length + delta.Length]; var fromIndex = 0; var deltaIndex = 0; var outIndex = 0; var currFrom = from[fromIndex]; var currDelta = delta[deltaIndex]; var currFromPrice = currFrom.Price; var currDeltaPrice = currDelta.Price; while (true) { var cmp = Decimal.Compare(currFromPrice, currDeltaPrice); if (cmp < 0 && isBids || cmp > 0 && !isBids) //currDeltaPrice > currFromPrice { result[outIndex++] = currDelta; deltaIndex++; if (deltaIndex < delta.Length) { currDelta = delta[deltaIndex]; currDeltaPrice = currDelta.Price; } else break; } else if (cmp < 0 && !isBids || cmp > 0 && isBids) { result[outIndex++] = currFrom; fromIndex++; if (fromIndex < from.Length) { currFrom = from[fromIndex]; currFromPrice = currFrom.Price; } else break; } else // cmp==0 { if (currDelta.Volume != 0) result[outIndex++] = currDelta; deltaIndex++; fromIndex++; if (deltaIndex < delta.Length) { currDelta = delta[deltaIndex]; currDeltaPrice = currDelta.Price; } else break; if (fromIndex < from.Length) { currFrom = from[fromIndex]; currFromPrice = currFrom.Price; } else break; } } Array.Copy(from, fromIndex, result, outIndex, from.Length - fromIndex); outIndex += from.Length - fromIndex; Array.Copy(delta, deltaIndex, result, outIndex, delta.Length - deltaIndex); outIndex += delta.Length - deltaIndex; Array.Resize(ref result, outIndex); return result; } /// /// Проверить, отменена ли заявка. /// /// Заявка, которую необходимо проверить. /// True, если заявка отменена, иначе, false. public static bool IsCanceled(this Order order) { if (order == null) throw new ArgumentNullException("order"); if (order.State != OrderStates.Done) // для ускорения в эмуляторе return false; using (order.BeginRead()) return (order.State == OrderStates.Done && order.Balance != 0); } /// /// Проверить, исполнена ли полностью заявка. /// /// Заявка, которую необходимо проверить. /// True, если заявка полностью исполнена, иначе, false. public static bool IsMatched(this Order order) { if (order == null) throw new ArgumentNullException("order"); using (order.BeginRead()) return (order.State == OrderStates.Done && order.Balance == 0); } /// /// Проверить, реализована ли часть объема в заявке. /// /// Заявка, которую необходимо проверить. /// True, если часть объема реализована, иначе, false. public static bool IsMatchedPartially(this Order order) { if (order == null) throw new ArgumentNullException("order"); using (order.BeginRead()) return (order.Balance > 0 && order.Balance != order.Volume); } /// /// Проверить, что не реализован ни один контракт в заявке. /// /// Заявка, которую необходимо проверить. /// True, если ни один контракт не реализована, иначе, false. public static bool IsMatchedEmpty(this Order order) { if (order == null) throw new ArgumentNullException("order"); using (order.BeginRead()) return (order.Balance > 0 && order.Balance == order.Volume); } /// /// Получить сделки заявки. /// /// Заявки. /// Сделки. public static IEnumerable GetTrades(this Order order) { return order.CheckTrader().MyTrades.Filter(order); } /// /// Расcчитать реализованную часть объема для заявки. /// /// Заявка, для которой необходимо расcчитать реализованную часть объема. /// Проверять реализованный объем по балансу заявке () или по полученным сделкам. /// По-умолчанию проверяется по заявке. /// Реализованная часть объема. public static decimal GetMatchedVolume(this Order order, bool byOrder = true) { if (order == null) throw new ArgumentNullException("order"); if (order.Type == OrderTypes.Conditional) { //throw new ArgumentException("Стоп-заявки не могут иметь реализованный объем.", "order"); order = order.DerivedOrder; if (order == null) return 0; } return order.Volume - (byOrder ? order.Balance : order.GetTrades().Sum(o => o.Trade.Volume)); } /// /// Получить средневзрешанную цену исполнения заявки. /// /// Заявка, для которой необходимо получить средневзрешанную цену исполнения. /// Средневзвешанная цена. Если заявка не существует ни одной сделки, то возвращается 0. public static decimal GetAveragePrice(this Order order) { return order.GetTrades().GetAveragePrice(); } /// /// Получить средневзрешанную цену исполнения по собственным сделкам. /// /// Сделки, для которых необходимо получить средневзрешанную цену исполнения. /// Средневзвешанная цена. Если сделки отсутствуют, то возвращается 0. public static decimal GetAveragePrice(this IEnumerable trades) { if (trades == null) throw new ArgumentNullException("trades"); return trades.Select(t => t.Trade).GetAveragePrice(); } /// /// Получить средневзрешанную цену исполнения по тиковым сделкам. /// /// Сделки, для которых необходимо получить средневзрешанную цену исполнения. /// Средневзвешанная цена. Если сделки отсутствуют, то возвращается 0. public static decimal GetAveragePrice(this IEnumerable trades) { if (trades == null) throw new ArgumentNullException("trades"); var nominator = 0m; var denominator = 0m; foreach (var trade in trades) { nominator += trade.Price * trade.Volume; denominator += trade.Volume; } if (denominator == 0) return 0; return nominator / denominator; } /// /// Получить вероятные сделки по стакану для заданной заявки. /// /// Стакан, который в момент вызова функции отражает ситуацию на рынке. /// Заявку, для которой необходимо расcчитать вероятные сделки. /// Вероятные сделки. public static IEnumerable GetTheoreticalTrades(this MarketDepth depth, Order order) { if (depth == null) throw new ArgumentNullException("depth"); if (depth.Trader == null) throw new ArgumentException("Стакан не имеет информацию о шлюзе.", "depth"); if (order == null) throw new ArgumentNullException("order"); using (var emulator = new MarketEmulator(depth.Trader)) { emulator.Reset(LoggingHelper.Now); emulator.Start(); emulator.RegisterOrder(order.Clone()); emulator.UpdateQuotes(depth.Clone(), null); return emulator.MyTrades; } } /// /// Получить вероятные сделки по стакану для рыночной цены и заданного объема. /// /// Стакан, который в момент вызова функции отражает ситуацию на рынке. /// Направление заявки. /// Объем, который предполагается реализовать. /// Вероятные сделки. public static IEnumerable GetTheoreticalTrades(this MarketDepth depth, OrderDirections orderDirection, decimal volume) { return depth.GetTheoreticalTrades(orderDirection, volume, 0); } private static readonly Portfolio _testPf = new Portfolio { Name = "test account" }; /// /// Получить вероятные сделки по стакану для заданных цены и объема. /// /// Стакан, который в момент вызова функции отражает ситуацию на рынке. /// Направление заявки. /// Объем, который предполагается реализовать. /// Цена, по которой предполагает выставить заявку. Если она равна 0, то будет рассматриваться вариант рыночной заявки. /// Вероятные сделки. public static IEnumerable GetTheoreticalTrades(this MarketDepth depth, OrderDirections orderDirection, decimal volume, decimal price) { if (depth == null) throw new ArgumentNullException("depth"); return depth.GetTheoreticalTrades(new Order { Direction = orderDirection, Type = price == 0 ? OrderTypes.Market : OrderTypes.Limit, Security = depth.Security, Price = price, Volume = volume, Portfolio = _testPf }); } /// /// Поменять направление заявки на противоположное. /// /// Первоначальное направление. /// Противоположное направление. public static OrderDirections Invert(this OrderDirections direction) { return direction == OrderDirections.Buy ? OrderDirections.Sell : OrderDirections.Buy; } /// /// Получить направление заявки для позиции. /// /// /// Положительное значение равно , отрицательное - , нулевое - null. /// /// Значение позиции. /// Направление заявки. public static OrderDirections? GetDirection(this Position position) { if (position == null) throw new ArgumentNullException("position"); return position.CurrentValue.GetDirection(); } /// /// Получить направление заявки для позиции. /// /// /// Положительное значение равно , отрицательное - , нулевое - null. /// /// Значение позиции. /// Направление заявки. public static OrderDirections? GetDirection(this decimal position) { if (position == 0) return null; return position > 0 ? OrderDirections.Buy : OrderDirections.Sell; } /// /// Отменить группу заявок на бирже по фильтру. /// /// Шлюз взаимодействия с торговыми системами. /// Группа заявок, из которой необходимо найти требуемые заявки и отменить их. /// True, если нужно отменить только стоп-заявки, false - если только обычный и null - если оба типа. /// Портфель. Если значение равно null, то портфель не попадает в фильтр снятия заявок. /// Направление заявки. Если значение равно null, то направление не попадает в фильтр снятия заявок. /// Код класса. Если переданная строка пустая, то код не попадает в фильтр снятия заявок. /// Инструмент. Если значение равно null, то инструмент не попадает в фильтр снятия заявок. public static void CancelOrders(this ITrader trader, IEnumerable orders, bool? isStopOrder = null, Portfolio portfolio = null, OrderDirections? direction = null, string classCode = null, Security security = null) { if (trader == null) throw new ArgumentNullException("trader"); if (orders == null) throw new ArgumentNullException("orders"); foreach (var order in orders.Where(o => o.State != OrderStates.Done).ToArray()) { if (isStopOrder == null || (order.Type == OrderTypes.Conditional) == isStopOrder) { if (portfolio == null || (order.Portfolio == portfolio)) { if (direction == null || order.Direction == direction) { if (classCode.IsEmpty() || order.Security.Class == classCode) { if (security == null || order.Security == security) { trader.CancelOrder(order); } } } } } } } /// /// Отфильтровать заявки для заданного инструмента. /// /// Все заявки, в которых необходимо искать требуемые. /// Инструмент, для которого нужно отфильтровать заявки. /// Отфильтрованные заявки. public static IEnumerable Filter(this IEnumerable orders, Security security) { if (orders == null) throw new ArgumentNullException("orders"); if (security == null) throw new ArgumentNullException("security"); var basket = security as BasketSecurity; return basket == null ? orders.Where(o => o.Security == security) : basket.InnerSecurities.SelectMany(s => Filter(orders, s)); } /// /// Отфильтровать заявки для заданного портфеля. /// /// Все заявки, в которых необходимо искать требуемые. /// Портфель, для которого нужно отфильтровать заявки. /// Отфильтрованные заявки. public static IEnumerable Filter(this IEnumerable orders, Portfolio portfolio) { if (orders == null) throw new ArgumentNullException("orders"); if (portfolio == null) throw new ArgumentNullException("portfolio"); return orders.Where(p => p.Portfolio == portfolio); } /// /// Отфильтровать заявки для заданного состояния. /// /// Все заявки, в которых необходимо искать требуемые. /// Условие выборки заявки. /// Отфильтрованные заявки. public static IEnumerable Filter(this IEnumerable orders, Func query) { if (orders == null) throw new ArgumentNullException("orders"); return orders.Where(query); //return orders.Where(p => p.State == state); } /// /// Отфильтровать заявки для заданного направления. /// /// Все заявки, в которых необходимо искать требуемые. /// Направление заявки. /// Отфильтрованные заявки. public static IEnumerable Filter(this IEnumerable orders, OrderDirections direction) { if (orders == null) throw new ArgumentNullException("orders"); return orders.Where(p => p.Direction == direction); } /// /// Отфильтровать сделки для заданного инструмента. /// /// Все сделки, в которых необходимо искать требуемые. /// Инструмент, для которого нужно отфильтровать сделки. /// Отфильтрованные сделки. public static IEnumerable Filter(this IEnumerable trades, Security security) { if (trades == null) throw new ArgumentNullException("trades"); if (security == null) throw new ArgumentNullException("security"); var basket = security as BasketSecurity; return basket == null ? trades.Where(t => t.Security == security) : basket.InnerSecurities.SelectMany(s => Filter(trades, s)); } /// /// Отфильтровать сделки для заданного временного периода. /// /// Все сделки, в которых необходимо искать требуемые. /// Дата, с которой нужно искать сделки. /// Дата, до которой нужно искать сделки. /// Отфильтрованные сделки. public static IEnumerable Filter(this IEnumerable trades, DateTime from, DateTime to) { if (trades == null) throw new ArgumentNullException("trades"); return trades.Where(trade => trade.Time >= from && trade.Time < to); } /// /// Отфильтровать позиции для заданного инструмента. /// /// Все позиции, в которых необходимо искать требуемые. /// Инструмент, для которого нужно отфильтровать позиции. /// Отфильтрованные позиции. public static IEnumerable Filter(this IEnumerable positions, Security security) { if (positions == null) throw new ArgumentNullException("positions"); if (security == null) throw new ArgumentNullException("security"); var basket = security as BasketSecurity; return basket == null ? positions.Where(p => p.Security == security) : basket.InnerSecurities.SelectMany(s => Filter(positions, s)); } /// /// Отфильтровать позиции для заданного портфеля. /// /// Все позиции, в которых необходимо искать требуемые. /// Портфель, для которого нужно отфильтровать позиции. /// Отфильтрованные позиции. public static IEnumerable Filter(this IEnumerable positions, Portfolio portfolio) { if (positions == null) throw new ArgumentNullException("positions"); if (portfolio == null) throw new ArgumentNullException("portfolio"); return positions.Where(p => p.Portfolio == portfolio); } /// /// Отфильтровать собственные сделки для заданного инструмента. /// /// Все собственные сделки, в которых необходимо искать требуемые. /// Инструмент, по которому нужно найти сделки. /// Отфильтрованные сделки. public static IEnumerable Filter(this IEnumerable myTrades, Security security) { if (myTrades == null) throw new ArgumentNullException("myTrades"); if (security == null) throw new ArgumentNullException("security"); var basket = security as BasketSecurity; return basket == null ? myTrades.Where(t => t.Order.Security == security) : basket.InnerSecurities.SelectMany(s => Filter(myTrades, s)); } /// /// Отфильтровать собственные сделки для заданного портфеля. /// /// Все собственные сделки, в которых необходимо искать требуемые. /// Портфель, для которого нужно отфильтровать сделки. /// Отфильтрованные сделки. public static IEnumerable Filter(this IEnumerable myTrades, Portfolio portfolio) { if (myTrades == null) throw new ArgumentNullException("myTrades"); if (portfolio == null) throw new ArgumentNullException("portfolio"); return myTrades.Where(t => t.Order.Portfolio == portfolio); } /// /// Отфильтровать собственные сделки для заданной заявки. /// /// Все собственные сделки, в которых необходимо искать требуемые. /// Заявка, для которой нужно отфильтровать сделки. /// Отфильтрованные заявки. public static IEnumerable Filter(this IEnumerable myTrades, Order order) { if (myTrades == null) throw new ArgumentNullException("myTrades"); if (order == null) throw new ArgumentNullException("order"); return myTrades.Where(t => t.Order == order); } /// /// Определить, является ли стакан пустым. /// /// Стакан. /// True, если стакан пустой, иначе, false. public static bool IsFullEmpty(this MarketDepth depth) { if (depth == null) throw new ArgumentNullException("depth"); return depth.Bids.Length ==0 && depth.Asks.Length == 0; } /// /// Определить, является ли стакан пустым на половину. /// /// Стакан. /// True, если стакан пустой на половину, иначе, false. public static bool IsHalfEmpty(this MarketDepth depth) { if (depth == null) throw new ArgumentNullException("depth"); return (depth.BestPair.Bid == null || depth.BestPair.Ask == null) && (depth.BestPair.Bid != depth.BestPair.Ask); } /// /// Получить T+N дату. /// /// Информация о режиме работы биржи. /// Начальная дата T. /// Размер N. /// Конечная дата T+N. public static DateTime GetTPlusNDate(this WorkingTime time, DateTime date, int n) { if (time == null) throw new ArgumentNullException("time"); date = date.Date; while (n > 0) { if (time.IsTradeDate(date)) n--; date = date.AddDays(1); } return date; } /// /// Перевести локальное время в биржевое. /// /// Информация о бирже. /// Локальное время. /// Время с биржевым сдвигом. public static DateTime ToExchangeTime(this Exchange exchange, DateTime time) { return exchange.ToExchangeTime(time, TimeZoneInfo.Local); } /// /// Перевести локальное время в биржевое. /// /// Информация о бирже. /// Локальное время. /// Времемнная зона, в которой записано значение . /// Время с биржевым сдвигом. public static DateTime ToExchangeTime(this Exchange exchange, DateTime time, TimeZoneInfo sourceZone) { if (exchange == null) throw new ArgumentNullException("exchange"); return TimeZoneInfo.ConvertTime(time, sourceZone, exchange.TimeZoneInfo); } /// /// Перевести биржевое время в локальное. /// /// Информация о бирже, из которой будет использоваться . /// Биржевое время. /// Локальное время. public static DateTime ToLocalTime(this Exchange exchange, DateTime exchangeTime) { if (exchange == null) throw new ArgumentNullException("exchange"); return TimeZoneInfo.ConvertTime(exchangeTime, exchange.TimeZoneInfo, TimeZoneInfo.Local); } /// /// Перевести биржевое время в UTC. /// /// Информация о бирже, из которой будет использоваться . /// Биржевое время. /// Биржевое время в UTC. public static DateTime ToUtc(this Exchange exchange, DateTime exchangeTime) { if (exchange == null) throw new ArgumentNullException("exchange"); return TimeZoneInfo.ConvertTimeToUtc(exchangeTime, exchange.TimeZoneInfo); } /// /// Инициализировать . Задержка равна разнице между и . /// /// Стакан. public static MarketDepth InitLatency(this MarketDepth depth) { if (depth == null) throw new ArgumentNullException("depth"); depth.Latency = depth.LastChangeTime.GetLatency(depth.Security); return depth; } /// /// Инициализировать . Задержка равна разнице между и . /// /// Тиковая сделка. public static Trade InitLatency(this Trade trade) { if (trade == null) throw new ArgumentNullException("trade"); trade.Latency = trade.Time.GetLatency(trade.Security); return trade; } /// /// Инициализировать . Задержка равна разнице между и . /// /// Собственная сделка. public static MyTrade InitLatency(this MyTrade trade) { if (trade == null) throw new ArgumentNullException("trade"); trade.Latency = (trade is OrderLogItem ? trade.Order.Time : trade.Trade.Time).GetLatency(trade.Order.Security); return trade; } private static TimeSpan GetLatency(this DateTime time, Security security) { return GetLatency(LoggingHelper.Now, time, security.CheckExchangeBoard().Exchange); } /// /// Рассчитать задержку. /// /// Текущее время. /// Биржевое время. /// Информация о бирже. /// Задержка. public static TimeSpan GetLatency(DateTime now, DateTime time, Exchange exchange) { return now - exchange.ToLocalTime(time); } /// /// Получить цену, которую необходимо оплатить для совершения торговой операции. /// /// Инструмент. /// Направление торговой операции. /// Цена. public static decimal GetMarginPrice(this Security security, OrderDirections? direction = null) { if (security == null) throw new ArgumentNullException("security"); if (direction == null) return security.LastTrade != null ? security.LastTrade.Price : 0; else return direction == OrderDirections.Buy ? security.MarginBuy : security.MarginSell; } /// /// Получить размер свободных денежных средств в портфеле. /// /// Портфель /// Использовать ли для рассчета размер плеча. /// Размер свободных денежных средств. public static decimal GetFreeMoney(this Portfolio portfolio, bool useLeverage = false) { if (portfolio == null) throw new ArgumentNullException("portfolio"); var freeMoney = portfolio.ExchangeBoard == ExchangeBoard.Forts ? portfolio.BeginValue - portfolio.CurrentValue + portfolio.VariationMargin : portfolio.CurrentValue; return useLeverage ? freeMoney * portfolio.Leverage : freeMoney; } /// /// Получить для непрерывного инструмента реальные экспирирующиеся инструменты. /// /// Непрерывный инструмент. /// Базовая часть кода инструмента. /// Начало диапазона экспираций. /// Окончание диапазона экспираций. /// Сгенерировать исключение, если какой-либо из инструментов для переданного отсутствует. /// Экспирирующиеся инструменты. public static IEnumerable GetFortsJumps(this ContinuousSecurity continuousSecurity, string baseCode, DateTime from, DateTime to, bool throwIfNotExists = true) { if (continuousSecurity == null) throw new ArgumentNullException("continuousSecurity"); if (baseCode.IsEmpty()) throw new ArgumentNullException("baseCode"); if (from > to) throw new ArgumentOutOfRangeException("from"); var trader = continuousSecurity.CheckTrader(); for (var year = from.Year; year <= to.Year; year++) { var monthFrom = year == from.Year ? from.Month : 1; var monthTo = year == to.Year ? to.Month : 12; for (var month = monthFrom; month <= monthTo; month++) { char monthCode; switch (month) { case 3: monthCode = 'H'; break; case 6: monthCode = 'M'; break; case 9: monthCode = 'U'; break; case 12: monthCode = 'Z'; break; default: continue; } var yearStr = year.To(); var code = baseCode + monthCode + yearStr.Substring(yearStr.Length - 1, 1); foreach (var security in trader.Securities) { if (security.Code.CompareIgnoreCase(code)) yield return security; } if (throwIfNotExists) throw new InvalidOperationException("Инструмент с кодом {0} не найден.".Put(code)); } } } /// /// Заполнить переходы . /// /// Непрерывный инструмент. /// Базовая часть кода инструмента. /// Начало диапазона экспираций. /// Окончание диапазона экспираций. public static void FillFortsJumps(this ContinuousSecurity continuousSecurity, string baseCode, DateTime from, DateTime to) { var securities = continuousSecurity.GetFortsJumps(baseCode, from, to); foreach (var security in securities) { if (security.ExpiryDate == null) throw new InvalidOperationException("Инструмент {0} не имеет информацию о времени экспирации.".Put(security.Id)); continuousSecurity.ExpirationJumps.Add(security, (DateTime)security.ExpiryDate); } } private sealed class CashPosition : Position, IDisposable { private readonly Portfolio _portfolio; public CashPosition(Portfolio portfolio) { if (portfolio == null) throw new ArgumentNullException("portfolio"); _portfolio = portfolio; Portfolio = _portfolio; Security = new Security { Id = _portfolio.Name, Name = _portfolio.Name, }; UpdatePosition(); _portfolio.Trader.PortfoliosChanged += TraderOnPortfoliosChanged; } private void UpdatePosition() { BeginValue = _portfolio.BeginValue; CurrentValue = _portfolio.CurrentValue; BlockedValue = _portfolio.Commission; } private void TraderOnPortfoliosChanged(IEnumerable portfolios) { if (portfolios.Contains(_portfolio)) UpdatePosition(); } void IDisposable.Dispose() { _portfolio.Trader.PortfoliosChanged -= TraderOnPortfoliosChanged; } } /// /// Сконвертировать портфель в денежную позицию. /// /// Портфель с торговым счетом. /// Денежная позиция. public static Position ToCashPosition(this Portfolio portfolio) { return new CashPosition(portfolio); } private sealed class NativePositionManager : IPositionManager { private readonly Position _position; public NativePositionManager(Position position) { if (position == null) throw new ArgumentNullException("position"); _position = position; } /// /// Суммарное значение позиции. /// decimal IPositionManager.Position { get { return _position.CurrentValue; } set { throw new NotSupportedException(); } } event Action IPositionManager.NewPosition { add { } remove { } } event Action IPositionManager.PositionChanged { add { } remove { } } /// /// Рассчитать позицию по заявке. /// /// Заявка. /// Позиция по заявке. decimal IPositionManager.ProcessOrder(Order order) { throw new NotSupportedException(); } /// /// Рассчитать позицию по сделке. /// /// Сделка. /// Позиция по сделке. decimal IPositionManager.ProcessMyTrade(MyTrade trade) { throw new NotSupportedException(); } IEnumerable IPositionManager.Positions { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } void IPositionManager.Reset() { throw new NotSupportedException(); } } /// /// Сконвертировать позицию в объект типа . /// /// Позиция. /// Менеджера расчета позиции. public static IPositionManager ToPositionManager(this Position position) { if (position == null) throw new ArgumentNullException("position"); return new NativePositionManager(position); } private sealed class EquityStrategy : Strategy { private readonly Dictionary _orders; private readonly Dictionary, Strategy> _childStrategies = new Dictionary, Strategy>(); public EquityStrategy(IEnumerable orders, IDictionary openedPositions) { _orders = orders.GroupBy(o => o.Time).ToDictionary(g => g.Key, g => g.ToArray()); _childStrategies = orders.ToDictionary(GetKey, o => new Strategy { Portfolio = o.Portfolio, Security = o.Security, Position = openedPositions.TryGetValue2(o.Security) ?? 0, }); ChildStrategies.AddRange(_childStrategies.Values); } protected override void OnStarted() { base.OnStarted(); Security .WhenTimeCome(_orders.Keys) .Do(time => _orders[time].ForEach(o => _childStrategies[GetKey(o)].RegisterOrder(o))) .Apply(this); } private static Tuple GetKey(Order order) { return new Tuple(order.Security, order.Portfolio); } } /// /// Сэмулировать заявки на истории. /// /// Заявки, которые необходимо сэмулировать на истории. /// Внешнеее хранилище для доступа к исторических данным. /// Сделки, описывающие начальные открытые позиции. /// Виртуальная стратегии, содержащая в себе ход эмуляционных торгов. public static Strategy EmulateOrders(this IEnumerable orders, IStorageRegistry storageRegistry, IDictionary openedPositions) { if (openedPositions == null) throw new ArgumentNullException("openedPositions"); if (storageRegistry == null) throw new ArgumentNullException("storageRegistry"); if (orders == null) throw new ArgumentNullException("orders"); if (orders.IsEmpty()) throw new ArgumentOutOfRangeException("orders"); using (var trader = new RealTimeEmulationTrader(new DataFeedTrader(orders.Select(o => o.Security).Distinct(), orders.Select(o => o.Portfolio).Distinct()) { StorageRegistry = storageRegistry })) { var from = orders.Min(o => o.Time).Date; var to = from + TimeSpanHelper.LessOneDay; var strategy = new EquityStrategy(orders, openedPositions) { Trader = trader }; var waitHandle = new object(); trader.UnderlyingTrader.StateChanged += (oldState, newState) => { if (trader.UnderlyingTrader.State == EmulationStates.Started) strategy.Start(); if (trader.UnderlyingTrader.State == EmulationStates.Stopped) { strategy.Stop(); lock (waitHandle) Monitor.Pulse(waitHandle); } }; trader.Connect(); trader.StartExport(); trader.UnderlyingTrader.Start(from, to); lock (waitHandle) { if (trader.UnderlyingTrader.State != EmulationStates.Stopped) Monitor.Wait(waitHandle); } return strategy; } } /// /// Записать сообщение о заявке в лог. /// /// Получатель логов. /// Заявка. /// Операция, которая проводится в заявокй. /// Дополнительная информация о заявке. public static void AddOrderInfoLog(this ILogReceiver receiver, Order order, string operation, Func getAdditionalInfo = null) { receiver.AddOrderLog(LogLevels.Info, order, operation, getAdditionalInfo); } /// /// Записать ошибку о заявке в лог. /// /// Получатель логов. /// Заявка. /// Операция, которая проводится в заявокй. /// Дополнительная информация о заявке. public static void AddOrderErrorLog(this ILogReceiver receiver, Order order, string operation, Func getAdditionalInfo = null) { receiver.AddOrderLog(LogLevels.Error, order, operation, getAdditionalInfo); } private static void AddOrderLog(this ILogReceiver receiver, LogLevels type, Order order, string operation, Func getAdditionalInfo) { if (receiver == null) throw new ArgumentNullException("receiver"); if (order == null) throw new ArgumentNullException("order"); var orderDescription = order.ToString(); var additionalInfo = getAdditionalInfo == null ? string.Empty : getAdditionalInfo(); receiver.AddLog(new LogMessage(receiver, receiver.CurrentTime, type, () => "{0}: {1} {2}".Put(operation, orderDescription, additionalInfo))); } } }