StockSharp
735 строк · 20.4 Кб
1#region S# License2/******************************************************************************************
3NOTICE!!! This program and source code is owned and licensed by
4StockSharp, LLC, www.stocksharp.com
5Viewing or use of this code requires your acceptance of the license
6agreement found at https://github.com/StockSharp/StockSharp/blob/master/LICENSE
7Removal of this comment is a violation of the license agreement.
8
9Project: StockSharp.BusinessEntities.BusinessEntities
10File: MarketDepth.cs
11Created: 2015, 11, 11, 2:32 PM
12
13Copyright 2010 by StockSharp, LLC
14*******************************************************************************************/
15#endregion S# License16namespace StockSharp.BusinessEntities17{
18using System;19using System.ComponentModel;20using System.Collections;21using System.Collections.Generic;22using System.ComponentModel.DataAnnotations;23using System.Linq;24
25using Ecng.Collections;26using Ecng.Common;27
28using StockSharp.Messages;29using StockSharp.Localization;30
31/// <summary>32/// Order book.33/// </summary>34[System.Runtime.Serialization.DataContract]35[Serializable]36[Obsolete("Use IOrderBookMessage.")]37public class MarketDepth : Cloneable<MarketDepth>, IEnumerable<QuoteChange>, IOrderBookMessage38{39/// <summary>40/// Create order book.41/// </summary>42/// <param name="security">Security.</param>43public MarketDepth(Security security)44{45Security = security ?? throw new ArgumentNullException(nameof(security));46}47
48QuoteChangeStates? IOrderBookMessage.State { get => null; set => throw new NotSupportedException(); }49
50private SecurityId? _securityId;51
52SecurityId ISecurityIdMessage.SecurityId53{54get => _securityId ??= Security?.Id.ToSecurityId() ?? default;55set => throw new NotSupportedException();56}57
58/// <summary>59/// Security.60/// </summary>61public Security Security { get; }62
63/// <summary>64/// Whether to use aggregated quotes <see cref="QuoteChange.InnerQuotes"/> at the join of the volumes with the same price.65/// </summary>66/// <remarks>67/// The default is disabled for performance.68/// </remarks>69public bool UseAggregatedQuotes { get; set; }70
71/// <inheritdoc/>72[Display(73ResourceType = typeof(LocalizedStrings),74Name = LocalizedStrings.ServerTimeKey,75Description = LocalizedStrings.ChangeServerTimeKey,76GroupName = LocalizedStrings.CommonKey,77Order = 2)]78public DateTimeOffset ServerTime { get; set; }79
80/// <summary>81/// Last change time.82/// </summary>83[Browsable(false)]84[Obsolete("Use ServerTime property.")]85public DateTimeOffset LastChangeTime86{87get => ServerTime;88set => ServerTime = value;89}90
91/// <inheritdoc/>92[Display(93ResourceType = typeof(LocalizedStrings),94Name = LocalizedStrings.LocalTimeKey,95Description = LocalizedStrings.LocalTimeDescKey,96GroupName = LocalizedStrings.CommonKey,97Order = 3)]98public DateTimeOffset LocalTime { get; set; }99
100/// <inheritdoc/>101public long SeqNum { get; set; }102
103/// <inheritdoc/>104public Messages.DataType BuildFrom { get; set; }105
106/// <summary>107/// Get the array of bids sorted by descending price. The first (best) bid will be the maximum price.108/// </summary>109[Obsolete("Use Bids property.")]110public QuoteChange[] Bids2 => Bids;111
112/// <summary>113/// Get the array of asks sorted by ascending price. The first (best) ask will be the minimum price.114/// </summary>115[Obsolete("Use Asks property.")]116public QuoteChange[] Asks2 => Asks;117
118private QuoteChange[] _bids = Array.Empty<QuoteChange>();119
120/// <inheritdoc/>121[Display(122ResourceType = typeof(LocalizedStrings),123Name = LocalizedStrings.BidsKey,124Description = LocalizedStrings.QuotesBuyKey,125GroupName = LocalizedStrings.CommonKey,126Order = 0)]127public QuoteChange[] Bids128{129get => _bids;130set => _bids = value ?? throw new ArgumentNullException(nameof(value));131}132
133private QuoteChange[] _asks = Array.Empty<QuoteChange>();134
135/// <inheritdoc/>136[Display(137ResourceType = typeof(LocalizedStrings),138Name = LocalizedStrings.AsksKey,139Description = LocalizedStrings.QuotesSellKey,140GroupName = LocalizedStrings.CommonKey,141Order = 1)]142public QuoteChange[] Asks143{144get => _asks;145set => _asks = value ?? throw new ArgumentNullException(nameof(value));146}147
148/// <summary>149/// Trading security currency.150/// </summary>151[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.CurrencyKey)]152public CurrencyTypes? Currency { get; set; }153
154/// <summary>155/// The best bid. If the order book does not contain bids, will be returned <see langword="null" />.156/// </summary>157[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.BestBidKey)]158public QuoteChange? BestBid2 { get; private set; }159
160/// <summary>161/// The best ask. If the order book does not contain asks, will be returned <see langword="null" />.162/// </summary>163[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.BestAskKey)]164public QuoteChange? BestAsk2 { get; private set; }165
166/// <summary>167/// The best pair. If the order book is empty, will be returned <see langword="null" />.168/// </summary>169[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.BestPairKey)]170public MarketDepthPair BestPair => GetPair(0);171
172/// <summary>173/// To get the total price size by bids.174/// </summary>175[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalBidsPriceKey)]176public decimal TotalBidsPrice => _bids.Length > 0 ? Security.ShrinkPrice(_bids.Sum(b => b.Price)) : 0;177
178/// <summary>179/// To get the total price size by offers.180/// </summary>181[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalAsksPriceKey)]182public decimal TotalAsksPrice => _asks.Length > 0 ? Security.ShrinkPrice(_asks.Sum(a => a.Price)) : 0;183
184/// <summary>185/// Get bids total volume.186/// </summary>187[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalBidsVolumeKey)]188public decimal TotalBidsVolume => _bids.Sum(b => b.Volume);189
190/// <summary>191/// Get asks total volume.192/// </summary>193[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalAsksVolumeKey)]194public decimal TotalAsksVolume => _asks.Sum(a => a.Volume);195
196/// <summary>197/// Get total volume.198/// </summary>199[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalVolumeKey)]200public decimal TotalVolume => TotalBidsVolume + TotalAsksVolume;201
202/// <summary>203/// To get the total price size.204/// </summary>205[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalPriceKey)]206public decimal TotalPrice => TotalBidsPrice + TotalAsksPrice;207
208/// <summary>209/// Total quotes count (bids + asks).210/// </summary>211[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.TotalQuotesCountKey)]212public int Count => _bids.Length + _asks.Length;213
214/// <summary>215/// Depth of book.216/// </summary>217[Display(ResourceType = typeof(LocalizedStrings), Name = LocalizedStrings.DepthOfBookKey)]218public int Depth { get; private set; }219
220/// <summary>221/// To reduce the order book to the required depth.222/// </summary>223/// <param name="newDepth">New order book depth.</param>224public void Decrease(int newDepth)225{226var currentDepth = Depth;227
228if (newDepth < 0)229throw new ArgumentOutOfRangeException(nameof(newDepth), newDepth, LocalizedStrings.InvalidValue);230else if (newDepth > currentDepth)231throw new ArgumentOutOfRangeException(nameof(newDepth), newDepth, LocalizedStrings.NewDepthCannotMoreCurrent.Put(currentDepth));232
233Bids = Decrease(_bids, newDepth);234Asks = Decrease(_asks, newDepth);235
236UpdateDepthAndTime();237}238
239private static QuoteChange[] Decrease(QuoteChange[] quotes, int newDepth)240{241if (quotes is null)242throw new ArgumentNullException(nameof(quotes));243
244if (newDepth <= quotes.Length)245Array.Resize(ref quotes, newDepth);246
247return quotes;248}249
250/// <summary>251/// To get a quote by the direction <see cref="Sides"/> and the depth index.252/// </summary>253/// <param name="orderDirection">Orders side.</param>254/// <param name="depthIndex">Depth index. Zero index means the best quote.</param>255/// <returns>Quote. If a quote does not exist for specified depth, then the <see langword="null" /> will be returned.</returns>256public QuoteChange? GetQuote(Sides orderDirection, int depthIndex)257{258return GetQuotesInternal(orderDirection).ElementAtOr(depthIndex);259}260
261/// <summary>262/// To get a quote by the price.263/// </summary>264/// <param name="price">Quote price.</param>265/// <returns>Found quote. If there is no quote in the order book for the passed price, then the <see langword="null" /> will be returned.</returns>266public QuoteChange? GetQuote(decimal price)267{268var quotes = GetQuotes(price);269var i = GetQuoteIndex(quotes, price);270return i < 0 ? default : quotes[i];271}272
273/// <summary>274/// To get quotes by the direction <see cref="Sides"/>.275/// </summary>276/// <param name="orderDirection">Orders side.</param>277/// <returns>Quotes.</returns>278public QuoteChange[] GetQuotes(Sides orderDirection)279{280return orderDirection == Sides.Buy ? Bids : Asks;281}282
283/// <summary>284/// To get the best quote by the direction <see cref="Sides"/>.285/// </summary>286/// <param name="orderDirection">Order side.</param>287/// <returns>The best quote. If the order book is empty, then the <see langword="null" /> will be returned.</returns>288public QuoteChange? GetBestQuote(Sides orderDirection)289{290return orderDirection == Sides.Buy ? BestBid2 : BestAsk2;291}292
293/// <summary>294/// To get a pair of quotes (bid + offer) by the depth index.295/// </summary>296/// <param name="depthIndex">Depth index. Zero index means the best pair of quotes.</param>297/// <returns>The pair of quotes. If the index is larger than book order depth <see cref="MarketDepth.Depth"/>, then the <see langword="null" /> is returned.</returns>298public MarketDepthPair GetPair(int depthIndex)299{300var (bid, ask) = Extensions.GetPair(this, depthIndex);301
302if (bid is null && ask is null)303return null;304
305return new MarketDepthPair(bid, ask);306}307
308/// <summary>309/// To get a pair of quotes for a given book depth.310/// </summary>311/// <param name="depth">Book depth. The counting is from the best quotes.</param>312/// <returns>Spread.</returns>313public IEnumerable<MarketDepthPair> GetTopPairs(int depth)314=> Extensions.GetTopPairs(this, depth).Select(t => new MarketDepthPair(t.bid, t.ask));315
316/// <summary>317/// To get quotes for a given book depth.318/// </summary>319/// <param name="depth">Book depth. Quotes are in order of price increasing from bids to offers.</param>320/// <returns>Spread.</returns>321public IEnumerable<QuoteChange> GetTopQuotes(int depth)322=> Extensions.GetTopQuotes(this, depth);323
324/// <summary>325/// To update the order book. The version without checks and blockings.326/// </summary>327/// <param name="bids">Sorted bids.</param>328/// <param name="asks">Sorted asks.</param>329/// <param name="lastChangeTime">Change time.</param>330/// <returns>Market depth.</returns>331public MarketDepth Update(QuoteChange[] bids, QuoteChange[] asks, DateTimeOffset lastChangeTime)332{333if (bids is null)334throw new ArgumentNullException(nameof(bids));335
336if (asks is null)337throw new ArgumentNullException(nameof(asks));338
339_bids = bids.ToArray();340_asks = asks.ToArray();341
342UpdateDepthAndTime(lastChangeTime);343
344return this;345}346
347/// <summary>348/// To refresh the quote. If a quote with the same price is already in the order book, it is updated as passed. Otherwise, it automatically rebuilds the order book.349/// </summary>350/// <param name="quote">The new quote.</param>351/// <param name="side">Side.</param>352public void UpdateQuote(QuoteChange quote, Sides side)353{354SetQuote(quote, side, false);355}356
357/// <summary>358/// Add buy quote.359/// </summary>360/// <param name="price">Buy price.</param>361/// <param name="volume">Buy volume.</param>362public void AddBid(decimal price, decimal volume)363{364AddQuote(new QuoteChange365{366Price = price,367Volume = volume,368}, Sides.Buy);369}370
371/// <summary>372/// Add sell quote.373/// </summary>374/// <param name="price">Sell price.</param>375/// <param name="volume">Sell volume.</param>376public void AddAsk(decimal price, decimal volume)377{378AddQuote(new QuoteChange379{380Price = price,381Volume = volume,382}, Sides.Sell);383}384
385/// <summary>386/// To add the quote. If a quote with the same price is already in the order book, they are combined into the <see cref="QuoteChange.InnerQuotes"/>.387/// </summary>388/// <param name="quote">The new quote.</param>389/// <param name="side">Side.</param>390public void AddQuote(QuoteChange quote, Sides side)391{392SetQuote(quote, side, true);393}394
395private void SetQuote(QuoteChange quote, Sides side, bool isAggregate)396{397//CheckQuote(quote);398
399//Quote outOfDepthQuote = null;400
401//lock (_syncRoot)402//{403var quotes = GetQuotes(side);404
405var index = GetQuoteIndex(quotes, quote.Price);406
407if (index != -1)408{409if (isAggregate)410{411var existedQuote = quotes[index];412
413//if (UseAggregatedQuotes)414//{415// if (existedQuote is not AggregatedQuote aggQuote)416// {417// aggQuote = new AggregatedQuote418// {419// Price = quote.Price,420// Security = quote.Security,421// OrderDirection = quote.OrderDirection422// };423
424// aggQuote.InnerQuotes.Add(existedQuote);425
426// quotes[index] = aggQuote;427// }428
429// aggQuote.InnerQuotes.Add(quote);430//}431//else432existedQuote.Volume += quote.Volume;433}434else435{436quotes[index] = quote;437}438}439else440{441for (index = 0; index < quotes.Length; index++)442{443var currentPrice = quotes[index].Price;444
445if (side == Sides.Buy)446{447if (quote.Price > currentPrice)448break;449}450else451{452if (quote.Price < currentPrice)453break;454}455}456
457Array.Resize(ref quotes, quotes.Length + 1);458
459if (index < (quotes.Length - 1))460Array.Copy(quotes, index, quotes, index + 1, quotes.Length - 1 - index);461
462quotes[index] = quote;463
464//if (quotes.Length > MaxDepth)465//{466// outOfDepthQuote = quotes[quotes.Length - 1];467// quotes = RemoveAt(quotes, quotes.Length - 1);468//}469
470if (side == Sides.Buy)471Bids = quotes;472else473Asks = quotes;474}475
476UpdateDepthAndTime();477//}478
479//if (outOfDepthQuote != null)480// QuoteOutOfDepth?.Invoke(outOfDepthQuote);481}482
483#region IEnumerable<QuoteChange>484
485/// <summary>486/// To get the enumerator object.487/// </summary>488/// <returns>The enumerator object.</returns>489public IEnumerator<QuoteChange> GetEnumerator()490{491return Bids.Reverse().Concat(Asks).Cast<QuoteChange>().GetEnumerator();492}493
494/// <summary>495/// To get the enumerator object.496/// </summary>497/// <returns>The enumerator object.</returns>498IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();499
500#endregion501
502/// <summary>503/// To get all pairs from the order book.504/// </summary>505/// <returns>Pairs from which the order book is composed.</returns>506public IEnumerable<MarketDepthPair> ToPairs()507{508return GetTopPairs(Depth);509}510
511/// <summary>512/// Remove the volume for the price.513/// </summary>514/// <param name="price">Remove the quote for the price.</param>515/// <param name="volume">The volume to be deleted. If it is not specified, then all the quote is removed.</param>516/// <param name="lastChangeTime">Order book change time.</param>517public void Remove(decimal price, decimal volume = 0, DateTimeOffset lastChangeTime = default)518{519var dir = GetDirection(price) ?? throw new ArgumentOutOfRangeException(nameof(price), price, LocalizedStrings.QuotePriceNotSpecified);520
521Remove(dir, price, volume, lastChangeTime);522}523
524/// <summary>525/// Remove the volume for the price.526/// </summary>527/// <param name="direction">Order side.</param>528/// <param name="price">Remove the quote for the price.</param>529/// <param name="volume">The volume to be deleted. If it is not specified, then all the quote is removed.</param>530/// <param name="lastChangeTime">Order book change time.</param>531public void Remove(Sides direction, decimal price, decimal volume = 0, DateTimeOffset lastChangeTime = default)532{533if (price <= 0)534throw new ArgumentOutOfRangeException(nameof(price), price, LocalizedStrings.InvalidValue);535
536if (volume < 0)537throw new ArgumentOutOfRangeException(nameof(volume), volume, LocalizedStrings.InvalidValue);538
539var quotes = GetQuotesInternal(direction);540var index = GetQuoteIndex(quotes, price);541
542if (index == -1)543throw new ArgumentOutOfRangeException(nameof(price), price, LocalizedStrings.QuotePriceNotSpecified);544
545var quote = quotes[index];546
547decimal leftVolume;548
549if (volume > 0)550{551if (quote.Volume < volume)552throw new ArgumentOutOfRangeException(nameof(volume), volume, LocalizedStrings.VolumeLessThanRequired.Put(quote));553
554leftVolume = quote.Volume - volume;555
556//if (UseAggregatedQuotes)557//{558// if (quote is AggregatedQuote aggQuote)559// {560// while (volume > 0)561// {562// var innerQuote = aggQuote.InnerQuotes.First();563
564// if (innerQuote.Volume > volume)565// {566// innerQuote.Volume -= volume;567// break;568// }569// else570// {571// aggQuote.InnerQuotes.Remove(innerQuote);572// volume -= innerQuote.Volume;573// }574// }575// }576//}577}578else579leftVolume = 0;580
581if (leftVolume == 0)582{583quotes = RemoveAt(quotes, index);584
585if (direction == Sides.Buy)586Bids = quotes;587else588Asks = quotes;589
590UpdateDepthAndTime(lastChangeTime);591}592else593{594quote.Volume = leftVolume;595UpdateTime(lastChangeTime);596}597}598
599private static QuoteChange[] RemoveAt(QuoteChange[] quotes, int index)600{601var newQuotes = new QuoteChange[quotes.Length - 1];602
603if (index > 0)604Array.Copy(quotes, 0, newQuotes, 0, index);605
606if (index < (quotes.Length - 1))607Array.Copy(quotes, index + 1, newQuotes, index, quotes.Length - index - 1);608
609return newQuotes;610}611
612private static int GetQuoteIndex(QuoteChange[] quotes, decimal price)613{614var stop = quotes.Length - 1;615if (stop < 0)616return -1;617
618var first = quotes[0];619
620var cmp = decimal.Compare(price, first.Price);621if (cmp == 0)622return 0;623
624var last = quotes[stop];625var desc = first.Price - last.Price > 0m;626
627if (desc)628cmp = -cmp;629
630if (cmp < 0)631return -1;632
633cmp = decimal.Compare(price, last.Price);634
635if (desc)636cmp = -cmp;637
638if (cmp > 0)639return -1;640
641if (cmp == 0)642return stop;643
644var start = 0;645
646while (stop - start >= 0)647{648var mid = (start + stop) >> 1;649
650cmp = decimal.Compare(price, quotes[mid].Price);651
652if (desc)653cmp = -cmp;654if (cmp > 0)655start = mid + 1;656else if (cmp < 0)657stop = mid - 1;658else659return mid;660}661
662return -1;663}664
665private QuoteChange[] GetQuotesInternal(Sides direction)666{667return direction == Sides.Buy ? _bids : _asks;668}669
670private QuoteChange[] GetQuotes(decimal price)671{672var dir = GetDirection(price);673
674if (dir == null)675return Array.Empty<QuoteChange>();676else677return dir == Sides.Buy ? _bids : _asks;678}679
680private Sides? GetDirection(decimal price)681{682if (BestBid2 != null && BestBid2.Value.Price >= price)683return Sides.Buy;684else if (BestAsk2 != null && BestAsk2.Value.Price <= price)685return Sides.Sell;686else687return null;688}689
690private void UpdateDepthAndTime(DateTimeOffset lastChangeTime = default)691{692Depth = _bids.Length > _asks.Length ? _bids.Length : _asks.Length;693
694BestBid2 = _bids.Length > 0 ? _bids[0] : null;695BestAsk2 = _asks.Length > 0 ? _asks[0] : null;696
697UpdateTime(lastChangeTime);698}699
700private void UpdateTime(DateTimeOffset lastChangeTime)701{702if (lastChangeTime != default)703{704ServerTime = lastChangeTime;705}706}707
708/// <summary>709/// Create a copy of <see cref="MarketDepth"/>.710/// </summary>711/// <returns>Copy.</returns>712public override MarketDepth Clone()713{714return new(Security)715{716//MaxDepth = MaxDepth,717//UseAggregatedQuotes = UseAggregatedQuotes,718//AutoVerify = AutoVerify,719Currency = Currency,720LocalTime = LocalTime,721ServerTime = ServerTime,722_bids = _bids.ToArray(),723_asks = _asks.ToArray(),724SeqNum = SeqNum,725BuildFrom = BuildFrom,726};727}728
729/// <inheritdoc />730public override string ToString()731{732return this.Select(q => q.ToString()).JoinNL();733}734}735}