package orderbook import ( "errors" "sort" "sync" "time" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // const values for orderbook package const ( errExchangeOrderbookNotFound = "orderbook for exchange does not exist" errPairNotSet = "orderbook currency pair not set" errAssetTypeNotSet = "orderbook asset type not set" errBaseCurrencyNotFound = "orderbook base currency not found" errQuoteCurrencyNotFound = "orderbook quote currency not found" ) // Vars for the orderbook package var ( Orderbooks []Orderbook m sync.Mutex ) // Item stores the amount and price values type Item struct { Amount float64 Price float64 ID int64 } // Base holds the fields for the orderbook base type Base struct { Pair currency.Pair `json:"pair"` Bids []Item `json:"bids"` Asks []Item `json:"asks"` LastUpdated time.Time `json:"lastUpdated"` AssetType asset.Item `json:"assetType"` ExchangeName string `json:"exchangeName"` } // Orderbook holds the orderbook information for a currency pair and type type Orderbook struct { Orderbook map[*currency.Item]map[*currency.Item]map[asset.Item]Base ExchangeName string } type byOBPrice []Item func (a byOBPrice) Len() int { return len(a) } func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price } // Verify ensures that the orderbook items are correctly sorted // Bids should always go from a high price to a low price and // asks should always go from a low price to a higher price func (o *Base) Verify() { var lastPrice float64 var sortBids, sortAsks bool for x := range o.Bids { if lastPrice != 0 && o.Bids[x].Price >= lastPrice { sortBids = true break } lastPrice = o.Bids[x].Price } lastPrice = 0 for x := range o.Asks { if lastPrice != 0 && o.Asks[x].Price <= lastPrice { sortAsks = true break } lastPrice = o.Asks[x].Price } if sortBids { sort.Sort(sort.Reverse(byOBPrice(o.Bids))) } if sortAsks { sort.Sort((byOBPrice(o.Asks))) } } // TotalBidsAmount returns the total amount of bids and the total orderbook // bids value func (o *Base) TotalBidsAmount() (amountCollated, total float64) { for _, x := range o.Bids { amountCollated += x.Amount total += x.Amount * x.Price } return amountCollated, total } // TotalAsksAmount returns the total amount of asks and the total orderbook // asks value func (o *Base) TotalAsksAmount() (amountCollated, total float64) { for _, x := range o.Asks { amountCollated += x.Amount total += x.Amount * x.Price } return amountCollated, total } // Update updates the bids and asks func (o *Base) Update(bids, asks []Item) { o.Bids = bids o.Asks = asks o.LastUpdated = time.Now() } // Get checks and returns the orderbook given an exchange name and currency pair // if it exists func Get(exchange string, p currency.Pair, orderbookType asset.Item) (Base, error) { orderbook, err := GetByExchange(exchange) if err != nil { return Base{}, err } if !BaseCurrencyExists(exchange, p.Base) { return Base{}, errors.New(errBaseCurrencyNotFound) } if !QuoteCurrencyExists(exchange, p) { return Base{}, errors.New(errQuoteCurrencyNotFound) } return orderbook.Orderbook[p.Base.Item][p.Quote.Item][orderbookType], nil } // GetByExchange returns an exchange orderbook func GetByExchange(exchange string) (*Orderbook, error) { m.Lock() defer m.Unlock() for x := range Orderbooks { if Orderbooks[x].ExchangeName == exchange { return &Orderbooks[x], nil } } return nil, errors.New(errExchangeOrderbookNotFound) } // BaseCurrencyExists checks to see if the base currency of the orderbook map // exists func BaseCurrencyExists(exchange string, currency currency.Code) bool { m.Lock() defer m.Unlock() for _, y := range Orderbooks { if y.ExchangeName == exchange { if _, ok := y.Orderbook[currency.Item]; ok { return true } } } return false } // QuoteCurrencyExists checks to see if the quote currency of the orderbook // map exists func QuoteCurrencyExists(exchange string, p currency.Pair) bool { m.Lock() defer m.Unlock() for _, y := range Orderbooks { if y.ExchangeName == exchange { if _, ok := y.Orderbook[p.Base.Item]; ok { if _, ok := y.Orderbook[p.Base.Item][p.Quote.Item]; ok { return true } } } } return false } // CreateNewOrderbook creates a new orderbook func CreateNewOrderbook(exchangeName string, orderbookNew *Base, orderbookType asset.Item) *Orderbook { m.Lock() defer m.Unlock() orderbook := Orderbook{} orderbook.ExchangeName = exchangeName orderbook.Orderbook = make(map[*currency.Item]map[*currency.Item]map[asset.Item]Base) a := make(map[*currency.Item]map[asset.Item]Base) b := make(map[asset.Item]Base) b[orderbookType] = *orderbookNew a[orderbookNew.Pair.Quote.Item] = b orderbook.Orderbook[orderbookNew.Pair.Base.Item] = a Orderbooks = append(Orderbooks, orderbook) return &orderbook } // Process processes incoming orderbooks, creating or updating the orderbook // list func (o *Base) Process() error { if o.Pair.IsEmpty() { return errors.New(errPairNotSet) } if o.AssetType == "" { return errors.New(errAssetTypeNotSet) } if o.LastUpdated.IsZero() { o.LastUpdated = time.Now() } o.Verify() orderbook, err := GetByExchange(o.ExchangeName) if err != nil { CreateNewOrderbook(o.ExchangeName, o, o.AssetType) return nil } if BaseCurrencyExists(o.ExchangeName, o.Pair.Base) { m.Lock() a := make(map[asset.Item]Base) a[o.AssetType] = *o orderbook.Orderbook[o.Pair.Base.Item][o.Pair.Quote.Item] = a m.Unlock() return nil } m.Lock() a := make(map[*currency.Item]map[asset.Item]Base) b := make(map[asset.Item]Base) b[o.AssetType] = *o a[o.Pair.Quote.Item] = b orderbook.Orderbook[o.Pair.Base.Item] = a m.Unlock() return nil }