diff --git a/README.md b/README.md index 88d6db769b..e6225f2017 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,9 @@ dcrdex │ │ ├── admin # administrative tools and portal (may need RPC server too) │ │ └── controller # controller for multiple markets, users, api, comms, etc. │ ├── docs +│ ├── htttpapi # HTTP API │ ├── market # market manager +│ │ └── order # the ubiquitous order type │ ├── matcher # order matching engine │ └── swap # the swap executor/coordinator └── spec diff --git a/server/account/account.go b/server/account/account.go new file mode 100644 index 0000000000..61a0850efc --- /dev/null +++ b/server/account/account.go @@ -0,0 +1,20 @@ +package account + +import ( + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrdex/server/account/pki" +) + +var HashFunc = blake256.Sum256 + +const ( + HashSize = blake256.Size +) + +type AccountID [HashSize]byte + +func New(pk [pki.PubKeySize]byte) AccountID { + // hash the pubkey hash(hash(pubkey)) + h := HashFunc(pk[:]) + return HashFunc(h[:]) +} diff --git a/server/account/go.mod b/server/account/go.mod new file mode 100644 index 0000000000..bd64e09da0 --- /dev/null +++ b/server/account/go.mod @@ -0,0 +1,8 @@ +module github.com/decred/dcrdex/server/account + +go 1.12 + +require ( + github.com/decred/dcrd/crypto/blake256 v1.0.0 + github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 +) diff --git a/server/account/go.sum b/server/account/go.sum new file mode 100644 index 0000000000..a347c22350 --- /dev/null +++ b/server/account/go.sum @@ -0,0 +1,8 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1/go.mod h1:OVfvaOsNLS/A1y4Eod0Ip/Lf8qga7VXCQjUQLbkY0Go= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 h1:awk7sYJ4pGWmtkiGHFfctztJjHMKGLV8jctGQhAbKe0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2/go.mod h1:CHTUIVfmDDd0KFVFpNX1pFVCBUegxW387nN0IGwNKR0= +github.com/decred/dcrdex v0.0.0-20190820222222-0c633a45b26a h1:IkTp1kie2MKG5zf4nlcVsP9UXPghXUBLa8JB+T538UQ= diff --git a/server/account/pki/pki.go b/server/account/pki/pki.go new file mode 100644 index 0000000000..b0d49d360d --- /dev/null +++ b/server/account/pki/pki.go @@ -0,0 +1,10 @@ +package pki + +import "github.com/decred/dcrd/dcrec/secp256k1" + +type PrivateKey = secp256k1.PrivateKey + +const ( + PrivKeySize = secp256k1.PrivKeyBytesLen + PubKeySize = secp256k1.PubKeyBytesLenCompressed +) diff --git a/server/market/epoch.go b/server/market/epoch.go new file mode 100644 index 0000000000..6c66a1817f --- /dev/null +++ b/server/market/epoch.go @@ -0,0 +1,9 @@ +package market + +import "github.com/decred/dcrdex/server/market/order" + +// TODO. PLACEHOLDER. + +type EpochQueue struct { + Orders []order.Order +} diff --git a/server/market/go.mod b/server/market/go.mod new file mode 100644 index 0000000000..8499a7f89f --- /dev/null +++ b/server/market/go.mod @@ -0,0 +1,10 @@ +module github.com/decred/dcrdex/server/market + +go 1.12 + +replace github.com/decred/dcrdex/server/account => ../account + +require ( + github.com/decred/dcrd/crypto/blake256 v1.0.0 + github.com/decred/dcrdex/server/account v0.0.0-00010101000000-000000000000 +) diff --git a/server/market/go.sum b/server/market/go.sum new file mode 100644 index 0000000000..eee0ef35ce --- /dev/null +++ b/server/market/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/decred/dcrd v1.3.0 h1:EEXm7BdiROfazDtuFsOu9mfotnyy00bgCuVwUqaszFo= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1/go.mod h1:OVfvaOsNLS/A1y4Eod0Ip/Lf8qga7VXCQjUQLbkY0Go= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 h1:awk7sYJ4pGWmtkiGHFfctztJjHMKGLV8jctGQhAbKe0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2/go.mod h1:CHTUIVfmDDd0KFVFpNX1pFVCBUegxW387nN0IGwNKR0= +github.com/decred/dcrdex v0.0.0-20190820222222-0c633a45b26a h1:IkTp1kie2MKG5zf4nlcVsP9UXPghXUBLa8JB+T538UQ= diff --git a/server/market/market.go b/server/market/market.go new file mode 100644 index 0000000000..6499048cf3 --- /dev/null +++ b/server/market/market.go @@ -0,0 +1,32 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package market + +// The Market[Manager] will: +// - Cycle the epochs. Epoch timing, the current epoch index, etc. +// - Manage multiple epochs (one active and others in various states of +// matching, swapping, or archival). +// - Receiving and validating new order data (amounts vs. lot size, check fees, +// utxos, sufficient market buy buffer, etc. with help from asset backends). +// - Putting incoming orders into the current epoch queue, which must implement +// matcher.Booker so that the order matching engine can work with it. +// - Possess an order book manager, which must also implement matcher.Booker. +// - Initiate order matching via matcher.Match(book, currentQueue) +// - During and/or after matching: +// * update the book (remove orders, add new standing orders, etc.) +// * retire/archive the epoch queue +// * publish the matches (and order book changes?) +// * initiate swaps for each match (possibly groups of related matches) +// - Continually update the order book based on data from the swap executors +// (e.g. failed swaps, partial fills?, etc.), communications hub (i.e. dropped +// clients), and other sources. +// - Recording all events with the archivist + +// The Market manager should not be overly involved with details of accounts and +// authentication. Via the account package it should request account status with +// new orders, verification of order signatures. The Market should also perform +// various account package callbacks such as order status updates so that the +// account package code can keep various data up-to-date, including order +// status, history, cancellation statistics, etc. +type Market struct{} // TODO diff --git a/server/market/order/match.go b/server/market/order/match.go new file mode 100644 index 0000000000..d797b502e4 --- /dev/null +++ b/server/market/order/match.go @@ -0,0 +1,19 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package order + +// Match represents the result of matching a single Taker order from the epoch +// queue with one or more standing limit orders from the book, the Makers. The +// Amounts and Rates of each limit order paired with the taker order are stored. +// The Rates slice is for convenience as each rate must match with the Maker's +// rates. However, one of the Amounts may be less than the full quantity of the +// corresponding limit order, indicating a partial fill of the Maker. The sum of +// the amounts, Total, is provided for convenience. +type Match struct { + Taker Order + Makers []*LimitOrder + Amounts []uint64 + Rates []uint64 + Total uint64 +} diff --git a/server/market/order/order.go b/server/market/order/order.go new file mode 100644 index 0000000000..a685a31875 --- /dev/null +++ b/server/market/order/order.go @@ -0,0 +1,339 @@ +// Copyright (c) 2019, The Decred developers +// See LICENSE for details. + +// Package order defines the Order and Match types used throughout the DEX. +package order + +import ( + "encoding/binary" + "encoding/hex" + "time" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrdex/server/account" +) + +// OrderIDSize defines the length in bytes of an OrderID. +const OrderIDSize = blake256.Size + +// OrderID is the unique identifier for each order. +type OrderID [OrderIDSize]byte + +// String returns a hexadecimal representation of the OrderID. String implements +// fmt.Stringer. +func (oid OrderID) String() string { + return hex.EncodeToString(oid[:]) +} + +// OrderType distinguishes the different kinds of orders (e.g. limit, market, +// cancel). +type OrderType uint8 + +// The different OrderType values. +const ( + LimitOrderType OrderType = iota + MarketOrderType + CancelOrderType +) + +// TimeInForce indicates how limit order execution is to be handled. That is, +// when the order is not immediately matched during processing of the order's +// epoch, the order may become a standing order or be revoked without a fill. +type TimeInForce uint8 + +// The TimeInForce is either ImmediateTiF, which prevents the order from +// becoming a standing order if there is no match during epoch processing, or +// StandingTiF, which allows limit orders to enter the order book if not +// immediately matched during epoch processing. +const ( + ImmediateTiF TimeInForce = iota + StandingTiF +) + +// Order specifies the methods required for a type to function as a DEX order. +// See the concrete implementations of MarketOrder, LimitOrder, and CancelOrder. +type Order interface { + // ID computes the Order's ID from its serialization. Serialization is + // detailed in the 'Client Order Management' section of the DEX + // specification. + ID() OrderID + + // UID gives the string representation of the order ID. It is named to + // reflect the intent of providing a unique identifier. + UID() string + + // Serialize marshals the order. Serialization is detailed in the 'Client + // Order Management' section of the DEX specification. + Serialize() []byte + + // SerializeSize gives the length of the serialized order in bytes. + SerializeSize() int + + // Type indicates the Order's type (e.g. LimitOrder, MarketOrder, etc.). + Type() OrderType + + // Time returns the Order's server time, when it was received by the server. + Time() int64 + + // Remaining computes the unfilled amount of the order. + Remaining() uint64 +} + +// An order's ID is computed as the Blake-256 hash of the serialized order. +func calcOrderID(order Order) OrderID { + return blake256.Sum256(order.Serialize()) +} + +// UTXO is the interface required to be satisfied by any asset's implementation +// of a UTXO type. +type UTXO interface { + TxHash() []byte + Vout() uint32 +} + +func utxoSize(u UTXO) int { + return len(u.TxHash()) + 4 +} + +func serializeUTXO(u UTXO) []byte { + b := make([]byte, utxoSize(u)) + hash := u.TxHash() + hashLen := len(hash) + copy(b, hash) + binary.LittleEndian.PutUint32(b[hashLen:], u.Vout()) + return b +} + +// Prefix is the order prefix containing data fields common to all orders. +type Prefix struct { + AccountID account.AccountID + BaseAsset uint32 + QuoteAsset uint32 + OrderType OrderType + ClientTime time.Time + ServerTime time.Time +} + +// PrefixLen is the length in bytes of the serialized order Prefix. +const PrefixLen = account.HashSize + 4 + 4 + 1 + 8 + 8 + +// SerializeSize returns the length of the serialized order Prefix. +func (p *Prefix) SerializeSize() int { + return PrefixLen +} + +// Time returns the order prefix's server time as a UNIX epoch time. +func (p *Prefix) Time() int64 { + return p.ServerTime.Unix() +} + +// Serialize marshals the Prefix into a []byte. +// TODO: Deserialize. +func (p *Prefix) Serialize() []byte { + b := make([]byte, PrefixLen) + + // account ID + offset := len(p.AccountID) + copy(b[:offset], p.AccountID[:]) + + // base asset + binary.LittleEndian.PutUint32(b[offset:offset+4], p.BaseAsset) + offset += 4 + + // quote asset + binary.LittleEndian.PutUint32(b[offset:offset+4], p.QuoteAsset) + offset += 4 + + // order type (e.g. market, limit, cancel) + b[offset] = uint8(p.OrderType) + offset++ + + // client time + binary.LittleEndian.PutUint64(b[offset:offset+8], uint64(p.ClientTime.Unix())) + offset += 8 + + // server time + binary.LittleEndian.PutUint64(b[offset:offset+8], uint64(p.ServerTime.Unix())) + return b +} + +// MarketOrder defines a market order in terms of a Prefix and the order +// details, including the backing UTXOs, the order direction/side, order +// quantity, and the address where the matched client will send funds. The order +// quantity is in atoms of the base asset, and must be an integral multiple of +// the asset's lot size, except for Market buy orders when it is in units of the +// quote asset and is not bound by integral lot size multiple constraints. +type MarketOrder struct { + Prefix + UTXOs []UTXO + Sell bool + Quantity uint64 + Address string + + // Filled is not part of the order's serialization. + Filled uint64 +} + +// ID computes the order ID. +func (o *MarketOrder) ID() OrderID { + return calcOrderID(o) +} + +// UID computes the order ID, returning the string representation. +func (o *MarketOrder) UID() string { + return o.ID().String() +} + +// SerializeSize returns the length of the serialized MarketOrder. +func (o *MarketOrder) SerializeSize() int { + // Compute the size of the serialized UTXOs. + var utxosSize int + for _, u := range o.UTXOs { + utxosSize += len(u.TxHash()) + 4 + // TODO: ensure all UTXOs have the same size, indicating the same asset? + } + // The serialized order includes a byte for UTXO count, but this is implicit + // in UTXO slice length. + return o.Prefix.SerializeSize() + 1 + utxosSize + 1 + 8 + len(o.Address) +} + +// Serialize marshals the MarketOrder into a []byte. +func (o *MarketOrder) Serialize() []byte { + b := make([]byte, o.SerializeSize()) + + // Prefix + copy(b[:PrefixLen], o.Prefix.Serialize()) + offset := PrefixLen + + // UTXO count + b[offset] = uint8(len(o.UTXOs)) + offset++ + + // UTXO data + for _, u := range o.UTXOs { + utxoSz := utxoSize(u) + copy(b[offset:offset+utxoSz], serializeUTXO(u)) + offset += utxoSz + } + + // order side + var side uint8 + if o.Sell { + side = 1 + } + b[offset] = side + offset++ + + // order quantity + binary.LittleEndian.PutUint64(b[offset:offset+8], o.Quantity) + offset += 8 + + // client address for received funds + copy(b[offset:offset+len(o.Address)], []byte(o.Address)) + return b +} + +// Type returns MarketOrderType for a MarketOrder. +func (o *MarketOrder) Type() OrderType { + return MarketOrderType +} + +// Remaining returns the remaining order amount. +func (o *MarketOrder) Remaining() uint64 { + return o.Quantity - o.Filled +} + +// Ensure MarketOrder is an Order. +var _ Order = (*MarketOrder)(nil) + +// LimitOrder defines a limit order in terms of a MarketOrder and limit-specific +// data including rate (price) and time in force. +type LimitOrder struct { + MarketOrder // order type in the prefix is the only difference + Rate uint64 // price as atoms of quote asset, applied per 1e8 units of the base asset + Force TimeInForce +} + +// ID computes the order ID. +func (o *LimitOrder) ID() OrderID { + return calcOrderID(o) +} + +// UID computes the order ID, returning the string representation. +func (o *LimitOrder) UID() string { + return o.ID().String() +} + +// SerializeSize returns the length of the serialized LimitOrder. +func (o *LimitOrder) SerializeSize() int { + return o.MarketOrder.SerializeSize() + 8 + 1 +} + +// Serialize marshals the LimitOrder into a []byte. +func (o *LimitOrder) Serialize() []byte { + b := make([]byte, o.SerializeSize()) + // Prefix and data common with MarketOrder + offset := o.MarketOrder.SerializeSize() + copy(b[:offset], o.MarketOrder.Serialize()) + + // Price rate + // var fb bytes.Buffer + // _ = binary.Write(&fb, binary.LittleEndian, o.Rate) + // copy(b[mSz:], fb.Bytes()) + //binary.LittleEndian.PutUint64(b[offset:offset+8], math.Float64bits(o.Rate)) + // Price rate in atoms of quote asset + binary.LittleEndian.PutUint64(b[offset:offset+8], o.Rate) + offset += 8 + + // Time in force + b[offset] = uint8(o.Force) + return b +} + +// Type returns LimitOrderType for a LimitOrder. +func (o *LimitOrder) Type() OrderType { + return LimitOrderType +} + +// Ensure LimitOrder is an Order. +var _ Order = (*LimitOrder)(nil) + +// CancelOrder defines a cancel order in terms of an order Prefix and the ID of +// the order to be canceled. +type CancelOrder struct { + Prefix + TargetOrderID OrderID +} + +// ID computes the order ID. +func (o *CancelOrder) ID() OrderID { + return calcOrderID(o) +} + +// UID computes the order ID, returning the string representation. +func (o *CancelOrder) UID() string { + return o.ID().String() +} + +// SerializeSize returns the length of the serialized CancelOrder. +func (o *CancelOrder) SerializeSize() int { + return o.Prefix.SerializeSize() + OrderIDSize +} + +// Serialize marshals the CancelOrder into a []byte. +func (o *CancelOrder) Serialize() []byte { + return append(o.Prefix.Serialize(), o.TargetOrderID[:]...) +} + +// Type returns CancelOrderType for a CancelOrder. +func (o *CancelOrder) Type() OrderType { + return CancelOrderType +} + +// Remaining always returns 0 for a CancelOrder. +func (o *CancelOrder) Remaining() uint64 { + return 0 +} + +// Ensure CancelOrder is an Order. +var _ Order = (*CancelOrder)(nil) diff --git a/server/market/order/order_test.go b/server/market/order/order_test.go new file mode 100644 index 0000000000..ac1a1fa007 --- /dev/null +++ b/server/market/order/order_test.go @@ -0,0 +1,515 @@ +// Package order defines the Order and Match types used throughout the DEX. +package order + +import ( + "encoding/hex" + "reflect" + "testing" + "time" + + "github.com/decred/dcrdex/server/account" +) + +// func randomAccount() (acct account.AccountID) { +// if _, err := rand.Read(acct[:]); err != nil { +// panic("boom") +// } +// return +// } + +var acct0 = account.AccountID{ + 0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, 0x15, + 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, 0x56, 0x99, + 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, +} + +// var acctX = account.AccountID{ +// 0x12, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, 0x15, +// 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, 0x56, 0x99, +// 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x41, +// } + +const ( + AssetDCR uint32 = iota + AssetBTC +) + +type utxo struct { + txHash []byte + vout uint32 +} + +func (u *utxo) TxHash() []byte { + return u.txHash +} + +func (u *utxo) Vout() uint32 { + return u.vout +} + +func newUtxo(txid string, vout uint32) *utxo { + hash, err := hex.DecodeString(txid) + if err != nil { + panic(err) + } + return &utxo{hash, vout} +} + +func TestPrefix_Serialize(t *testing.T) { + type fields struct { + AccountID account.AccountID + BaseAsset uint32 + QuoteAsset uint32 + OrderType OrderType + ClientTime time.Time + ServerTime time.Time + } + tests := []struct { + name string + fields fields + want []byte + }{ + { + "ok acct0", + fields{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: LimitOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + []byte{0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, + 0x15, 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, + 0x56, 0x99, 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, 0x0, + 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x75, 0xdb, 0x5e, + 0x5d, 0x0, 0x0, 0x0, 0x0, 0x78, 0xdb, 0x5e, 0x5d, 0x0, 0x0, + 0x0, 0x0}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Prefix{ + AccountID: tt.fields.AccountID, + BaseAsset: tt.fields.BaseAsset, + QuoteAsset: tt.fields.QuoteAsset, + OrderType: tt.fields.OrderType, + ClientTime: tt.fields.ClientTime, + ServerTime: tt.fields.ServerTime, + } + got := p.Serialize() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Prefix.Serialize() = %#v, want %#v", got, tt.want) + } + sz := p.SerializeSize() + wantSz := len(got) + if sz != wantSz { + t.Errorf("Prefix.SerializeSize() = %d,\n want %d", sz, wantSz) + } + }) + } +} + +func TestMarketOrder_Serialize_SerializeSize(t *testing.T) { + type fields struct { + Prefix Prefix + UTXOs []UTXO + Sell bool + Quantity uint64 + Address string + } + tests := []struct { + name string + fields fields + want []byte + }{ + { + "ok acct0", + fields{ + Prefix: Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: MarketOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []UTXO{ + newUtxo("aed8e9b2b889bf0a78e559684796800144cd76dc8faac2aeac44fbd1c310124b", 1), + newUtxo("45b82138ca90e665a1c8793aa901aa232dd82be41b8e630dd621f24e717fc13a", 2), + }, + Sell: false, + Quantity: 132413241324, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + []byte{0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, 0x15, + 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, 0x56, 0x99, + 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x1, 0x75, 0xdb, 0x5e, 0x5d, 0x0, 0x0, 0x0, + 0x0, 0x78, 0xdb, 0x5e, 0x5d, 0x0, 0x0, 0x0, 0x0, 0x2, 0xae, 0xd8, + 0xe9, 0xb2, 0xb8, 0x89, 0xbf, 0xa, 0x78, 0xe5, 0x59, 0x68, 0x47, 0x96, + 0x80, 0x1, 0x44, 0xcd, 0x76, 0xdc, 0x8f, 0xaa, 0xc2, 0xae, 0xac, 0x44, + 0xfb, 0xd1, 0xc3, 0x10, 0x12, 0x4b, 0x1, 0x0, 0x0, 0x0, 0x45, 0xb8, + 0x21, 0x38, 0xca, 0x90, 0xe6, 0x65, 0xa1, 0xc8, 0x79, 0x3a, 0xa9, 0x1, + 0xaa, 0x23, 0x2d, 0xd8, 0x2b, 0xe4, 0x1b, 0x8e, 0x63, 0xd, 0xd6, 0x21, + 0xf2, 0x4e, 0x71, 0x7f, 0xc1, 0x3a, 0x2, 0x0, 0x0, 0x0, 0x0, 0xec, + 0xb7, 0x71, 0xd4, 0x1e, 0x0, 0x0, 0x0, 0x44, 0x63, 0x71, 0x58, 0x73, + 0x77, 0x6a, 0x54, 0x50, 0x6e, 0x55, 0x63, 0x64, 0x34, 0x46, 0x52, 0x43, + 0x6b, 0x58, 0x34, 0x76, 0x52, 0x4a, 0x78, 0x6d, 0x56, 0x74, 0x66, 0x67, + 0x47, 0x56, 0x61, 0x35, 0x75, 0x69}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &MarketOrder{ + Prefix: tt.fields.Prefix, + UTXOs: tt.fields.UTXOs, + Sell: tt.fields.Sell, + Quantity: tt.fields.Quantity, + Address: tt.fields.Address, + } + got := o.Serialize() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarketOrder.Serialize() = %#v,\n want %#v", got, tt.want) + } + sz := o.SerializeSize() + wantSz := len(got) + if sz != wantSz { + t.Errorf("MarketOrder.SerializeSize() = %d,\n want %d", sz, wantSz) + } + }) + } +} + +func TestLimitOrder_Serialize_SerializeSize(t *testing.T) { + type fields struct { + MarketOrder MarketOrder + Rate uint64 + Force TimeInForce + } + tests := []struct { + name string + fields fields + want []byte + }{ + { + "ok acct0", + fields{ + MarketOrder: MarketOrder{ + Prefix: Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: LimitOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []UTXO{ + newUtxo("d186e4b6625c9c94797cc494f535fc150177e0619e2303887e0a677f29ef1bab", 0), + newUtxo("11d9580e19ad65a875a5bc558d600e96b2916062db9e8b65cbc2bb905207c1ad", 16), + }, + Sell: false, + Quantity: 132413241324, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + Rate: 13241324, + Force: StandingTiF, + }, + []byte{0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, 0x15, + 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, 0x56, 0x99, + 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x75, 0xdb, 0x5e, 0x5d, 0x0, 0x0, 0x0, + 0x0, 0x78, 0xdb, 0x5e, 0x5d, 0x0, 0x0, 0x0, 0x0, 0x2, 0xd1, 0x86, + 0xe4, 0xb6, 0x62, 0x5c, 0x9c, 0x94, 0x79, 0x7c, 0xc4, 0x94, 0xf5, 0x35, + 0xfc, 0x15, 0x1, 0x77, 0xe0, 0x61, 0x9e, 0x23, 0x3, 0x88, 0x7e, 0xa, + 0x67, 0x7f, 0x29, 0xef, 0x1b, 0xab, 0x0, 0x0, 0x0, 0x0, 0x11, 0xd9, + 0x58, 0xe, 0x19, 0xad, 0x65, 0xa8, 0x75, 0xa5, 0xbc, 0x55, 0x8d, 0x60, + 0xe, 0x96, 0xb2, 0x91, 0x60, 0x62, 0xdb, 0x9e, 0x8b, 0x65, 0xcb, 0xc2, + 0xbb, 0x90, 0x52, 0x7, 0xc1, 0xad, 0x10, 0x0, 0x0, 0x0, 0x0, 0xec, + 0xb7, 0x71, 0xd4, 0x1e, 0x0, 0x0, 0x0, 0x44, 0x63, 0x71, 0x58, 0x73, + 0x77, 0x6a, 0x54, 0x50, 0x6e, 0x55, 0x63, 0x64, 0x34, 0x46, 0x52, 0x43, + 0x6b, 0x58, 0x34, 0x76, 0x52, 0x4a, 0x78, 0x6d, 0x56, 0x74, 0x66, 0x67, + 0x47, 0x56, 0x61, 0x35, 0x75, 0x69, 0xec, 0xb, 0xca, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &LimitOrder{ + MarketOrder: tt.fields.MarketOrder, + Rate: tt.fields.Rate, + Force: tt.fields.Force, + } + got := o.Serialize() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("LimitOrder.Serialize() = %#v, want %#v", got, tt.want) + } + sz := o.SerializeSize() + wantSz := len(got) + if sz != wantSz { + t.Errorf("LimitOrder.SerializeSize() = %d,\n want %d", sz, wantSz) + } + }) + } +} + +func TestCancelOrder_Serialize(t *testing.T) { + type fields struct { + Prefix Prefix + TargetOrderID OrderID + } + tests := []struct { + name string + fields fields + want []byte + }{ + { + "ok", + fields{ + Prefix: Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: CancelOrderType, + ClientTime: time.Unix(1566497693, 0), + ServerTime: time.Unix(1566497696, 0), + }, + TargetOrderID: OrderID{0xce, 0x8c, 0xc8, 0xd, 0xda, 0x9a, 0x40, 0xbb, 0x43, + 0xba, 0x58, 0x9, 0x75, 0xfd, 0x23, 0x85, 0x4c, 0x4, 0x4d, 0x8, 0x12, + 0x54, 0x1f, 0x88, 0x25, 0x48, 0xaa, 0x8, 0x78, 0xe5, 0xa2, 0x67}, + }, + []byte{0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, + 0x15, 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, + 0x56, 0x99, 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, 0x0, + 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x9d, 0xdb, 0x5e, + 0x5d, 0x0, 0x0, 0x0, 0x0, 0xa0, 0xdb, 0x5e, 0x5d, 0x0, 0x0, + 0x0, 0x0, + 0xce, 0x8c, 0xc8, 0xd, 0xda, 0x9a, 0x40, 0xbb, 0x43, 0xba, 0x58, + 0x9, 0x75, 0xfd, 0x23, 0x85, 0x4c, 0x4, 0x4d, 0x8, 0x12, 0x54, + 0x1f, 0x88, 0x25, 0x48, 0xaa, 0x8, 0x78, 0xe5, 0xa2, 0x67}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &CancelOrder{ + Prefix: tt.fields.Prefix, + TargetOrderID: tt.fields.TargetOrderID, + } + got := o.Serialize() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CancelOrder.Serialize() = %#v, want %v", got, tt.want) + } + sz := o.SerializeSize() + wantSz := len(got) + if sz != wantSz { + t.Errorf("CancelOrder.SerializeSize() = %d,\n want %d", sz, wantSz) + } + }) + } +} + +func TestMarketOrder_ID(t *testing.T) { + orderID0, _ := hex.DecodeString("93f565294da3e939f90b1d41288c55dfd21d2f5947001942a9773fd627b5dfbe") + var orderID OrderID + copy(orderID[:], orderID0) + + type fields struct { + Prefix Prefix + UTXOs []UTXO + Sell bool + Quantity uint64 + Address string + } + tests := []struct { + name string + fields fields + want OrderID + }{ + { + "ok", + fields{ + Prefix: Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: MarketOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []UTXO{ + newUtxo("a985d8df97571b130ce30a049a76ffedaa79b6e69b173ff81b1bf9fc07f063c7", 1), + }, + Sell: true, + Quantity: 132413241324, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + orderID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &MarketOrder{ + Prefix: tt.fields.Prefix, + UTXOs: tt.fields.UTXOs, + Sell: tt.fields.Sell, + Quantity: tt.fields.Quantity, + Address: tt.fields.Address, + } + remaining := o.Remaining() + if remaining != o.Quantity-o.Filled { + t.Errorf("MarketOrder.Remaining incorrect, got %d, expected %d", + remaining, o.Quantity-o.Filled) + } + if got := o.ID(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarketOrder.ID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLimitOrder_ID(t *testing.T) { + orderID0, _ := hex.DecodeString("60ba7b92d0905aeaf93df3a69f28df2c7133b52361ec6114c825989b69bcf25b") + var orderID OrderID + copy(orderID[:], orderID0) + + type fields struct { + MarketOrder MarketOrder + Rate uint64 + Force TimeInForce + } + tests := []struct { + name string + fields fields + want OrderID + }{ + { + "ok", + fields{ + MarketOrder: MarketOrder{ + Prefix: Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: LimitOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []UTXO{ + newUtxo("01516d9c7ffbe260b811dc04462cedd3f8969ce3a3ffe6231ae870775a92e9b0", 1), + }, + Sell: false, + Quantity: 132413241324, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + Rate: 13241324, + Force: StandingTiF, + }, + orderID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &LimitOrder{ + MarketOrder: tt.fields.MarketOrder, + Rate: tt.fields.Rate, + Force: tt.fields.Force, + } + remaining := o.Remaining() + if remaining != o.Quantity-o.Filled { + t.Errorf("LimitOrder.Remaining incorrect, got %d, expected %d", + remaining, o.Quantity-o.Filled) + } + if got := o.ID(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("LimitOrder.ID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCancelOrder_ID(t *testing.T) { + limitOrderID0, _ := hex.DecodeString("60ba7b92d0905aeaf93df3a69f28df2c7133b52361ec6114c825989b69bcf25b") + var limitOrderID OrderID + copy(limitOrderID[:], limitOrderID0) + + orderID0, _ := hex.DecodeString("ba9aa378c9bfdce863ca65833cd254a1a2cc7a935a2942eb7ca47631a764cb2d") + var cancelOrderID OrderID + copy(cancelOrderID[:], orderID0) + + type fields struct { + Prefix Prefix + TargetOrderID OrderID + } + tests := []struct { + name string + fields fields + want OrderID + }{ + { + "ok", + fields{ + Prefix: Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: CancelOrderType, + ClientTime: time.Unix(1566497693, 0), + ServerTime: time.Unix(1566497696, 0), + }, + TargetOrderID: limitOrderID, + }, + cancelOrderID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &CancelOrder{ + Prefix: tt.fields.Prefix, + TargetOrderID: tt.fields.TargetOrderID, + } + remaining := o.Remaining() + if remaining != 0 { + t.Errorf("CancelOrder.Remaining should be 0, got %d", remaining) + } + if got := o.ID(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CancelOrder.ID() = %v, want %v", got, tt.want) + } + }) + } +} + +// func randomHash() (h [32]byte) { +// rand.Read(h[:]) +// return +// } + +func Test_serializeUTXO(t *testing.T) { + type args struct { + u *utxo + } + tests := []struct { + name string + args args + want []byte + }{ + { + "ok", + args{ + newUtxo("bc4b0ffe3a70cf159657b1f8f12c2d895c5d7e849de6ac1c3358be86842f4549", 4), + }, + []byte{ + 0xbc, 0x4b, 0xf, 0xfe, 0x3a, 0x70, 0xcf, 0x15, + 0x96, 0x57, 0xb1, 0xf8, 0xf1, 0x2c, 0x2d, 0x89, + 0x5c, 0x5d, 0x7e, 0x84, 0x9d, 0xe6, 0xac, 0x1c, + 0x33, 0x58, 0xbe, 0x86, 0x84, 0x2f, 0x45, 0x49, + 0x4, 0x0, 0x0, 0x0, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := serializeUTXO(tt.args.u); !reflect.DeepEqual(got, tt.want) { + t.Errorf("serializeUTXO() = %#v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/matcher/go.mod b/server/matcher/go.mod new file mode 100644 index 0000000000..5f52390f33 --- /dev/null +++ b/server/matcher/go.mod @@ -0,0 +1,15 @@ +module github.com/decred/dcrdex/server/matcher + +go 1.12 + +replace ( + github.com/decred/dcrdex/server/account => ../account + github.com/decred/dcrdex/server/market => ../market +) + +require ( + github.com/decred/dcrd/crypto/blake256 v1.0.0 + github.com/decred/dcrdex/server/account v0.0.0-00010101000000-000000000000 + github.com/decred/dcrdex/server/market v0.0.0-00010101000000-000000000000 + github.com/decred/slog v1.0.0 +) diff --git a/server/matcher/go.sum b/server/matcher/go.sum new file mode 100644 index 0000000000..6bf5325cc0 --- /dev/null +++ b/server/matcher/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1/go.mod h1:OVfvaOsNLS/A1y4Eod0Ip/Lf8qga7VXCQjUQLbkY0Go= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 h1:awk7sYJ4pGWmtkiGHFfctztJjHMKGLV8jctGQhAbKe0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2/go.mod h1:CHTUIVfmDDd0KFVFpNX1pFVCBUegxW387nN0IGwNKR0= +github.com/decred/dcrdex v0.0.0-20190820222222-0c633a45b26a h1:IkTp1kie2MKG5zf4nlcVsP9UXPghXUBLa8JB+T538UQ= +github.com/decred/slog v1.0.0 h1:Dl+W8O6/JH6n2xIFN2p3DNjCmjYwvrXsjlSJTQQ4MhE= +github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtAonQ= diff --git a/server/matcher/interface.go b/server/matcher/interface.go new file mode 100644 index 0000000000..c1c36b3406 --- /dev/null +++ b/server/matcher/interface.go @@ -0,0 +1,24 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package matcher + +import "github.com/decred/dcrdex/server/market/order" + +// TODO. PLACEHOLDER. There needs to be an interface satisfied by the order book +// type provided to the matcher. This Booker is just a hint. + +type Booker interface { + LotSize() uint64 + BuyCount() int + SellCount() int + BestSell() *order.LimitOrder + BestBuy() *order.LimitOrder + //Best(sell bool) *order.LimitOrder + Insert(*order.LimitOrder) + Remove(order.OrderID) (*order.LimitOrder, bool) +} + +// type EpochQueuer interface { +// Booker +// } diff --git a/server/matcher/log.go b/server/matcher/log.go new file mode 100644 index 0000000000..9184da393d --- /dev/null +++ b/server/matcher/log.go @@ -0,0 +1,24 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package matcher + +import ( + "github.com/decred/slog" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log = slog.Disabled + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + log = slog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger slog.Logger) { + log = logger +} diff --git a/server/matcher/match.go b/server/matcher/match.go new file mode 100644 index 0000000000..1ff8601081 --- /dev/null +++ b/server/matcher/match.go @@ -0,0 +1,439 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package matcher + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + "math/rand" + "sort" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrdex/server/market/order" +) + +// HashFunc is the hash function used to generate the shuffling seed. +var HashFunc = blake256.Sum256 + +const ( + HashSize = blake256.Size + + atomsPerCoin uint64 = 1e8 +) + +var bigAtomsPerCoin = big.NewInt(int64(atomsPerCoin)) + +type Matcher struct{} + +// New creates a new Matcher. +func New() *Matcher { + return &Matcher{} +} + +// orderLotSizeOK checks if the remaining Order quantity is not a multiple of +// lot size, unless the order is a market buy order, which is not subject to +// this constraint. +func orderLotSizeOK(ord order.Order, lotSize uint64) bool { + if mo, ok := ord.(*order.MarketOrder); ok { + // Market buy orders are not subject to lot size constraints. + if !mo.Sell { + return true + } + } + + // NOTE: Cancel orders have 0 remaining by definition. + return ord.Remaining()%lotSize == 0 +} + +// assertOrderLotSize will panic if the remaining Order quantity is not a +// multiple of lot size, unless the order is a market buy order. +func assertOrderLotSize(ord order.Order, lotSize uint64) { + if orderLotSizeOK(ord, lotSize) { + return + } + panic(fmt.Sprintf( + "order %v has remaining quantity %d that is not a multiple of lot size %d", + ord.ID(), ord.Remaining(), lotSize)) +} + +// BaseToQuote computes a quote asset amount based on a base asset amount +// and an integer representation of the price rate. That is, +// quoteAmt = rate * baseAmt / atomsPerCoin +func BaseToQuote(rate uint64, base uint64) (quote uint64) { + bigRate := big.NewInt(int64(rate)) + bigBase := big.NewInt(int64(base)) + bigBase.Mul(bigBase, bigRate) + bigBase.Div(bigBase, bigAtomsPerCoin) + return bigBase.Uint64() +} + +// QuoteToBase computes a base asset amount based on a quote asset amount +// and an integer representation of the price rate. That is, +// baseAmt = quoteAmt * atomsPerCoin / rate +func QuoteToBase(rate uint64, quote uint64) (base uint64) { + bigRate := big.NewInt(int64(rate)) + bigQuote := big.NewInt(int64(quote)) + bigQuote.Mul(bigQuote, bigAtomsPerCoin) + bigQuote.Div(bigQuote, bigRate) + return bigQuote.Uint64() +} + +// CheckMarketBuyBuffer verifies that the given market buy order's quantity +// (specified in quote asset) is sufficient according to the Matcher's +// configured market buy buffer, which is in base asset units, and the best +// standing sell order according to the provided Booker. +func CheckMarketBuyBuffer(book Booker, ord *order.MarketOrder, marketBuyBuffer float64) bool { + if ord.Sell { + return true // The market buy buffer does not apply to sell orders. + } + minBaseAsset := uint64(marketBuyBuffer * float64(book.LotSize())) + return ord.Remaining() >= BaseToQuote(book.BestSell().Rate, minBaseAsset) +} + +// Match matches orders given a standing order book and an epoch queue. Matched +// orders from the book are removed from the book. +func (m *Matcher) Match(book Booker, queue []order.Order) (matches []*order.Match, passed, failed, partial, inserted []order.Order) { + // Apply the deterministic pseudorandom shuffling. + shuffleQueue(queue) + + // For each order in the queue, find the best match in the book. + for _, q := range queue { + if !orderLotSizeOK(q, book.LotSize()) { + log.Warnf("Order with bad lot size in the queue: %v!", q.ID()) + failed = append(failed, q) + continue + } + + switch o := q.(type) { + case *order.CancelOrder: + removed, ok := book.Remove(o.TargetOrderID) + if !ok { + // The targeted order might be down queue or non-existent. + log.Debugf("Failed to remove order %v set by a cancel order %v", + o.ID(), o.TargetOrderID) + failed = append(failed, q) + continue + } + + passed = append(passed, q) + // CancelOrder Match has zero values for Amounts, Rates, and Total. + matches = append(matches, &order.Match{ + Taker: q, + Makers: []*order.LimitOrder{removed}, + }) + + case *order.LimitOrder: + // limit-limit order matching + match := matchLimitOrder(book, o) + if match != nil { + matches = append(matches, match) + passed = append(passed, q) + } else if o.Force == order.ImmediateTiF { + // There was no match and TiF is Immediate. Fail. + failed = append(failed, q) + break + } + + if o.Remaining() > 0 { + partial = append(partial, q) + if o.Force == order.StandingTiF { + // Standing TiF orders go on the book. + book.Insert(o) + inserted = append(inserted, q) + } + } + + case *order.MarketOrder: + // market-limit order matching + var match *order.Match + if o.Sell { + match = matchMarketSellOrder(book, o) + } else { + // Market buy order Quantity is denominated in the quote asset, + // and lot size multiples are not applicable. + match = matchMarketBuyOrder(book, o) + } + if match != nil { + matches = append(matches, match) + passed = append(passed, q) + } else { + // There was no match and this is a market order. Fail. + failed = append(failed, q) + } + + // Regardless of remaining amount, market orders never go on the book. + } + + } + + return +} + +// limit-limit order matching +func matchLimitOrder(book Booker, ord *order.LimitOrder) (match *order.Match) { + amtRemaining := ord.Remaining() // i.e. ord.Quantity - ord.Filled + if amtRemaining == 0 { + return + } + + lotSize := book.LotSize() + assertOrderLotSize(ord, lotSize) + + bestFunc := book.BestSell + rateMatch := func(b, s uint64) bool { return s <= b } + if ord.Sell { + // order is a sell order + bestFunc = book.BestBuy + rateMatch = func(s, b uint64) bool { return s <= b } + } + + // Find matches until the order has been depleted. + for amtRemaining > 0 { + // Get the best book order for this limit order. + best := bestFunc() // maker + if best == nil { + return + } + assertOrderLotSize(best, lotSize) + + // Check rate. + if !rateMatch(ord.Rate, best.Rate) { + return + } + // now, best.Rate <= ord.Rate + + // The match amount is the smaller of the order's remaining quantity or + // the best matching order amount. + amt := best.Remaining() + if amtRemaining < amt { + // Partially fill the standing order, updating its value. + amt = amtRemaining + } else { + // The standing order has been consumed. Remove it from the book. + if _, ok := book.Remove(best.ID()); !ok { + log.Errorf("Failed to remove standing order %v.", best) + } + } + best.Filled += amt + + // Reduce the remaining quantity of the taker order. + amtRemaining -= amt + ord.Filled += amt + + // Add the matched maker order to the output. + if match == nil { + match = &order.Match{ + Taker: ord, + Makers: []*order.LimitOrder{best}, + Amounts: []uint64{amt}, + Rates: []uint64{best.Rate}, + Total: amt, + } + } else { + match.Makers = append(match.Makers, best) + match.Amounts = append(match.Amounts, amt) + match.Rates = append(match.Rates, best.Rate) + match.Total += amt + } + } + + return +} + +// market(sell)-limit order matching +func matchMarketSellOrder(book Booker, ord *order.MarketOrder) (match *order.Match) { + if !ord.Sell { + panic("matchMarketSellOrder: not a sell order") + } + + // A market sell order is a special case of a limit order with time-in-force + // immediate and no minimum rate (a rate of 0). + limOrd := &order.LimitOrder{ + MarketOrder: *ord, + Force: order.ImmediateTiF, + Rate: 0, + } + match = matchLimitOrder(book, limOrd) + if match == nil { + return + } + // The Match.Taker must be the *MarketOrder, not the wrapped *LimitOrder. + match.Taker = ord + return +} + +// market(buy)-limit order matching +func matchMarketBuyOrder(book Booker, ord *order.MarketOrder) (match *order.Match) { + if ord.Sell { + panic("matchMarketBuyOrder: not a buy order") + } + + lotSize := book.LotSize() + + // Amount remaining for market buy is in *quoute* asset, not base asset. + amtRemaining := ord.Remaining() // i.e. ord.Quantity - ord.Filled + if amtRemaining == 0 { + return + } + + // Find matches until the order has been depleted. + for amtRemaining > 0 { + // Get the best book order for this limit order. + best := book.BestSell() // maker + if best == nil { + return + } + + // Convert the market buy order's quantity into base asset: + // quoteAmt = rate * baseAmt + amtRemainingBase := QuoteToBase(best.Rate, amtRemaining) + //amtRemainingBase := uint64(float64(amtRemaining) / best.Rate) // trunc + if amtRemainingBase < lotSize { + return + } + + // To convert the matching limit order's quantity into quote asset: + // amt := uint64(best.Rate * float64(best.Quantity)) // trunc + + // The match amount is the smaller of the order's remaining quantity or + // the best matching order amount. + amt := best.Remaining() + if amtRemainingBase < amt { + // Partially fill the standing order, updating its value. + amt = amtRemainingBase - amtRemainingBase%lotSize // amt is a multiple of lot size + } else { + // The standing order has been consumed. Remove it from the book. + if _, ok := book.Remove(best.ID()); !ok { + log.Errorf("Failed to remove standing order %v.", best) + } + } + best.Filled += amt + + // Reduce the remaining quantity of the taker order. + // amtRemainingBase -= amt // FYI + amtQuote := BaseToQuote(best.Rate, amt) + //amtQuote := uint64(float64(amt) * best.Rate) + amtRemaining -= amtQuote // quote asset remaining + ord.Filled += amtQuote // quote asset filled + + // Add the matched maker order to the output. + if match == nil { + match = &order.Match{ + Taker: ord, + Makers: []*order.LimitOrder{best}, + Amounts: []uint64{amt}, + Rates: []uint64{best.Rate}, + Total: amt, + } + } else { + match.Makers = append(match.Makers, best) + match.Amounts = append(match.Amounts, amt) + match.Rates = append(match.Rates, best.Rate) + match.Total += amt + } + } + + return +} + +// OrdersMatch checks if two orders are valid matches, without regard to quantity. +// - not a cancel order +// - not two market orders +// - orders on different sides (one buy and one sell) +// - two limit orders with overlapping rates, or one market and one limit +func OrdersMatch(a, b order.Order) bool { + // Get order data needed for comparison. + aType, aSell, _, aRate := orderData(a) + bType, bSell, _, bRate := orderData(b) + + // Orders must be on opposite sides of the market. + if aSell == bSell { + return false + } + + // Screen order types. + switch aType { + case order.MarketOrderType: + switch bType { + case order.LimitOrderType: + return true // market-limit + case order.MarketOrderType: + fallthrough // no two market orders + default: + return false // cancel or unknown + } + case order.LimitOrderType: + switch bType { + case order.LimitOrderType: + // limit-limit: must check rates + case order.MarketOrderType: + return true // limit-market + default: + return false // cancel or unknown + } + default: // cancel or unknown + return false + } + + // For limit-limit orders, check that the rates overlap. + cmp := func(buyRate, sellRate uint64) bool { return sellRate <= buyRate } + if bSell { + // a is buy, b is sell + return cmp(aRate, bRate) + } + // a is sell, b is buy + return cmp(bRate, aRate) +} + +func orderData(o order.Order) (orderType order.OrderType, sell bool, amount, rate uint64) { + orderType = o.Type() + + switch ot := o.(type) { + case *order.LimitOrder: + sell = ot.Sell + amount = ot.Quantity + rate = ot.Rate + case *order.MarketOrder: + sell = ot.Sell + amount = ot.Quantity + } + + return +} + +// sortQueue lexicographically sorts the Orders by their IDs. +func sortQueue(queue []order.Order) { + sort.Slice(queue, func(i, j int) bool { + ii, ij := queue[i].ID(), queue[j].ID() + return bytes.Compare(ii[:], ij[:]) >= 0 + }) +} + +// shuffleQueue deterministically shuffles the Orders using a Fisher-Yates +// algorithm seeded with the hash of the concatenated order ID hashes. +func shuffleQueue(queue []order.Order) { + // The shuffling seed is derived from the sorted orders. + sortQueue(queue) + + // Compute and concatenate the hashes of the order IDs. + qLen := len(queue) + hashCat := make([]byte, HashSize*qLen) + for i, o := range queue { + id := o.ID() + h := HashFunc(id[:]) + copy(hashCat[HashSize*i:HashSize*(i+1)], h[:]) + } + + // Fisher-Yates shuffle the slice using a seed derived from the hash of the + // concatenated order ID hashes. + seedHash := HashFunc(hashCat) + seed := int64(binary.LittleEndian.Uint64(seedHash[:8])) + prng := rand.New(rand.NewSource(seed)) + for i := range queue { + j := prng.Intn(qLen-i) + i + queue[i], queue[j] = queue[j], queue[i] + } +} diff --git a/server/matcher/match_test.go b/server/matcher/match_test.go new file mode 100644 index 0000000000..ede9b7a64e --- /dev/null +++ b/server/matcher/match_test.go @@ -0,0 +1,1682 @@ +package matcher + +import ( + "fmt" + "os" + "reflect" + "testing" + "time" + + "github.com/decred/dcrdex/server/account" + "github.com/decred/dcrdex/server/market/order" + "github.com/decred/slog" +) + +// An arbitrary account ID for test orders. +var acct0 = account.AccountID{ + 0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 0xd1, 0xff, 0x73, 0x15, + 0x90, 0xbc, 0xbd, 0xda, 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, 0x56, 0x99, + 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, +} + +const ( + AssetDCR uint32 = iota + AssetBTC + + LotSize = uint64(10 * 1e8) +) + +func startLogger() { + logger := slog.NewBackend(os.Stdout).Logger("MATCHTEST") + logger.SetLevel(slog.LevelDebug) + UseLogger(logger) +} + +var ( + marketOrders = []*order.MarketOrder{ + { // market BUY of 4 lots + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.MarketOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []order.UTXO{}, + Sell: false, + Quantity: 4 * LotSize, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + { // market SELL of 2 lots + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.MarketOrderType, + ClientTime: time.Unix(1566497654, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []order.UTXO{}, + Sell: true, + Quantity: 2 * LotSize, + Address: "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm", + }, + } + + limitOrders = []*order.LimitOrder{ + { // limit BUY of 2 lots at 0.043 + MarketOrder: order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.LimitOrderType, + ClientTime: time.Unix(1566497653, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []order.UTXO{}, + Sell: false, + Quantity: 2 * LotSize, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + Rate: 4300000, + Force: order.StandingTiF, + }, + { // limit SELL of 3 lots at 0.045 + MarketOrder: order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.LimitOrderType, + ClientTime: time.Unix(1566497651, 0), + ServerTime: time.Unix(1566497652, 0), + }, + UTXOs: []order.UTXO{}, + Sell: true, + Quantity: 3 * LotSize, + Address: "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm", + }, + Rate: 4500000, + Force: order.StandingTiF, + }, + { // limit BUY of 1 lot at 0.046 + MarketOrder: order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.LimitOrderType, + ClientTime: time.Unix(1566497655, 0), + ServerTime: time.Unix(1566497656, 0), + }, + UTXOs: []order.UTXO{}, + Sell: false, + Quantity: 1 * LotSize, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + Rate: 4600000, + Force: order.StandingTiF, + }, + { // limit BUY of 1 lot at 0.045 + MarketOrder: order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.LimitOrderType, + ClientTime: time.Unix(1566497649, 0), + ServerTime: time.Unix(1566497651, 0), + }, + UTXOs: []order.UTXO{}, + Sell: false, + Quantity: 1 * LotSize, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + }, + Rate: 4500000, + Force: order.StandingTiF, + }, + } +) + +type BookStub struct { + lotSize uint64 + sellOrders []*order.LimitOrder // sorted descending in this stub + buyOrders []*order.LimitOrder // sorted ascending +} + +func (b *BookStub) LotSize() uint64 { + return b.lotSize +} + +func (b *BookStub) BestSell() *order.LimitOrder { + if len(b.sellOrders) == 0 { + return nil + } + return b.sellOrders[len(b.sellOrders)-1] +} + +func (b *BookStub) BestBuy() *order.LimitOrder { + if len(b.buyOrders) == 0 { + return nil + } + return b.buyOrders[len(b.buyOrders)-1] +} + +func (b *BookStub) SellCount() int { + return len(b.sellOrders) +} + +func (b *BookStub) BuyCount() int { + return len(b.buyOrders) +} + +func (b *BookStub) Insert(ord *order.LimitOrder) { + // Only "inserts" by making it the best order. + if ord.Sell { + b.sellOrders = append(b.sellOrders, ord) + } else { + b.buyOrders = append(b.buyOrders, ord) + } +} + +func (b *BookStub) Remove(orderID order.OrderID) (*order.LimitOrder, bool) { + for i := range b.buyOrders { + if b.buyOrders[i].ID() == orderID { + //fmt.Println("Removing", orderID) + removed := b.buyOrders[i] + b.buyOrders = append(b.buyOrders[:i], b.buyOrders[i+1:]...) + return removed, true + } + } + for i := range b.sellOrders { + if b.sellOrders[i].ID() == orderID { + //fmt.Println("Removing", orderID) + removed := b.sellOrders[i] + b.sellOrders = append(b.sellOrders[:i], b.sellOrders[i+1:]...) + return removed, true + } + } + return nil, false +} + +var _ Booker = (*BookStub)(nil) + +func newLimitOrder(sell bool, rate, quantityLots uint64, force order.TimeInForce, timeOffset int64) *order.LimitOrder { + addr := "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui" + if sell { + addr = "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm" + } + return &order.LimitOrder{ + MarketOrder: order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.LimitOrderType, + ClientTime: time.Unix(1566497653+timeOffset, 0), + ServerTime: time.Unix(1566497656+timeOffset, 0), + }, + UTXOs: []order.UTXO{}, + Sell: sell, + Quantity: quantityLots * LotSize, + Address: addr, + }, + Rate: rate, + Force: force, + } +} + +func newMarketSellOrder(quantityLots uint64, timeOffset int64) *order.MarketOrder { + return &order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.MarketOrderType, + ClientTime: time.Unix(1566497653+timeOffset, 0), + ServerTime: time.Unix(1566497656+timeOffset, 0), + }, + UTXOs: []order.UTXO{}, + Sell: true, + Quantity: quantityLots * LotSize, + Address: "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm", + } +} + +func newMarketBuyOrder(quantityQuoteAsset uint64, timeOffset int64) *order.MarketOrder { + return &order.MarketOrder{ + Prefix: order.Prefix{ + AccountID: acct0, + BaseAsset: AssetDCR, + QuoteAsset: AssetBTC, + OrderType: order.MarketOrderType, + ClientTime: time.Unix(1566497653+timeOffset, 0), + ServerTime: time.Unix(1566497656+timeOffset, 0), + }, + UTXOs: []order.UTXO{}, + Sell: false, + Quantity: quantityQuoteAsset, + Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", + } +} + +var ( + // Create a coherent order book of standing orders and sorted rates. + bookBuyOrders = []*order.LimitOrder{ + newLimitOrder(false, 2500000, 2, order.StandingTiF, 0), + newLimitOrder(false, 2700000, 2, order.StandingTiF, 0), + newLimitOrder(false, 3200000, 2, order.StandingTiF, 0), + newLimitOrder(false, 3300000, 2, order.StandingTiF, 0), + newLimitOrder(false, 3300000, 1, order.StandingTiF, 2), // newer + newLimitOrder(false, 3600000, 4, order.StandingTiF, 0), + newLimitOrder(false, 3900000, 2, order.StandingTiF, 0), + newLimitOrder(false, 4000000, 10, order.StandingTiF, 0), + newLimitOrder(false, 4300000, 2, order.StandingTiF, 0), + newLimitOrder(false, 4300000, 4, order.StandingTiF, 1), // newer + newLimitOrder(false, 4500000, 1, order.StandingTiF, 0), + } + bookSellOrders = []*order.LimitOrder{ + newLimitOrder(true, 6200000, 2, order.StandingTiF, 0), + newLimitOrder(true, 6200000, 2, order.StandingTiF, 1), // newer + newLimitOrder(true, 6100000, 2, order.StandingTiF, 0), + newLimitOrder(true, 6000000, 2, order.StandingTiF, 0), + newLimitOrder(true, 5500000, 1, order.StandingTiF, 0), + newLimitOrder(true, 5400000, 4, order.StandingTiF, 0), + newLimitOrder(true, 5000000, 2, order.StandingTiF, 0), + newLimitOrder(true, 4700000, 10, order.StandingTiF, 0), + newLimitOrder(true, 4700000, 4, order.StandingTiF, 1), // newer + newLimitOrder(true, 4600000, 2, order.StandingTiF, 0), + newLimitOrder(true, 4550000, 1, order.StandingTiF, 0), + } +) + +func newBooker() Booker { + resetMakers() + buyOrders := make([]*order.LimitOrder, len(bookBuyOrders)) + copy(buyOrders, bookBuyOrders) + sellOrders := make([]*order.LimitOrder, len(bookSellOrders)) + copy(sellOrders, bookSellOrders) + return &BookStub{ + lotSize: LotSize, + buyOrders: buyOrders, + sellOrders: sellOrders, + } +} + +func resetMakers() { + for _, o := range bookBuyOrders { + o.Filled = 0 + } + for _, o := range bookSellOrders { + o.Filled = 0 + } +} + +func newMatch(taker order.Order, makers []*order.LimitOrder, lastPartialAmount ...uint64) *order.Match { + amounts := make([]uint64, len(makers)) + rates := make([]uint64, len(makers)) + var total uint64 + for i := range makers { + total += makers[i].Quantity + amounts[i] = makers[i].Quantity + rates[i] = makers[i].Rate + } + if len(lastPartialAmount) > 0 { + amounts[len(makers)-1] = lastPartialAmount[0] + total -= makers[len(makers)-1].Quantity - lastPartialAmount[0] + } + return &order.Match{ + Taker: taker, + Makers: makers, + Amounts: amounts, + Rates: rates, + Total: total, + } +} + +func Test_matchLimitOrder(t *testing.T) { + // Setup the match package's logger. + startLogger() + + takers := []*order.LimitOrder{ + newLimitOrder(false, 4550000, 1, order.ImmediateTiF, 0), // buy, 1 lot, immediate, equal rate + newLimitOrder(true, 4450000, 1, order.ImmediateTiF, 0), // sell, 1 lot, immediate, overlapping rate + newLimitOrder(true, 4300000, 5, order.StandingTiF, 0), // sell, 5 lots, immediate, multiple makers + newLimitOrder(true, 4300000, 4, order.StandingTiF, 0), // sell, 4 lots, immediate, multiple makers, partial last maker + newLimitOrder(true, 4300000, 8, order.StandingTiF, 0), // sell, 8 lots, immediate, multiple makers, partial taker remaining + } + resetTakers := func() { + for _, o := range takers { + o.Filled = 0 + } + } + + nSell := len(bookSellOrders) + nBuy := len(bookBuyOrders) + + type args struct { + book Booker + ord *order.LimitOrder + } + tests := []struct { + name string + args args + doesMatch bool + wantMatch *order.Match + takerRemaining uint64 + }{ + { + "OK limit buy immediate rate match", + args{ + book: newBooker(), + ord: takers[0], + }, + true, + newMatch(takers[0], []*order.LimitOrder{bookSellOrders[nSell-1]}), + 0, + }, + { + "OK limit sell immediate rate overlap", + args{ + book: newBooker(), + ord: takers[1], + }, + true, + newMatch(takers[1], []*order.LimitOrder{bookBuyOrders[nBuy-1]}), + 0, + }, + { + "OK limit sell immediate multiple makers", + args{ + book: newBooker(), + ord: takers[2], + }, + true, + newMatch(takers[2], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2]}), + 0, + }, + { + "OK limit sell immediate multiple makers partial maker fill", + args{ + book: newBooker(), + ord: takers[3], + }, + true, + newMatch(takers[3], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2]}, 3*LotSize), + 0, + }, + { + "OK limit sell immediate multiple makers partial taker fill", + args{ + book: newBooker(), + ord: takers[4], + }, + true, + newMatch(takers[4], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2], bookBuyOrders[nBuy-3]}), + 1 * LotSize, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset Filled amounts of all pre-defined orders before each test. + resetTakers() + resetMakers() + + gotMatch := matchLimitOrder(tt.args.book, tt.args.ord) + matchMade := gotMatch != nil + if tt.doesMatch != matchMade { + t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) + } + if !reflect.DeepEqual(gotMatch, tt.wantMatch) { + t.Errorf("matchLimitOrder() = %v, want %v", gotMatch, tt.wantMatch) + } + if tt.takerRemaining != tt.args.ord.Remaining() { + t.Errorf("Taker remaining incorrect. Expected %d, got %d.", + tt.takerRemaining, tt.args.ord.Remaining()) + } + }) + } +} + +func newCancelOrder(targetOrderID order.OrderID) *order.CancelOrder { + return &order.CancelOrder{ + TargetOrderID: targetOrderID, + } +} + +func TestMatch_cancelOnly(t *testing.T) { + // Setup the match package's logger. + startLogger() + + // New matching engine. + me := New() + + fakeOrder := newLimitOrder(false, 4550000, 1, order.ImmediateTiF, 0) + + // takers is heterogenous w.r.t. type + takers := []order.Order{ + newCancelOrder(bookBuyOrders[3].ID()), + newCancelOrder(fakeOrder.ID()), + } + + //nSell := len(bookSellOrders) + //nBuy := len(bookBuyOrders) + + type args struct { + book Booker + queue []order.Order + } + tests := []struct { + name string + args args + doesMatch bool + wantMatches []*order.Match + wantNumPassed int + wantNumFailed int + wantNumPartial int + wantNumInserted int + }{ + { + name: "cancel standing ok", + args: args{ + book: newBooker(), + queue: []order.Order{takers[0]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + {Taker: takers[0], Makers: []*order.LimitOrder{bookBuyOrders[3]}}, + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "cancel non-existent standing", + args: args{ + book: newBooker(), + queue: []order.Order{takers[1]}, + }, + doesMatch: false, + wantMatches: nil, + wantNumPassed: 0, + wantNumFailed: 1, + wantNumPartial: 0, + wantNumInserted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset Filled amounts of all pre-defined orders before each test. + resetMakers() + + // var cancels int + // for _, oi := range tt.args.queue { + // if oi.Type() == order.CancelOrderType { + // cancels++ + // } + // } + + numBuys0 := tt.args.book.BuyCount() + + matches, passed, failed, partial, inserted := me.Match(tt.args.book, tt.args.queue) + matchMade := len(matches) > 0 && matches[0] != nil + if tt.doesMatch != matchMade { + t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) + } + if len(matches) != len(tt.wantMatches) { + t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) + } + for i := range matches { + if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { + t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) + } + } + if len(passed) != tt.wantNumPassed { + t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) + } + if len(failed) != tt.wantNumFailed { + t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) + } + if len(partial) != tt.wantNumPartial { + t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) + } + if len(inserted) != tt.wantNumInserted { + t.Errorf("number inserted %d, expected %d", len(inserted), tt.wantNumInserted) + } + + numBuys1 := tt.args.book.BuyCount() + if numBuys0-len(passed) != numBuys1 { + t.Errorf("Buy side order book size %d, expected %d", numBuys1, numBuys0-len(passed)) + } + }) + } +} + +func TestMatch_limitsOnly(t *testing.T) { + // Setup the match package's logger. + startLogger() + + // New matching engine. + me := New() + + badLotsizeOrder := newLimitOrder(false, 05000000, 1, order.ImmediateTiF, 0) + badLotsizeOrder.Quantity /= 2 + + // takers is heterogenous w.r.t. type + takers := []order.Order{ + newLimitOrder(false, 4550000, 1, order.ImmediateTiF, 0), // buy, 1 lot, immediate, equal rate + newLimitOrder(false, 4550000, 2, order.StandingTiF, 0), // buy, 2 lot, standing, equal rate, partial taker insert to book + newLimitOrder(false, 4550000, 2, order.ImmediateTiF, 0), // buy, 2 lot, immediate, equal rate, partial taker unfilled + newLimitOrder(false, 4100000, 1, order.ImmediateTiF, 0), // buy, 1 lot, immediate, unfilled fail + newLimitOrder(true, 4540000, 1, order.ImmediateTiF, 0), // sell, 1 lot, immediate + } + + resetTakers := func() { + for _, o := range takers { + switch ot := o.(type) { + case *order.MarketOrder: + ot.Filled = 0 + case *order.LimitOrder: + ot.Filled = 0 + } + } + } + + nSell := len(bookSellOrders) + //nBuy := len(bookBuyOrders) + + type args struct { + book Booker + queue []order.Order + } + tests := []struct { + name string + args args + doesMatch bool + wantMatches []*order.Match + wantNumPassed int + wantNumFailed int + wantNumPartial int + wantNumInserted int + }{ + { + name: "limit buy immediate rate match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[0]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[0], []*order.LimitOrder{bookSellOrders[nSell-1]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "limit buy standing partial taker inserted to book", + args: args{ + book: newBooker(), + queue: []order.Order{takers[1]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[1], []*order.LimitOrder{bookSellOrders[nSell-1]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 1, + wantNumInserted: 1, + }, + { + name: "limit buy immediate partial taker unfilled", + args: args{ + book: newBooker(), + queue: []order.Order{takers[2]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[2], []*order.LimitOrder{bookSellOrders[nSell-1]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 1, + wantNumInserted: 0, + }, + { + name: "limit buy immediate unfilled fail", + args: args{ + book: newBooker(), + queue: []order.Order{takers[3]}, + }, + doesMatch: false, + wantMatches: nil, + wantNumPassed: 0, + wantNumFailed: 1, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "bad lot size order", + args: args{ + book: newBooker(), + queue: []order.Order{badLotsizeOrder}, + }, + doesMatch: false, + wantMatches: nil, + wantNumPassed: 0, + wantNumFailed: 1, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "limit buy standing partial taker inserted to book, then filled by down-queue sell", + args: args{ + book: newBooker(), + queue: []order.Order{takers[1], takers[4]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[1], []*order.LimitOrder{bookSellOrders[nSell-1]}), + { // the maker is reduced by matching first item in the queue + Taker: takers[4], + Makers: []*order.LimitOrder{takers[1].(*order.LimitOrder)}, + Amounts: []uint64{1 * LotSize}, // 2 - 1 + Rates: []uint64{4550000}, + Total: 1 * LotSize, + }, + }, + wantNumPassed: 2, + wantNumFailed: 0, + wantNumPartial: 1, + wantNumInserted: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset Filled amounts of all pre-defined orders before each test. + resetTakers() + resetMakers() + + matches, passed, failed, partial, inserted := me.Match(tt.args.book, tt.args.queue) + matchMade := len(matches) > 0 && matches[0] != nil + if tt.doesMatch != matchMade { + t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) + } + if len(matches) != len(tt.wantMatches) { + t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) + } + for i := range matches { + if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { + t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) + } + } + if len(passed) != tt.wantNumPassed { + t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) + } + if len(failed) != tt.wantNumFailed { + t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) + } + if len(partial) != tt.wantNumPartial { + t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) + } + if len(inserted) != tt.wantNumInserted { + t.Errorf("number inserted %d, expected %d", len(inserted), tt.wantNumInserted) + } + }) + } +} + +func TestMatch_marketSellsOnly(t *testing.T) { + // Setup the match package's logger. + startLogger() + + // New matching engine. + me := New() + + badLotsizeOrder := newMarketSellOrder(1, 0) + badLotsizeOrder.Quantity /= 2 + + // takers is heterogenous w.r.t. type + takers := []order.Order{ + newMarketSellOrder(1, 0), // sell, 1 lot + newMarketSellOrder(5, 0), // sell, 2 lot + newMarketSellOrder(6, 0), // sell, 3 lot, partial maker fill + newMarketSellOrder(99, 0), // sell, 99 lot, partial taker fill + } + + resetTakers := func() { + for _, o := range takers { + switch ot := o.(type) { + case *order.MarketOrder: + ot.Filled = 0 + case *order.LimitOrder: + ot.Filled = 0 + } + } + } + + //nSell := len(bookSellOrders) + nBuy := len(bookBuyOrders) + + type args struct { + book Booker + queue []order.Order + } + tests := []struct { + name string + args args + doesMatch bool + wantMatches []*order.Match + wantNumPassed int + wantNumFailed int + wantNumPartial int + wantNumInserted int + }{ + { + name: "market sell, 1 maker match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[0]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[0], []*order.LimitOrder{bookBuyOrders[nBuy-1]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "market sell, 2 maker match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[1]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[1], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "market sell, 2 maker match, partial taker fill", + args: args{ + book: newBooker(), + queue: []order.Order{takers[2]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[2], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2], bookBuyOrders[nBuy-3]}, 1*LotSize), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "market sell bad lot size", + args: args{ + book: newBooker(), + queue: []order.Order{badLotsizeOrder}, + }, + doesMatch: false, + wantMatches: nil, + wantNumPassed: 0, + wantNumFailed: 1, + wantNumPartial: 0, + wantNumInserted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset Filled amounts of all pre-defined orders before each test. + resetTakers() + resetMakers() + + fmt.Printf("%v\n", takers) + + matches, passed, failed, partial, inserted := me.Match(tt.args.book, tt.args.queue) + matchMade := len(matches) > 0 && matches[0] != nil + if tt.doesMatch != matchMade { + t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) + } + if len(matches) != len(tt.wantMatches) { + t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) + } + for i := range matches { + if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { + t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) + } + } + if len(passed) != tt.wantNumPassed { + t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) + } + if len(failed) != tt.wantNumFailed { + t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) + } + if len(partial) != tt.wantNumPartial { + t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) + } + if len(inserted) != tt.wantNumInserted { + t.Errorf("number inserted %d, expected %d", len(inserted), tt.wantNumInserted) + } + }) + } +} + +func TestMatch_marketBuysOnly(t *testing.T) { + // Setup the match package's logger. + startLogger() + + // New matching engine. + me := New() + + nSell := len(bookSellOrders) + //nBuy := len(bookBuyOrders) + + // marketBuyRate computes the effective price rate if the specified number + // of lots were to be purchased given the current sell order book. + marketBuyRate := func(lots uint64) uint64 { + var weightedRate uint64 + if lots > uint64(len(bookSellOrders)) { + lots = uint64(len(bookSellOrders)) + } + lotsRemaining := lots + var i int + for lotsRemaining > 0 { + orderLots := bookSellOrders[nSell-1-i].Quantity / LotSize + if orderLots > lotsRemaining { + orderLots = lotsRemaining + } + lotsRemaining -= orderLots + i++ + weightedRate += orderLots * bookSellOrders[nSell-1-i].Rate + } + return weightedRate / lots + } + + // buyLotsAmt computes the base asset amount required to purchase the + // specified number of lots, where buying just 1 lot requires a buffer. + buyLotsAmt := func(lots uint64) uint64 { + totalBufferedBase := lots * LotSize + if lots < 2 { + totalBufferedBase += LotSize / 2 + } + return totalBufferedBase + } + + // quoteAmt computes the required amount of the quote asset required to + // purchase the specified number of lots given the current order book and + // required amount buffering in the single lot case. + quoteAmt := func(lots uint64) uint64 { + return BaseToQuote(marketBuyRate(lots), buyLotsAmt(lots)) + } + + // fmt.Printf("%d\n", buyLotsAmt(99)) // exact cost of N lots in base asset + // fmt.Printf("%f\n", quoteAmt(99)) // float cost of N lots in quote asset + // fmt.Printf("%f\n", quoteAmt(99)/marketBuyRate(99)) // back to base asset -- NOTE PRECISION LOSS!!! + + // takers is heterogenous w.r.t. type + takers := []order.Order{ + newMarketBuyOrder(quoteAmt(1), 0), // buy, 1 lot + newMarketBuyOrder(quoteAmt(2), 0), // buy, 2 lot + newMarketBuyOrder(quoteAmt(3), 0), // buy, 3 lot + newMarketBuyOrder(quoteAmt(99), 0), // buy, 99 lot + } + + resetTakers := func() { + for _, o := range takers { + switch ot := o.(type) { + case *order.MarketOrder: + ot.Filled = 0 + case *order.LimitOrder: + ot.Filled = 0 + } + } + } + + bookSellOrdersReverse := make([]*order.LimitOrder, len(bookSellOrders)) + for i := range bookSellOrders { + bookSellOrdersReverse[len(bookSellOrders)-1-i] = bookSellOrders[i] + } + + type args struct { + book Booker + queue []order.Order + } + tests := []struct { + name string + args args + doesMatch bool + wantMatches []*order.Match + wantNumPassed int + wantNumFailed int + wantNumPartial int + wantNumInserted int + }{ + { + name: "market buy, 1 maker match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[0]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[0], []*order.LimitOrder{bookSellOrders[nSell-1]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "market buy, 2 maker match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[1]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[1], []*order.LimitOrder{bookSellOrders[nSell-1], bookSellOrders[nSell-2]}, 1*LotSize), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "market buy, 3 maker match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[2]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[2], []*order.LimitOrder{bookSellOrders[nSell-1], bookSellOrders[nSell-2]}), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + { + name: "market buy, 99 maker match", + args: args{ + book: newBooker(), + queue: []order.Order{takers[3]}, + }, + doesMatch: true, + wantMatches: []*order.Match{ + newMatch(takers[3], bookSellOrdersReverse), + }, + wantNumPassed: 1, + wantNumFailed: 0, + wantNumPartial: 0, + wantNumInserted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset Filled amounts of all pre-defined orders before each test. + resetTakers() + resetMakers() + + fmt.Printf("%v\n", takers) + + matches, passed, failed, partial, inserted := me.Match(tt.args.book, tt.args.queue) + matchMade := len(matches) > 0 && matches[0] != nil + if tt.doesMatch != matchMade { + t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) + } + if len(matches) != len(tt.wantMatches) { + t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) + } + for i := range matches { + if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { + t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) + } + } + if len(passed) != tt.wantNumPassed { + t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) + } + if len(failed) != tt.wantNumFailed { + t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) + } + if len(partial) != tt.wantNumPartial { + t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) + } + if len(inserted) != tt.wantNumInserted { + t.Errorf("number inserted %d, expected %d", len(inserted), tt.wantNumInserted) + } + }) + } +} +func Test_shuffleQueue(t *testing.T) { + // Setup the match package's logger. + startLogger() + + // order queues to be shuffled + q3_1 := []order.Order{ + limitOrders[0], + marketOrders[0], + marketOrders[1], + } + + // q3_2 has same orders as q1 in different order + q3_2 := []order.Order{ + marketOrders[0], + limitOrders[0], + marketOrders[1], + } + + // q3Shuffled is the expected result of sorting q3_1 and q3_2 + q3Shuffled := []order.Order{ + marketOrders[0], + marketOrders[1], + limitOrders[0], + } + + // shuffleQueue should work with nil slice + var qNil []order.Order + + // shuffleQueue should work with empty slice + qEmpty := []order.Order{} + + // shuffleQueue should work with single element slice + q1 := []order.Order{ + marketOrders[0], + } + + // shuffleQueue should work with two element slice + q2_a := []order.Order{ + limitOrders[0], + marketOrders[0], + } + + // ... with same output regardless of input order + q2_b := []order.Order{ + marketOrders[0], + limitOrders[0], + } + + // repeated orders should be handled + qDup := []order.Order{ + marketOrders[0], + marketOrders[0], + } + + q2Shuffled := []order.Order{ + limitOrders[0], + marketOrders[0], + } + + tests := []struct { + name string + inOut []order.Order + want []order.Order + }{ + { + "q3_1 iter 1", + q3_1, + q3Shuffled, + }, + { + "q3_1 iter 2", + q3_1, + q3Shuffled, + }, + { + "q3_1 iter 3", + q3_1, + q3Shuffled, + }, + { + "q3_2", + q3_2, + q3Shuffled, + }, + { + "qEmpty", + qEmpty, + []order.Order{}, + }, + { + "qNil", + qNil, + []order.Order(nil), + }, + { + "q1", + q1, + []order.Order{q2Shuffled[1]}, + }, + { + "q2_a", + q2_a, + q2Shuffled, + }, + { + "q2_b", + q2_b, + q2Shuffled, + }, + { + "qDup", + qDup, + []order.Order{q2Shuffled[1], q2Shuffled[1]}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + shuffleQueue(tt.inOut) + if !reflect.DeepEqual(tt.inOut, tt.want) { + t.Errorf("shuffleQueue(q): q = %#v, want %#v", tt.inOut, tt.want) + } + }) + } +} + +func Test_sortQueue(t *testing.T) { + // Setup the match package's logger. + startLogger() + + // order queues to be sorted + q3_1 := []order.Order{ + limitOrders[0], + marketOrders[0], + marketOrders[1], + } + + // q3_2 has same orders as q1 in different order + q3_2 := []order.Order{ + marketOrders[0], + limitOrders[0], + marketOrders[1], + } + + // q3Sorted is the expected result of sorting q3_1 and q3_2 + q3Sorted := []order.Order{ + limitOrders[0], + marketOrders[0], + marketOrders[1], + } + + // sortQueue should work with nil slice + var qNil []order.Order + + // sortQueue should work with empty slice + qEmpty := []order.Order{} + + // sortQueue should work with single element slice + q1 := []order.Order{ + marketOrders[0], + } + + // sortQueue should work with two element slice + q2_a := []order.Order{ + limitOrders[0], + marketOrders[0], + } + + // ... with same output regardless of input order + q2_b := []order.Order{ + marketOrders[0], + limitOrders[0], + } + + // repeated orders should be handled + qDup := []order.Order{ + marketOrders[0], + marketOrders[0], + } + + q2Sorted := []order.Order{ + limitOrders[0], + marketOrders[0], + } + + tests := []struct { + name string + inOut []order.Order + want []order.Order + }{ + { + "q3_1 iter 1", + q3_1, + q3Sorted, + }, + { + "q3_1 iter 2", + q3_1, + q3Sorted, + }, + { + "q3_1 iter 3", + q3_1, + q3Sorted, + }, + { + "q3_2", + q3_2, + q3Sorted, + }, + { + "qEmpty", + qEmpty, + []order.Order{}, + }, + { + "qNil", + qNil, + []order.Order(nil), + }, + { + "q1", + q1, + []order.Order{q2Sorted[1]}, + }, + { + "q2_a", + q2_a, + q2Sorted, + }, + { + "q2_b", + q2_b, + q2Sorted, + }, + { + "qDup", + qDup, + []order.Order{q2Sorted[1], q2Sorted[1]}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sortQueue(tt.inOut) + if !reflect.DeepEqual(tt.inOut, tt.want) { + t.Errorf("sortQueue(q): q = %#v, want %#v", tt.inOut, tt.want) + } + }) + } +} + +func TestOrdersMatch(t *testing.T) { + // Setup the match package's logger. + startLogger() + + type args struct { + a order.Order + b order.Order + } + tests := []struct { + name string + args args + want bool + }{ + { + "MATCH market buy : limit sell", + args{ + marketOrders[0], + limitOrders[1], + }, + true, + }, + { + "MATCH market sell : limit buy", + args{ + marketOrders[1], + limitOrders[0], + }, + true, + }, + { + "MATCH limit sell : market buy", + args{ + limitOrders[1], + marketOrders[0], + }, + true, + }, + { + "MATCH limit buy : market sell", + args{ + limitOrders[0], + marketOrders[1], + }, + true, + }, + { + "NO MATCH market sell : market buy", + args{ + marketOrders[1], + marketOrders[0], + }, + false, + }, + { + "NO MATCH (rates) limit sell : limit buy", + args{ + limitOrders[0], + limitOrders[1], + }, + false, + }, + { + "NO MATCH (rates) limit buy : limit sell", + args{ + limitOrders[1], + limitOrders[0], + }, + false, + }, + { + "MATCH (overlapping rates) limit sell : limit buy", + args{ + limitOrders[1], + limitOrders[2], + }, + true, + }, + { + "MATCH (same rates) limit sell : limit buy", + args{ + limitOrders[1], + limitOrders[3], + }, + true, + }, + { + "NO MATCH (same side) limit buy : limit buy", + args{ + limitOrders[2], + limitOrders[3], + }, + false, + }, + { + "NO MATCH (cancel) market buy : cancel", + args{ + marketOrders[0], + &order.CancelOrder{}, + }, + false, + }, + { + "NO MATCH (cancel) cancel : market sell", + args{ + &order.CancelOrder{}, + marketOrders[1], + }, + false, + }, + { + "NO MATCH (cancel) limit sell : cancel", + args{ + limitOrders[1], + &order.CancelOrder{}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := OrdersMatch(tt.args.a, tt.args.b); got != tt.want { + t.Errorf("OrdersMatch() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBaseToQuote(t *testing.T) { + type args struct { + rate uint64 + rateFloat float64 + base uint64 + } + tests := []struct { + name string + args args + wantQuote uint64 + }{ + { + name: "ok <1", + args: args{ + rate: 1234132, + rateFloat: 0.01234132, + base: 4200000000, + }, + wantQuote: 51833544, + }, + { + name: "ok 1", + args: args{ + rate: 100000000, + rateFloat: 1.0, + base: 4200000000, + }, + wantQuote: 4200000000, + }, + { + name: "ok >1", + args: args{ + rate: 100000022, + rateFloat: 1.00000022, + base: 4200000000, + }, + wantQuote: 4200000924, + }, + { + name: "ok >>1", + args: args{ + rate: 19900000022, + rateFloat: 199.00000022, + base: 4200000000, + }, + wantQuote: 835800000924, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotQuote := BaseToQuote(tt.args.rate, tt.args.base) + if gotQuote != tt.wantQuote { + t.Errorf("BaseToQuote() = %v, want %v", gotQuote, tt.wantQuote) + } + quote2 := uint64(tt.args.rateFloat * float64(tt.args.base)) + t.Logf("quote from integer rate = %d, from float rate = %d, diff = %d", + gotQuote, quote2, int64(gotQuote-quote2)) + }) + } +} + +func TestQuoteToBase(t *testing.T) { + type args struct { + rate uint64 + rateFloat float64 + quote uint64 + } + tests := []struct { + name string + args args + wantBase uint64 + }{ + { + name: "ok <1", + args: args{ + rate: 1234132, + rateFloat: 0.01234132, + quote: 51833544, + }, + wantBase: 4200000000, + }, + { + name: "ok 1", + args: args{ + rate: 100000000, + rateFloat: 1.0, + quote: 4200000000, + }, + wantBase: 4200000000, + }, + { + name: "ok >1", + args: args{ + rate: 100000022, + rateFloat: 1.00000022, + quote: 4200000924, + }, + wantBase: 4200000000, + }, + { + name: "ok >>1", + args: args{ + rate: 19900000022, + rateFloat: 199.00000022, + quote: 835800000924, + }, + wantBase: 4200000000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBase := QuoteToBase(tt.args.rate, tt.args.quote) + if gotBase != tt.wantBase { + t.Errorf("QuoteToBase() = %v, want %v", gotBase, tt.wantBase) + } + base2 := uint64(float64(tt.args.quote) / tt.args.rateFloat) + t.Logf("base2 from integer rate = %d, from float rate = %d, diff = %d", + gotBase, base2, int64(gotBase-base2)) + }) + } +} + +func TestQuoteToBaseToQuote(t *testing.T) { + type args struct { + rate uint64 + rateFloat float64 + quote uint64 + } + tests := []struct { + name string + args args + }{ + { + name: "ok <1", + args: args{ + rate: 1234132, + rateFloat: 0.01234132, + quote: 51833544, + }, + }, + { + name: "ok 1", + args: args{ + rate: 100000000, + rateFloat: 1.0, + quote: 4200000000, + }, + }, + { + name: "ok >1", + args: args{ + rate: 100000022, + rateFloat: 1.00000022, + quote: 4200000924, + }, + }, + { + name: "ok >>1", + args: args{ + rate: 19900000022, + rateFloat: 199.00000022, + quote: 835800000924, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBase := QuoteToBase(tt.args.rate, tt.args.quote) + gotQuote := BaseToQuote(tt.args.rate, gotBase) + if gotQuote != tt.args.quote { + t.Errorf("Failed quote->base->quote round trip. %d != %d", + gotQuote, tt.args.quote) + } + + baseFlt := float64(tt.args.quote) / tt.args.rateFloat + quoteFlt := baseFlt * tt.args.rateFloat + + t.Logf("expected quote = %d, final quote = %d, float quote = %f", + tt.args.quote, gotQuote, quoteFlt) + }) + } +} + +func TestBaseToQuoteToBase(t *testing.T) { + type args struct { + rate uint64 + rateFloat float64 + base uint64 + } + tests := []struct { + name string + args args + }{ + { + name: "ok <1", + args: args{ + rate: 1234132, + rateFloat: 0.01234132, + base: 4200000000, + }, + }, + { + name: "ok 1", + args: args{ + rate: 100000000, + rateFloat: 1.0, + base: 4200000000, + }, + }, + { + name: "ok >1", + args: args{ + rate: 100000022, + rateFloat: 1.00000022, + base: 4200000000, + }, + }, + { + name: "ok >>1", + args: args{ + rate: 19900000022, + rateFloat: 199.00000022, + base: 4200000000, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotQuote := BaseToQuote(tt.args.rate, tt.args.base) + gotBase := QuoteToBase(tt.args.rate, gotQuote) + if gotBase != tt.args.base { + t.Errorf("Failed base->quote->base round trip. %d != %d", + gotBase, tt.args.base) + } + + quoteFlt := float64(tt.args.base) * tt.args.rateFloat + baseFlt := quoteFlt / tt.args.rateFloat + + t.Logf("expected quote = %d, final quote = %d, float quote = %f", + tt.args.base, gotBase, baseFlt) + }) + } +}