Skip to content

Commit e8cc572

Browse files
author
Ariel Silahian
committed
- Ability to compute delta changes in orderbook
- Compute zero sizes based on decimal places
1 parent 8562671 commit e8cc572

File tree

1 file changed

+171
-1
lines changed

1 file changed

+171
-1
lines changed

VisualHFT.Commons/Model/OrderBook.cs

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using VisualHFT.Commons.Model;
22
using VisualHFT.Commons.Pools;
3+
using VisualHFT.Enums;
34
using VisualHFT.Helpers;
45
using VisualHFT.Studies;
5-
using VisualHFT.Enums;
66

77
namespace VisualHFT.Model
88
{
@@ -404,6 +404,13 @@ public virtual void AddOrUpdateLevel(DeltaBookItem item)
404404
{
405405
if (!item.IsBid.HasValue)
406406
return;
407+
408+
409+
if (item.Size.HasValue && IsZeroAtDp(item.Size.Value, this.SizeDecimalPlaces))
410+
{
411+
DeleteLevel(item); // explicit remove, not an update to zero
412+
return;
413+
}
407414
eMDUpdateAction eAction = eMDUpdateAction.None;
408415

409416
lock (_data.Lock)
@@ -440,6 +447,13 @@ public virtual void AddLevel(DeltaBookItem item)
440447
{
441448
if (!item.IsBid.HasValue)
442449
return;
450+
if (item.Size.HasValue && IsZeroAtDp(item.Size.Value, this.SizeDecimalPlaces))
451+
{
452+
DeleteLevel(item);
453+
return;
454+
}
455+
// quantize what we store so internals never carry float dust
456+
item.Size = QuantizeToDp(item.Size.Value, this.SizeDecimalPlaces);
443457

444458
lock (_data.Lock)
445459
{
@@ -498,6 +512,14 @@ public virtual void AddLevel(DeltaBookItem item)
498512

499513
public virtual void UpdateLevel(DeltaBookItem item)
500514
{
515+
if (item.Size.HasValue && IsZeroAtDp(item.Size.Value, this.SizeDecimalPlaces))
516+
{
517+
DeleteLevel(item);
518+
return;
519+
}
520+
// quantize what we store so internals never carry float dust
521+
item.Size = QuantizeToDp(item.Size.Value, this.SizeDecimalPlaces);
522+
501523
lock (_data.Lock)
502524
{
503525
(item.IsBid.HasValue && item.IsBid.Value ? _data.Bids : _data.Asks).Update(x => x.Price == item.Price,
@@ -554,6 +576,154 @@ public virtual void DeleteLevel(DeltaBookItem item)
554576
return result;
555577
}
556578

579+
580+
581+
582+
583+
584+
585+
/// <summary>
586+
/// Computes the delta needed to transform THIS order book into <paramref name="other"/>,
587+
/// emitting one <see cref="DeltaBookItem"/> per changed price level (absolute size; 0 = delete).
588+
/// Performance: O(N+M) per side; allocation-free (uses pooled DeltaBookItem).
589+
/// IMPORTANT: <paramref name="onDelta"/> MUST consume the item synchronously and copy its data.
590+
/// This method RETURNS the pooled item immediately after invoking the callback.
591+
/// </summary>
592+
/// <param name="other">The newer snapshot for the same venue/symbol.</param>
593+
/// <param name="onDelta">
594+
/// Callback that receives one pooled DeltaBookItem per change. Do not retain the reference.
595+
/// Typical usage: obLocal.AddOrUpdateLevel(delta); // which copies fields synchronously
596+
/// </param>
597+
public void ComputeDeltaAgainst(OrderBook other, Action<DeltaBookItem> onDelta)
598+
{
599+
if (other == null) throw new ArgumentNullException(nameof(other));
600+
if (onDelta == null) throw new ArgumentNullException(nameof(onDelta));
601+
602+
// Lock both books in a deterministic order to avoid deadlocks when called from multiple modules.
603+
var lockA = _data.Lock;
604+
var lockB = other._data.Lock;
605+
int ha = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(lockA);
606+
int hb = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(lockB);
607+
608+
if (ha <= hb)
609+
{
610+
lock (lockA) lock (lockB) { DiffBothSides_NoAlloc_(this, other, onDelta); }
611+
}
612+
else
613+
{
614+
lock (lockB) lock (lockA) { DiffBothSides_NoAlloc_(this, other, onDelta); }
615+
}
616+
}
617+
618+
619+
private static void DiffBothSides_NoAlloc_(OrderBook oldBook, OrderBook newBook, Action<DeltaBookItem> emit)
620+
{
621+
// Access underlying storage directly to avoid MaxDepth filtering and extra wrappers.
622+
var oldBids = oldBook._data.Bids;
623+
var newBids = newBook._data.Bids;
624+
var oldAsks = oldBook._data.Asks;
625+
var newAsks = newBook._data.Asks;
626+
627+
DiffSide_NoAlloc_(oldBids, newBids, /*isBid*/ true, emit);
628+
DiffSide_NoAlloc_(oldAsks, newAsks, /*isBid*/ false, emit);
629+
}
630+
631+
private static void DiffSide_NoAlloc_(
632+
CachedCollection<BookItem> oldSide,
633+
CachedCollection<BookItem> newSide,
634+
bool isBid,
635+
Action<DeltaBookItem> emit)
636+
{
637+
int i = 0, j = 0;
638+
int oldCount = oldSide == null ? 0 : oldSide.Count();
639+
int newCount = newSide == null ? 0 : newSide.Count();
640+
641+
// Local function to emit a pooled delta and immediately return it to the pool.
642+
static void Emit(bool isBidL, double price, double size, Action<DeltaBookItem> sink)
643+
{
644+
var d = DeltaBookItemPool.Get();
645+
d.IsBid = isBidL;
646+
d.Price = price;
647+
d.Size = size;
648+
d.LocalTimeStamp = DateTime.UtcNow; // caller may overwrite if needed
649+
d.ServerTimeStamp = d.LocalTimeStamp;
650+
sink(d);
651+
DeltaBookItemPool.Return(d); // safe because consumer must copy synchronously
652+
}
653+
654+
// Merge walk
655+
while (i < oldCount && j < newCount)
656+
{
657+
var o = oldSide[i];
658+
var n = newSide[j];
659+
660+
// Assuming exact price equality as elsewhere in the codebase (AddOrUpdateLevel uses ==).
661+
double op = o.Price.GetValueOrDefault();
662+
double np = n.Price.GetValueOrDefault();
663+
664+
if (op == np)
665+
{
666+
// Same price level: update only if size changed
667+
double os = o.Size.GetValueOrDefault();
668+
double ns = n.Size.GetValueOrDefault();
669+
if (os != ns)
670+
Emit(isBid, np, ns, emit);
671+
672+
i++; j++;
673+
}
674+
else
675+
{
676+
// Determine ordering based on side sort (bids: desc, asks: asc)
677+
bool oldComesFirst = isBid ? (op > np) : (op < np);
678+
679+
if (oldComesFirst)
680+
{
681+
// Level present in old but not (at this position) in new => removed (size -> 0)
682+
Emit(isBid, op, 0.0, emit);
683+
i++;
684+
}
685+
else
686+
{
687+
// Level present in new but not in old => added (size -> ns)
688+
Emit(isBid, np, n.Size.GetValueOrDefault(), emit);
689+
j++;
690+
}
691+
}
692+
}
693+
694+
// Any remaining old levels are deletions
695+
while (i < oldCount)
696+
{
697+
var o = oldSide[i++];
698+
double op = o.Price.GetValueOrDefault();
699+
if (op != 0.0) Emit(isBid, op, 0.0, emit);
700+
}
701+
702+
// Any remaining new levels are additions
703+
while (j < newCount)
704+
{
705+
var n = newSide[j++];
706+
double np = n.Price.GetValueOrDefault();
707+
if (np != 0.0) Emit(isBid, np, n.Size.GetValueOrDefault(), emit);
708+
}
709+
}
710+
711+
712+
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
713+
private static double QuantizeToDp(double size, int dp)
714+
{
715+
return Math.Round(size, dp, MidpointRounding.ToZero);
716+
}
717+
718+
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
719+
private static bool IsZeroAtDp(double size, int dp)
720+
{
721+
// Anything that quantizes to 0 at this precision is treated as zero.
722+
return QuantizeToDp(size, dp) == 0.0;
723+
}
724+
725+
726+
557727
protected virtual void Dispose(bool disposing)
558728
{
559729
if (!_disposed)

0 commit comments

Comments
 (0)