Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class Module:
ENCRYPTLY_BINARIES = {
"linux-x64": ENCRYPTLY_DIR / "linux-x64" / "encryptly",
"linux-arm64": ENCRYPTLY_DIR / "linux-arm64" / "encryptly",
"macos-x64": ENCRYPTLY_DIR / "macos-x64" / "encryptly",
"macos-arm64": ENCRYPTLY_DIR / "macos-arm64" / "encryptly",
"windows-x64": ENCRYPTLY_DIR / "windows-x64" / "encryptly.exe",
"windows-arm64": ENCRYPTLY_DIR / "windows-arm64" / "encryptly.exe",
Expand Down
86 changes: 86 additions & 0 deletions diagnostic/build-2b54872c.json

Large diffs are not rendered by default.

Binary file added diagnostic/build-2b54872c.logd
Binary file not shown.
225 changes: 217 additions & 8 deletions market/orderbook/orderbook.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package orderbook

import (
"fmt"
"sort"
"strings"
"sync"
"time"

Expand All @@ -28,6 +30,22 @@ type OrderBook struct {
closed bool
}

type DeltaSide string

const (
DeltaBid DeltaSide = "bid"
DeltaAsk DeltaSide = "ask"
)

type Delta struct {
Symbol types.Symbol
Sequence uint64
Side DeltaSide
Price decimal.Decimal
Quantity decimal.Decimal
Checksum string
}

func NewOrderBook(symbol types.Symbol, config Config) *OrderBook {
return &OrderBook{
symbol: symbol,
Expand All @@ -39,6 +57,131 @@ func NewOrderBook(symbol types.Symbol, config Config) *OrderBook {
}
}

func (ob *OrderBook) Sequence() uint64 {
ob.mu.RLock()
defer ob.mu.RUnlock()
return ob.sequence
}

func (ob *OrderBook) ApplySnapshot(snapshot *types.DepthUpdate, sequence uint64) error {
if snapshot == nil {
return ErrInvalidSnapshot
}
if snapshot.Symbol != ob.symbol {
return ErrInvalidSymbol
}

bids := make([]*types.Level, 0, len(snapshot.Bids))
for _, level := range snapshot.Bids {
if err := validateLevel(level.Price, level.Quantity); err != nil {
return err
}
copy := level
bids = append(bids, &copy)
}

asks := make([]*types.Level, 0, len(snapshot.Asks))
for _, level := range snapshot.Asks {
if err := validateLevel(level.Price, level.Quantity); err != nil {
return err
}
copy := level
asks = append(asks, &copy)
}

sortLevels(bids, true)
sortLevels(asks, false)

ob.mu.Lock()
defer ob.mu.Unlock()

if ob.closed {
return ErrBookClosed
}

ob.bids = trimDepth(bids, ob.config.MaxDepth)
ob.asks = trimDepth(asks, ob.config.MaxDepth)
ob.sequence = sequence
ob.updatedAt = time.Now()
return nil
}

func (ob *OrderBook) ApplyDelta(delta Delta) error {
if delta.Symbol != ob.symbol {
return ErrInvalidSymbol
}
if delta.Sequence == 0 {
return ErrInvalidSequence
}
if delta.Side != DeltaBid && delta.Side != DeltaAsk {
return ErrInvalidSide
}
if delta.Price.LessThanOrEqual(decimal.Zero) {
return ErrInvalidPrice
}
if delta.Quantity.IsNegative() {
return ErrInvalidQuantity
}

ob.mu.Lock()
defer ob.mu.Unlock()

if ob.closed {
return ErrBookClosed
}
if delta.Sequence <= ob.sequence {
return ErrStaleDelta
}

previousBids := cloneLevels(ob.bids)
previousAsks := cloneLevels(ob.asks)
previousSequence := ob.sequence

if delta.Side == DeltaBid {
ob.bids = applyLevelDelta(ob.bids, delta.Price, delta.Quantity, true, ob.config.MaxDepth)
} else {
ob.asks = applyLevelDelta(ob.asks, delta.Price, delta.Quantity, false, ob.config.MaxDepth)
}
ob.sequence = delta.Sequence

if delta.Checksum != "" && delta.Checksum != checksumForLevels(ob.bids, ob.asks) {
ob.bids = previousBids
ob.asks = previousAsks
ob.sequence = previousSequence
return ErrChecksumMismatch
}

ob.updatedAt = time.Now()
return nil
}

func (ob *OrderBook) Checksum() string {
ob.mu.RLock()
defer ob.mu.RUnlock()
return checksumForLevels(ob.bids, ob.asks)
}

func checksumForLevels(bids []*types.Level, asks []*types.Level) string {
var builder strings.Builder
writeLevels := func(prefix string, levels []*types.Level) {
for _, level := range levels {
if level == nil {
continue
}
fmt.Fprintf(
&builder,
"%s:%s:%s;",
prefix,
level.Price.String(),
level.Quantity.String(),
)
}
}
writeLevels("b", bids)
writeLevels("a", asks)
return builder.String()
}

func (ob *OrderBook) AddOrder(order *types.Order) ([]*types.Trade, error) {
ob.mu.Lock()
defer ob.mu.Unlock()
Expand Down Expand Up @@ -155,8 +298,16 @@ func (ob *OrderBook) Close() {
}

var (
ErrBookClosed = &BookError{"order book is closed"}
ErrOrderNotFound = &BookError{"order not found"}
ErrBookClosed = &BookError{"order book is closed"}
ErrOrderNotFound = &BookError{"order not found"}
ErrInvalidSnapshot = &BookError{"invalid snapshot"}
ErrInvalidSymbol = &BookError{"invalid symbol"}
ErrInvalidSequence = &BookError{"invalid sequence"}
ErrInvalidSide = &BookError{"invalid side"}
ErrInvalidPrice = &BookError{"invalid price"}
ErrInvalidQuantity = &BookError{"invalid quantity"}
ErrStaleDelta = &BookError{"stale or out-of-order delta"}
ErrChecksumMismatch = &BookError{"delta checksum mismatch"}
)

type BookError struct {
Expand All @@ -169,12 +320,7 @@ func (e *BookError) Error() string {

func insertLevel(levels []*types.Level, level *types.Level, desc bool) []*types.Level {
levels = append(levels, level)
sort.Slice(levels, func(i, j int) bool {
if desc {
return levels[i].Price.GreaterThan(levels[j].Price)
}
return levels[i].Price.LessThan(levels[j].Price)
})
sortLevels(levels, desc)
return levels
}

Expand All @@ -186,3 +332,66 @@ func removeLevel(levels []*types.Level, price decimal.Decimal) []*types.Level {
}
return levels
}

func validateLevel(price decimal.Decimal, quantity decimal.Decimal) error {
if price.LessThanOrEqual(decimal.Zero) {
return ErrInvalidPrice
}
if quantity.IsNegative() {
return ErrInvalidQuantity
}
return nil
}

func cloneLevels(levels []*types.Level) []*types.Level {
result := make([]*types.Level, 0, len(levels))
for _, level := range levels {
if level == nil {
continue
}
copy := *level
result = append(result, &copy)
}
return result
}

func applyLevelDelta(levels []*types.Level, price decimal.Decimal, quantity decimal.Decimal, desc bool, maxDepth int) []*types.Level {
for i, level := range levels {
if level.Price.Equal(price) {
if quantity.IsZero() {
return append(levels[:i], levels[i+1:]...)
}
level.Quantity = quantity
sortLevels(levels, desc)
return trimDepth(levels, maxDepth)
}
}

if quantity.IsZero() {
return levels
}

levels = append(levels, &types.Level{
Price: price,
Quantity: quantity,
Count: 1,
})
sortLevels(levels, desc)
return trimDepth(levels, maxDepth)
}

func sortLevels(levels []*types.Level, desc bool) {
sort.Slice(levels, func(i, j int) bool {
if desc {
return levels[i].Price.GreaterThan(levels[j].Price)
}
return levels[i].Price.LessThan(levels[j].Price)
})
}

func trimDepth(levels []*types.Level, maxDepth int) []*types.Level {
if maxDepth <= 0 || len(levels) <= maxDepth {
return levels
}
return levels[:maxDepth]
}
Loading