diff --git a/.gitignore b/.gitignore index 0e82613dfd..3482948aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ client/cmd/translationsreport/translationsreport client/cmd/translationsreport/worksheets server/cmd/dexadm/dexadm server/cmd/geogame/geogame +client/mm/binance/cmd/lotsizes/lotsizes diff --git a/client/cmd/testbinance/harness_test.go b/client/cmd/testbinance/harness_test.go index 3d13f77ec1..b45aad6e3a 100644 --- a/client/cmd/testbinance/harness_test.go +++ b/client/cmd/testbinance/harness_test.go @@ -15,7 +15,7 @@ import ( "time" "decred.org/dcrdex/client/comms" - "decred.org/dcrdex/client/mm/libxc/bntypes" + "decred.org/dcrdex/client/mm/binance/bntypes" "decred.org/dcrdex/dex" ) @@ -49,7 +49,7 @@ func TestEvmWallet(t *testing.T) { func testWallet(t *testing.T, ctx context.Context, w Wallet) { addr := w.DepositAddress() fmt.Println("##### Deposit address:", addr) - txID, err := w.Send(ctx, addr, 0.1) + txID, err := w.Send(ctx, addr, "", 0.1) if err != nil { t.Fatalf("Send error: %v", err) } @@ -211,7 +211,7 @@ func TestAccountFeed(t *testing.T) { if err != nil { t.Fatalf("Error constructing btc wallet: %v", err) } - txID, err := w.Send(ctx, addrResp.Address, 0.1) + txID, err := w.Send(ctx, addrResp.Address, "", 0.1) if err != nil { t.Fatalf("Send error: %v", err) } diff --git a/client/cmd/testbinance/main.go b/client/cmd/testbinance/main.go index fa977a6f77..7c407879b9 100644 --- a/client/cmd/testbinance/main.go +++ b/client/cmd/testbinance/main.go @@ -23,7 +23,7 @@ import ( "sync/atomic" "time" - "decred.org/dcrdex/client/mm/libxc/bntypes" + "decred.org/dcrdex/client/mm/binance/bntypes" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/encode" "decred.org/dcrdex/dex/fiatrates" diff --git a/client/mm/libxc/binance.go b/client/mm/binance/binance.go similarity index 92% rename from client/mm/libxc/binance.go rename to client/mm/binance/binance.go index 3e8fcc7967..681c1da496 100644 --- a/client/mm/libxc/binance.go +++ b/client/mm/binance/binance.go @@ -1,7 +1,7 @@ // 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 libxc +package binance import ( "bytes" @@ -24,7 +24,8 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/comms" "decred.org/dcrdex/client/core" - "decred.org/dcrdex/client/mm/libxc/bntypes" + "decred.org/dcrdex/client/mm/binance/bntypes" + "decred.org/dcrdex/client/mm/libxc" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/calc" "decred.org/dcrdex/dex/dexnet" @@ -52,6 +53,17 @@ const ( fakeBinanceWsURL = "ws://localhost:37346" ) +var _ libxc.CEXConstructor = (func(cfg *libxc.CEXConfig) libxc.CEX)(nil) + +func init() { + libxc.RegisterCEXConstructor(libxc.Binance, func(cfg *libxc.CEXConfig) libxc.CEX { + return New(cfg, false) + }) + libxc.RegisterCEXConstructor(libxc.BinanceUS, func(cfg *libxc.CEXConfig) libxc.CEX { + return New(cfg, true) + }) +} + // binanceOrderBook manages an orderbook for a single market. It keeps // the orderbook synced and allows querying of vwap. type binanceOrderBook struct { @@ -63,7 +75,7 @@ type binanceOrderBook struct { getSnapshot func() (*bntypes.OrderbookSnapshot, error) - book *orderbook + book *libxc.Orderbook updateQueue chan *bntypes.BookUpdate mktID string baseConversionFactor uint64 @@ -78,7 +90,7 @@ func newBinanceOrderBook( log dex.Logger, ) *binanceOrderBook { return &binanceOrderBook{ - book: newOrderBook(), + book: libxc.NewOrderBook(), mktID: mktID, updateQueue: make(chan *bntypes.BookUpdate, 1024), numSubscribers: 1, @@ -92,9 +104,9 @@ func newBinanceOrderBook( // convertBinanceBook converts bids and asks in the binance format, // with the conventional quantity and rate, to the DEX message format which // can be used to update the orderbook. -func (b *binanceOrderBook) convertBinanceBook(binanceBids, binanceAsks [][2]json.Number) (bids, asks []*obEntry, err error) { - convert := func(updates [][2]json.Number) ([]*obEntry, error) { - convertedUpdates := make([]*obEntry, 0, len(updates)) +func (b *binanceOrderBook) convertBinanceBook(binanceBids, binanceAsks [][2]json.Number) (bids, asks []*libxc.PriceBin, err error) { + convert := func(updates [][2]json.Number) ([]*libxc.PriceBin, error) { + convertedUpdates := make([]*libxc.PriceBin, 0, len(updates)) for _, update := range updates { price, err := update[0].Float64() @@ -107,9 +119,9 @@ func (b *binanceOrderBook) convertBinanceBook(binanceBids, binanceAsks [][2]json return nil, fmt.Errorf("error parsing qty: %v", err) } - convertedUpdates = append(convertedUpdates, &obEntry{ - rate: calc.MessageRateAlt(price, b.baseConversionFactor, b.quoteConversionFactor), - qty: uint64(qty * float64(b.baseConversionFactor)), + convertedUpdates = append(convertedUpdates, &libxc.PriceBin{ + Rate: calc.MessageRateAlt(price, b.baseConversionFactor, b.quoteConversionFactor), + Qty: uint64(qty * float64(b.baseConversionFactor)), }) } @@ -206,7 +218,7 @@ func (b *binanceOrderBook) Connect(ctx context.Context) (*sync.WaitGroup, error // Data is compromised. Trigger a resync. return false } - b.book.update(bids, asks) + b.book.Update(bids, asks) return true } @@ -244,8 +256,8 @@ func (b *binanceOrderBook) Connect(ctx context.Context) (*sync.WaitGroup, error b.log.Debugf("Got %s orderbook snapshot with update ID %d", b.mktID, snapshot.LastUpdateID) - b.book.clear() - b.book.update(bids, asks) + b.book.Clear() + b.book.Update(bids, asks) return processSyncCache(snapshot.LastUpdateID) } @@ -316,62 +328,33 @@ func (b *binanceOrderBook) vwap(bids bool, qty uint64) (vwap, extrema uint64, fi return 0, 0, filled, errors.New("orderbook not synced") } - vwap, extrema, filled = b.book.vwap(bids, qty) + vwap, extrema, filled = b.book.VWAP(bids, qty) return } func (b *binanceOrderBook) midGap() uint64 { - return b.book.midGap() + return b.book.MidGap() } -// TODO: check all symbols -var dexToBinanceSymbol = map[string]string{ - "POLYGON": "MATIC", - "WETH": "ETH", -} - -var binanceToDexSymbol = make(map[string]string) - func convertBnCoin(coin string) string { - symbol := strings.ToLower(coin) - if convertedSymbol, found := binanceToDexSymbol[strings.ToUpper(coin)]; found { - symbol = strings.ToLower(convertedSymbol) - } - return symbol -} - -func convertBnNetwork(network string) string { - symbol := convertBnCoin(network) - if symbol == "weth" { - return "eth" + switch coin { + case "MATIC", "POL": + return "polygon" } - return symbol + return strings.ToLower(coin) } // binanceCoinNetworkToDexSymbol takes the coin name and its network name as // returned by the binance API and returns the DEX symbol. func binanceCoinNetworkToDexSymbol(coin, network string) string { - symbol, netSymbol := convertBnCoin(coin), convertBnNetwork(network) - if symbol == "weth" && netSymbol == "eth" { - return "eth" - } + symbol, netSymbol := convertBnCoin(coin), convertBnCoin(network) if symbol == netSymbol { return symbol } - return symbol + "." + netSymbol -} - -func init() { - for key, value := range dexToBinanceSymbol { - binanceToDexSymbol[value] = key + if symbol == "eth" { + return "weth." + netSymbol } -} - -func mapDexToBinanceSymbol(symbol string) string { - if binanceSymbol, found := dexToBinanceSymbol[symbol]; found { - return binanceSymbol - } - return symbol + return symbol + "." + netSymbol } type bncAssetConfig struct { @@ -394,7 +377,7 @@ func bncAssetCfg(assetID uint32) (*bncAssetConfig, error) { if err != nil { return nil, err } - coin := mapDexToBinanceSymbol(ui.Conventional.Unit) + coin := ui.Conventional.Unit chain := coin if tkn := asset.TokenInfo(assetID); tkn != nil { pui, err := asset.UnitInfo(tkn.ParentID) @@ -408,7 +391,7 @@ func bncAssetCfg(assetID uint32) (*bncAssetConfig, error) { assetID: assetID, symbol: dex.BipIDSymbol(assetID), coin: coin, - chain: mapDexToBinanceSymbol(chain), + chain: chain, conversionFactor: ui.Conventional.ConversionFactor, }, nil } @@ -460,11 +443,11 @@ type binance struct { marketSnapshotMtx sync.Mutex marketSnapshot struct { stamp time.Time - m map[string]*Market + m map[string]*libxc.Market } balanceMtx sync.RWMutex - balances map[uint32]*ExchangeBalance + balances map[uint32]*libxc.ExchangeBalance marketStreamMtx sync.RWMutex marketStream comms.WsConn @@ -474,16 +457,16 @@ type binance struct { tradeUpdaterMtx sync.RWMutex tradeInfo map[string]*tradeInfo - tradeUpdaters map[int]chan *Trade + tradeUpdaters map[int]chan *libxc.Trade tradeUpdateCounter int } -var _ CEX = (*binance)(nil) +var _ libxc.CEX = (*binance)(nil) // TODO: Investigate stablecoin auto-conversion. // https://developers.binance.com/docs/wallet/endpoints/switch-busd-stable-coins-convertion -func newBinance(cfg *CEXConfig, binanceUS bool) *binance { +func New(cfg *libxc.CEXConfig, binanceUS bool) *binance { var marketsURL, accountsURL, wsURL string switch cfg.Net { @@ -515,11 +498,11 @@ func newBinance(cfg *CEXConfig, binanceUS bool) *binance { apiKey: cfg.APIKey, secretKey: cfg.SecretKey, knownAssets: knownAssets, - balances: make(map[uint32]*ExchangeBalance), + balances: make(map[uint32]*libxc.ExchangeBalance), books: make(map[string]*binanceOrderBook), net: cfg.Net, tradeInfo: make(map[string]*tradeInfo), - tradeUpdaters: make(map[int]chan *Trade), + tradeUpdaters: make(map[int]chan *libxc.Trade), tradeIDNoncePrefix: encode.RandomBytes(10), } @@ -556,7 +539,7 @@ func (bnc *binance) refreshBalances(ctx context.Context) error { bnc.log.Errorf("no unit info for known asset ID %d?", assetID) continue } - updatedBalance := &ExchangeBalance{ + updatedBalance := &libxc.ExchangeBalance{ Available: uint64(math.Round(bal.Free * float64(ui.Conventional.ConversionFactor))), Locked: uint64(math.Round(bal.Locked * float64(ui.Conventional.ConversionFactor))), } @@ -576,8 +559,6 @@ func (bnc *binance) refreshBalances(ctx context.Context) error { return nil } -// readCoins stores the token IDs for which deposits and withdrawals are -// enabled on binance and sets the minWithdraw map. func (bnc *binance) readCoins(coins []*bntypes.CoinInfo) { tokenIDs := make(map[string][]uint32) minWithdraw := make(map[uint32]uint64) @@ -610,12 +591,10 @@ func (bnc *binance) readCoins(coins []*bntypes.CoinInfo) { // getCoinInfo retrieves binance configs then updates the user balances and // the tokenIDs. func (bnc *binance) getCoinInfo(ctx context.Context) error { - coins := make([]*bntypes.CoinInfo, 0) - err := bnc.getAPI(ctx, "/sapi/v1/capital/config/getall", nil, true, true, &coins) - if err != nil { + var coins []*bntypes.CoinInfo + if err := bnc.getAPI(ctx, "/sapi/v1/capital/config/getall", nil, true, true, &coins); err != nil { return fmt.Errorf("error getting binance coin info: %w", err) } - bnc.readCoins(coins) return nil } @@ -723,7 +702,7 @@ func (bnc *binance) Connect(ctx context.Context) (*sync.WaitGroup, error) { } // Balance returns the balance of an asset at the CEX. -func (bnc *binance) Balance(assetID uint32) (*ExchangeBalance, error) { +func (bnc *binance) Balance(assetID uint32) (*libxc.ExchangeBalance, error) { assetConfig, err := bncAssetCfg(assetID) if err != nil { return nil, err @@ -748,7 +727,7 @@ func (bnc *binance) generateTradeID() string { // Trade executes a trade on the CEX. subscriptionID takes an ID returned from // SubscribeTradeUpdates. -func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, subscriptionID int) (*Trade, error) { +func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, subscriptionID int) (*libxc.Trade, error) { side := "BUY" if sell { side = "SELL" @@ -818,7 +797,7 @@ func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool success = true - return &Trade{ + return &libxc.Trade{ ID: tradeID, Sell: sell, Rate: rate, @@ -880,7 +859,7 @@ func (bnc *binance) ConfirmWithdrawal(ctx context.Context, withdrawalID string, bnc.log.Tracef("Withdrawal status: %+v", status) if status.TxID == "" { - return 0, "", ErrWithdrawalPending + return 0, "", libxc.ErrWithdrawalPending } amt := status.Amount * float64(assetCfg.conversionFactor) @@ -943,7 +922,7 @@ func (bnc *binance) GetDepositAddress(ctx context.Context, assetID uint32) (stri // ConfirmDeposit is an async function that calls onConfirm when the status of // a deposit has been confirmed. -func (bnc *binance) ConfirmDeposit(ctx context.Context, deposit *DepositData) (bool, uint64) { +func (bnc *binance) ConfirmDeposit(ctx context.Context, deposit *libxc.DepositData) (bool, uint64) { var resp []*bntypes.PendingDeposit // We'll add info for the fake server. var query url.Values @@ -1008,12 +987,12 @@ func (bnc *binance) ConfirmDeposit(ctx context.Context, deposit *DepositData) (b // returned from this function is passed as the updaterID argument to // Trade, then updates to the trade will be sent on the updated channel // returned from this function. -func (bnc *binance) SubscribeTradeUpdates() (<-chan *Trade, func(), int) { +func (bnc *binance) SubscribeTradeUpdates() (<-chan *libxc.Trade, func(), int) { bnc.tradeUpdaterMtx.Lock() defer bnc.tradeUpdaterMtx.Unlock() updaterID := bnc.tradeUpdateCounter bnc.tradeUpdateCounter++ - updater := make(chan *Trade, 256) + updater := make(chan *libxc.Trade, 256) bnc.tradeUpdaters[updaterID] = updater unsubscribe := func() { @@ -1051,7 +1030,7 @@ func (bnc *binance) CancelTrade(ctx context.Context, baseID, quoteID uint32, tra return requestInto(req, &struct{}{}) } -func (bnc *binance) Balances(ctx context.Context) (map[uint32]*ExchangeBalance, error) { +func (bnc *binance) Balances(ctx context.Context) (map[uint32]*libxc.ExchangeBalance, error) { bnc.balanceMtx.RLock() defer bnc.balanceMtx.RUnlock() @@ -1061,7 +1040,7 @@ func (bnc *binance) Balances(ctx context.Context) (map[uint32]*ExchangeBalance, } } - balances := make(map[uint32]*ExchangeBalance) + balances := make(map[uint32]*libxc.ExchangeBalance) for assetID, bal := range bnc.balances { assetConfig, err := bncAssetCfg(assetID) @@ -1084,7 +1063,7 @@ func (bnc *binance) minimumWithdraws(baseID, quoteID uint32) (uint64, uint64) { return mins[baseID], mins[quoteID] } -func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) { +func (bnc *binance) Markets(ctx context.Context) (map[string]*libxc.Market, error) { bnc.marketSnapshotMtx.Lock() defer bnc.marketSnapshotMtx.Unlock() @@ -1098,7 +1077,7 @@ func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) { return nil, fmt.Errorf("error getting market list for market data request: %w", err) } - mkts := make(map[string][]*MarketMatch, len(matches)) + mkts := make(map[string][]*libxc.MarketMatch, len(matches)) for _, m := range matches { mkts[m.Slug] = append(mkts[m.Slug], m) } @@ -1115,7 +1094,7 @@ func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) { return nil, err } - m := make(map[string]*Market, len(ds)) + m := make(map[string]*libxc.Market, len(ds)) for _, d := range ds { ms, found := mkts[d.Symbol] if !found { @@ -1124,12 +1103,12 @@ func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) { } for _, mkt := range ms { baseMinWithdraw, quoteMinWithdraw := bnc.minimumWithdraws(mkt.BaseID, mkt.QuoteID) - m[mkt.MarketID] = &Market{ + m[mkt.MarketID] = &libxc.Market{ BaseID: mkt.BaseID, QuoteID: mkt.QuoteID, BaseMinWithdraw: baseMinWithdraw, QuoteMinWithdraw: quoteMinWithdraw, - Day: &MarketDay{ + Day: &libxc.MarketDay{ Vol: d.Volume, QuoteVol: d.QuoteVolume, PriceChange: d.PriceChange, @@ -1148,7 +1127,7 @@ func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) { return m, nil } -func (bnc *binance) MatchedMarkets(ctx context.Context) (_ []*MarketMatch, err error) { +func (bnc *binance) MatchedMarkets(ctx context.Context) (_ []*libxc.MarketMatch, err error) { if tokenIDsI := bnc.tokenIDs.Load(); tokenIDsI == nil { if err := bnc.getCoinInfo(ctx); err != nil { return nil, fmt.Errorf("error getting coin info for token IDs: %v", err) @@ -1163,10 +1142,32 @@ func (bnc *binance) MatchedMarkets(ctx context.Context) (_ []*MarketMatch, err e return nil, fmt.Errorf("error getting markets: %v", err) } } - markets := make([]*MarketMatch, 0, len(bnMarkets)) + markets := make([]*libxc.MarketMatch, 0, len(bnMarkets)) + + lotSize := func(mkt *bntypes.Market) uint64 { + var assetID uint32 + if tids := tokenIDs[mkt.BaseAsset]; len(tids) > 0 { + assetID = tids[0] + } else { + var found bool + if assetID, found = dex.BipSymbolID(convertBnCoin(mkt.BaseAsset)); !found { + return 0 + } + } + ui, err := asset.UnitInfo(assetID) + if err != nil { + return 0 + } + for _, filt := range mkt.Filters { + if filt.FilterType == "LOT_SIZE" { + return uint64(math.Round(filt.MinQty * float64(ui.Conventional.ConversionFactor))) + } + } + return 0 + } for _, mkt := range bnMarkets { - dexMarkets := binanceMarketToDexMarkets(mkt.BaseAsset, mkt.QuoteAsset, tokenIDs, bnc.isUS) + dexMarkets := binanceMarketToDexMarkets(mkt.BaseAsset, mkt.QuoteAsset, tokenIDs, lotSize(mkt), bnc.isUS) markets = append(markets, dexMarkets...) } @@ -1250,7 +1251,7 @@ func (bnc *binance) handleOutboundAccountPosition(update *bntypes.StreamUpdate) } supportedTokens := bnc.tokenIDs.Load().(map[string][]uint32) - updates := make([]*BalanceUpdate, 0, len(update.Balances)) + updates := make([]*libxc.BalanceUpdate, 0, len(update.Balances)) processSymbol := func(symbol string, bal *bntypes.WSBalance) { for _, assetID := range getDEXAssetIDs(symbol, supportedTokens) { @@ -1260,13 +1261,13 @@ func (bnc *binance) handleOutboundAccountPosition(update *bntypes.StreamUpdate) return } oldBal := bnc.balances[assetID] - newBal := &ExchangeBalance{ + newBal := &libxc.ExchangeBalance{ Available: uint64(math.Round(bal.Free * float64(ui.Conventional.ConversionFactor))), Locked: uint64(math.Round(bal.Locked * float64(ui.Conventional.ConversionFactor))), } bnc.balances[assetID] = newBal if oldBal != nil && *oldBal != *newBal { - updates = append(updates, &BalanceUpdate{ + updates = append(updates, &libxc.BalanceUpdate{ AssetID: assetID, Balance: newBal, }) @@ -1288,7 +1289,7 @@ func (bnc *binance) handleOutboundAccountPosition(update *bntypes.StreamUpdate) } } -func (bnc *binance) getTradeUpdater(tradeID string) (chan *Trade, *tradeInfo, error) { +func (bnc *binance) getTradeUpdater(tradeID string) (chan *libxc.Trade, *tradeInfo, error) { bnc.tradeUpdaterMtx.RLock() defer bnc.tradeUpdaterMtx.RUnlock() @@ -1341,7 +1342,7 @@ func (bnc *binance) handleExecutionReport(update *bntypes.StreamUpdate) { return } - updater <- &Trade{ + updater <- &libxc.Trade{ ID: id, Complete: complete, Rate: tradeInfo.rate, @@ -1779,16 +1780,16 @@ func (bnc *binance) Book(baseID, quoteID uint32) (buys, sells []*core.MiniOrder, if err != nil { return nil, nil, err } - bids, asks := book.book.snap() + bids, asks := book.book.Snap() bFactor := float64(book.baseConversionFactor) - convertSide := func(side []*obEntry, sell bool) []*core.MiniOrder { + convertSide := func(side []*libxc.PriceBin, sell bool) []*core.MiniOrder { ords := make([]*core.MiniOrder, len(side)) for i, e := range side { ords[i] = &core.MiniOrder{ - Qty: float64(e.qty) / bFactor, - QtyAtomic: e.qty, - Rate: calc.ConventionalRateAlt(e.rate, book.baseConversionFactor, book.quoteConversionFactor), - MsgRate: e.rate, + Qty: float64(e.Qty) / bFactor, + QtyAtomic: e.Qty, + Rate: calc.ConventionalRateAlt(e.Rate, book.baseConversionFactor, book.quoteConversionFactor), + MsgRate: e.Rate, Sell: sell, } } @@ -1820,7 +1821,7 @@ func (bnc *binance) MidGap(baseID, quoteID uint32) uint64 { } // TradeStatus returns the current status of a trade. -func (bnc *binance) TradeStatus(ctx context.Context, tradeID string, baseID, quoteID uint32) (*Trade, error) { +func (bnc *binance) TradeStatus(ctx context.Context, tradeID string, baseID, quoteID uint32) (*libxc.Trade, error) { baseAsset, err := bncAssetCfg(baseID) if err != nil { return nil, err @@ -1841,7 +1842,7 @@ func (bnc *binance) TradeStatus(ctx context.Context, tradeID string, baseID, quo return nil, err } - return &Trade{ + return &libxc.Trade{ ID: tradeID, Sell: resp.Side == "SELL", Rate: calc.MessageRateAlt(resp.Price, baseAsset.conversionFactor, quoteAsset.conversionFactor), @@ -1899,7 +1900,7 @@ func assetDisabled(isUS bool, assetID uint32) bool { // A symbol represents a single market on the CEX, but tokens on the DEX // have a different assetID for each network they are on, therefore they will // match multiple markets as defined using assetID. -func binanceMarketToDexMarkets(binanceBaseSymbol, binanceQuoteSymbol string, tokenIDs map[string][]uint32, isUS bool) []*MarketMatch { +func binanceMarketToDexMarkets(binanceBaseSymbol, binanceQuoteSymbol string, tokenIDs map[string][]uint32, lotSize uint64, isUS bool) []*libxc.MarketMatch { var baseAssetIDs, quoteAssetIDs []uint32 baseAssetIDs = getDEXAssetIDs(binanceBaseSymbol, tokenIDs) @@ -1912,17 +1913,18 @@ func binanceMarketToDexMarkets(binanceBaseSymbol, binanceQuoteSymbol string, tok return nil } - markets := make([]*MarketMatch, 0, len(baseAssetIDs)*len(quoteAssetIDs)) + markets := make([]*libxc.MarketMatch, 0, len(baseAssetIDs)*len(quoteAssetIDs)) for _, baseID := range baseAssetIDs { for _, quoteID := range quoteAssetIDs { if assetDisabled(isUS, baseID) || assetDisabled(isUS, quoteID) { continue } - markets = append(markets, &MarketMatch{ + markets = append(markets, &libxc.MarketMatch{ Slug: binanceBaseSymbol + binanceQuoteSymbol, MarketID: dex.BipIDSymbol(baseID) + "_" + dex.BipIDSymbol(quoteID), BaseID: baseID, QuoteID: quoteID, + LotSize: lotSize, }) } } diff --git a/client/mm/libxc/binance_live_test.go b/client/mm/binance/binance_live_test.go similarity index 91% rename from client/mm/libxc/binance_live_test.go rename to client/mm/binance/binance_live_test.go index 109d83c7c1..2470cf050a 100644 --- a/client/mm/libxc/binance_live_test.go +++ b/client/mm/binance/binance_live_test.go @@ -1,13 +1,13 @@ //go:build bnclive -package libxc +package binance import ( "context" "encoding/json" + "flag" "fmt" "os" - "os/user" "strings" "sync" "testing" @@ -15,18 +15,32 @@ import ( "decred.org/dcrdex/client/asset" _ "decred.org/dcrdex/client/asset/importall" - "decred.org/dcrdex/client/mm/libxc/bntypes" + "decred.org/dcrdex/client/mm/binance/bntypes" + "decred.org/dcrdex/client/mm/libxc" "decred.org/dcrdex/dex" ) var ( log = dex.StdOutLogger("T", dex.LevelTrace) - u, _ = user.Current() - apiKey = "" - apiSecret = "" + binanceUS = true + net = dex.Mainnet + apiKey string + apiSecret string ) func TestMain(m *testing.M) { + var global, testnet bool + flag.BoolVar(&global, "global", false, "use Binance global") + flag.BoolVar(&testnet, "testnet", false, "use testnet") + flag.Parse() + + if global { + binanceUS = false + } + if testnet { + net = dex.Testnet + } + if s := os.Getenv("SECRET"); s != "" { apiSecret = s } @@ -37,8 +51,8 @@ func TestMain(m *testing.M) { m.Run() } -func tNewBinance(t *testing.T, net dex.Network) *binance { - cfg := &CEXConfig{ +func tNewBinance() *binance { + cfg := &libxc.CEXConfig{ Net: net, APIKey: apiKey, SecretKey: apiSecret, @@ -47,8 +61,7 @@ func tNewBinance(t *testing.T, net dex.Network) *binance { log.Infof("Notification sent: %+v", n) }, } - const binanceUS = true - return newBinance(cfg, binanceUS) + return New(cfg, binanceUS) } type spoofDriver struct { @@ -74,7 +87,7 @@ func (drv *spoofDriver) Info() *asset.WalletInfo { } func TestConnect(t *testing.T) { - bnc := tNewBinance(t, dex.Simnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -99,7 +112,7 @@ func TestConnect(t *testing.T) { // This may fail due to balance being to low. You can try switching the side // of the trade or the qty. func TestTrade(t *testing.T) { - bnc := tNewBinance(t, dex.Testnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() _, err := bnc.Connect(ctx) @@ -150,7 +163,7 @@ func TestTrade(t *testing.T) { func TestCancelTrade(t *testing.T) { tradeID := "42641326270691d752e000000001" - bnc := tNewBinance(t, dex.Testnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() _, err := bnc.Connect(ctx) @@ -165,7 +178,7 @@ func TestCancelTrade(t *testing.T) { } func TestMatchedMarkets(t *testing.T) { - bnc := tNewBinance(t, dex.Mainnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -185,7 +198,7 @@ func TestMatchedMarkets(t *testing.T) { } func TestVWAP(t *testing.T) { - bnc := tNewBinance(t, dex.Mainnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() _, err := bnc.Connect(ctx) @@ -250,7 +263,7 @@ func TestVWAP(t *testing.T) { } func TestSubscribeMarket(t *testing.T) { - bnc := tNewBinance(t, dex.Testnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() wg, err := bnc.Connect(ctx) @@ -267,7 +280,7 @@ func TestSubscribeMarket(t *testing.T) { } func TestWithdrawal(t *testing.T) { - bnc := tNewBinance(t, dex.Mainnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -286,7 +299,7 @@ func TestWithdrawal(t *testing.T) { } func TestConfirmDeposit(t *testing.T) { - bnc := tNewBinance(t, dex.Mainnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -295,12 +308,12 @@ func TestConfirmDeposit(t *testing.T) { t.Fatalf("Connect error: %v", err) } - confirmed, amt := bnc.ConfirmDeposit(ctx, &DepositData{}) + confirmed, amt := bnc.ConfirmDeposit(ctx, &libxc.DepositData{}) t.Logf("confirmed: %v, amt: %v", confirmed, amt) } func TestGetDepositAddress(t *testing.T) { - bnc := tNewBinance(t, dex.Mainnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -318,7 +331,7 @@ func TestGetDepositAddress(t *testing.T) { } func TestBalances(t *testing.T) { - bnc := tNewBinance(t, dex.Testnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -336,7 +349,7 @@ func TestBalances(t *testing.T) { } func TestGetCoinInfo(t *testing.T) { - bnc := tNewBinance(t, dex.Mainnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -371,7 +384,7 @@ func TestGetCoinInfo(t *testing.T) { } func TestTradeStatus(t *testing.T) { - bnc := tNewBinance(t, dex.Testnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() @@ -390,7 +403,7 @@ func TestTradeStatus(t *testing.T) { func TestMarkets(t *testing.T) { // Need keys for getCoinInfo - bnc := tNewBinance(t, dex.Testnet) + bnc := tNewBinance() ctx, cancel := context.WithTimeout(context.Background(), time.Hour*23) defer cancel() diff --git a/client/mm/libxc/binance_test.go b/client/mm/binance/binance_test.go similarity index 92% rename from client/mm/libxc/binance_test.go rename to client/mm/binance/binance_test.go index 7c80842cfe..5886852aa1 100644 --- a/client/mm/libxc/binance_test.go +++ b/client/mm/binance/binance_test.go @@ -1,15 +1,17 @@ // 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 libxc +package binance import ( "testing" + + "decred.org/dcrdex/client/mm/libxc" ) func TestSubscribeTradeUpdates(t *testing.T) { bn := &binance{ - tradeUpdaters: make(map[int]chan *Trade), + tradeUpdaters: make(map[int]chan *libxc.Trade), } _, unsub0, _ := bn.SubscribeTradeUpdates() _, _, id1 := bn.SubscribeTradeUpdates() diff --git a/client/mm/libxc/bntypes/types.go b/client/mm/binance/bntypes/types.go similarity index 82% rename from client/mm/libxc/bntypes/types.go rename to client/mm/binance/bntypes/types.go index 1f4515fde2..dc42a7c644 100644 --- a/client/mm/libxc/bntypes/types.go +++ b/client/mm/binance/bntypes/types.go @@ -10,7 +10,69 @@ type Market struct { QuoteAsset string `json:"quoteAsset"` QuoteAssetPrecision int `json:"quoteAssetPrecision"` OrderTypes []string `json:"orderTypes"` -} + Filters []struct { + FilterType string `json:"filterType"` + MinQty float64 `json:"minQty,string"` + MaxQty float64 `json:"maxQty,string"` + StepSize float64 `json:"stepSize,string"` + } `json:"filters"` +} + +// "filters": [ +// { +// "filterType": "PRICE_FILTER", +// "minPrice": "0.00010000", +// "maxPrice": "1000.00000000", +// "tickSize": "0.00010000" +// }, +// { +// "filterType": "LOT_SIZE", +// "minQty": "0.10000000", +// "maxQty": "92141578.00000000", +// "stepSize": "0.10000000" +// }, +// { +// "filterType": "ICEBERG_PARTS", +// "limit": 10 +// }, +// { +// "filterType": "MARKET_LOT_SIZE", +// "minQty": "0.00000000", +// "maxQty": "14989.56610878", +// "stepSize": "0.00000000" +// }, +// { +// "filterType": "TRAILING_DELTA", +// "minTrailingAboveDelta": 10, +// "maxTrailingAboveDelta": 2000, +// "minTrailingBelowDelta": 10, +// "maxTrailingBelowDelta": 2000 +// }, +// { +// "filterType": "PERCENT_PRICE_BY_SIDE", +// "bidMultiplierUp": "5", +// "bidMultiplierDown": "0.2", +// "askMultiplierUp": "5", +// "askMultiplierDown": "0.2", +// "avgPriceMins": 5 +// }, +// { +// "filterType": "NOTIONAL", +// "minNotional": "10.00000000", +// "applyMinToMarket": true, +// "maxNotional": "9000000.00000000", +// "applyMaxToMarket": false, +// "avgPriceMins": 5 +// }, +// { +// "filterType": "MAX_NUM_ORDERS", +// "maxNumOrders": 200 +// }, +// { +// "filterType": "MAX_NUM_ALGO_ORDERS", +// "maxNumAlgoOrders": 5 +// } +// ], type Balance struct { Asset string `json:"asset"` diff --git a/client/mm/binance/cmd/lotsizes/main.go b/client/mm/binance/cmd/lotsizes/main.go new file mode 100644 index 0000000000..df9f3d0cbf --- /dev/null +++ b/client/mm/binance/cmd/lotsizes/main.go @@ -0,0 +1,69 @@ +// 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 main + +import ( + "context" + "flag" + "fmt" + "os" + + "decred.org/dcrdex/client/asset" + _ "decred.org/dcrdex/client/asset/importall" + "decred.org/dcrdex/client/mm/binance" + "decred.org/dcrdex/client/mm/libxc" + "decred.org/dcrdex/dex" +) + +func main() { + if err := mainErr(); err != nil { + fmt.Fprint(os.Stderr, err, "\n") + os.Exit(1) + } + os.Exit(0) +} + +func mainErr() error { + var global bool + flag.BoolVar(&global, "global", false, "use Binance Global (binance.com)") + flag.Parse() + + apiKey, apiSecret := os.Getenv("KEY"), os.Getenv("SECRET") + if len(apiKey) == 0 || len(apiSecret) == 0 { + return fmt.Errorf("must specify both KEY and SECRET as environment variables") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + bn := binance.New(&libxc.CEXConfig{ + Net: dex.Mainnet, + APIKey: apiKey, + SecretKey: apiSecret, + Logger: dex.StdOutLogger("BN", dex.LevelWarn, false), + Notify: func(i interface{}) { + // Do nothing + }, + }, !global) + + mkts, err := bn.MatchedMarkets(ctx) + if err != nil { + return fmt.Errorf("error getting market info: %w", err) + } + + marketPrinted := make(map[string]bool) + + for _, mkt := range mkts { + if marketPrinted[mkt.Slug] { + continue + } + marketPrinted[mkt.Slug] = true + ui, err := asset.UnitInfo(mkt.BaseID) + if err != nil { + return fmt.Errorf("no unit info for asset %d", mkt.BaseID) + } + fmt.Printf("%s: %d %s (%s %s)\n", mkt.Slug, mkt.LotSize, ui.AtomicUnit, ui.ConventionalString(mkt.LotSize), ui.Conventional.Unit) + } + return nil +} diff --git a/client/mm/libxc/interface.go b/client/mm/libxc/interface.go index b6c1e25f66..67abc49e83 100644 --- a/client/mm/libxc/interface.go +++ b/client/mm/libxc/interface.go @@ -69,6 +69,8 @@ type MarketMatch struct { MarketID string `json:"marketID"` // Slug is a market identifier used by the cex. Slug string `json:"slug"` + // LotSize is the minimum trade size for the market. + LotSize uint64 `json:"lotSize"` } type BalanceUpdate struct { @@ -153,14 +155,19 @@ type CEXConfig struct { Notify func(interface{}) } +type CEXConstructor func(cfg *CEXConfig) CEX + +var cexConstructors = map[string]CEXConstructor{} + +func RegisterCEXConstructor(s string, f CEXConstructor) { + cexConstructors[s] = f +} + // NewCEX creates a new CEX. func NewCEX(cexName string, cfg *CEXConfig) (CEX, error) { - switch cexName { - case Binance: - return newBinance(cfg, false), nil - case BinanceUS: - return newBinance(cfg, true), nil - default: + c, found := cexConstructors[cexName] + if !found { return nil, fmt.Errorf("unrecognized CEX: %v", cexName) } + return c(cfg), nil } diff --git a/client/mm/libxc/orderbook.go b/client/mm/libxc/orderbook.go index 78eced2cf3..b9138eae18 100644 --- a/client/mm/libxc/orderbook.go +++ b/client/mm/libxc/orderbook.go @@ -10,9 +10,9 @@ import ( "github.com/huandu/skiplist" ) -type obEntry struct { - qty uint64 - rate uint64 +type PriceBin struct { + Qty uint64 + Rate uint64 } // obEntryComparable is a skiplist.Comparable implementation for @@ -25,14 +25,14 @@ const asksComparable = obEntryComparable(1) var _ skiplist.Comparable = (*obEntryComparable)(nil) func (o obEntryComparable) Compare(lhs, rhs interface{}) int { - lhsEntry := lhs.(*obEntry) - rhsEntry := rhs.(*obEntry) + lhsEntry := lhs.(*PriceBin) + rhsEntry := rhs.(*PriceBin) var toReturn int - if lhsEntry.rate < rhsEntry.rate { + if lhsEntry.Rate < rhsEntry.Rate { toReturn = -1 } - if lhsEntry.rate > rhsEntry.rate { + if lhsEntry.Rate > rhsEntry.Rate { toReturn = 1 } @@ -45,52 +45,52 @@ func (o obEntryComparable) Compare(lhs, rhs interface{}) int { func (o obEntryComparable) CalcScore(key interface{}) float64 { if o == bidsComparable { - return math.MaxFloat64 - float64(key.(*obEntry).rate) + return math.MaxFloat64 - float64(key.(*PriceBin).Rate) } else { - return float64(key.(*obEntry).rate) + return float64(key.(*PriceBin).Rate) } } -// orderbook is an implementation of a limit order book that allows for quick +// Orderbook is an implementation of a limit order book that allows for quick // updates and calculation of the volume weighted average price (VWAP). -type orderbook struct { +type Orderbook struct { mtx sync.RWMutex bids skiplist.SkipList asks skiplist.SkipList } -func newOrderBook() *orderbook { - return &orderbook{ +func NewOrderBook() *Orderbook { + return &Orderbook{ bids: *skiplist.New(bidsComparable), asks: *skiplist.New(asksComparable), } } -func (ob *orderbook) String() string { +func (ob *Orderbook) String() string { ob.mtx.RLock() defer ob.mtx.RUnlock() - bids := make([]obEntry, 0, ob.bids.Len()) + bids := make([]PriceBin, 0, ob.bids.Len()) for curr := ob.bids.Front(); curr != nil; curr = curr.Next() { - bids = append(bids, *curr.Value.(*obEntry)) + bids = append(bids, *curr.Value.(*PriceBin)) } - asks := make([]obEntry, 0, ob.asks.Len()) + asks := make([]PriceBin, 0, ob.asks.Len()) for curr := ob.asks.Front(); curr != nil; curr = curr.Next() { - asks = append(asks, *curr.Value.(*obEntry)) + asks = append(asks, *curr.Value.(*PriceBin)) } return fmt.Sprintf("bids: %v, asks: %v", bids, asks) } -// update updates the orderbook new quantities at the given rates. +// Update updates the orderbook new quantities at the given rates. // If the quantity is 0, the entry is removed from the orderbook. -func (ob *orderbook) update(bids []*obEntry, asks []*obEntry) { +func (ob *Orderbook) Update(bids []*PriceBin, asks []*PriceBin) { ob.mtx.Lock() defer ob.mtx.Unlock() for _, entry := range bids { - if entry.qty == 0 { + if entry.Qty == 0 { ob.bids.Remove(entry) continue } @@ -98,7 +98,7 @@ func (ob *orderbook) update(bids []*obEntry, asks []*obEntry) { } for _, entry := range asks { - if entry.qty == 0 { + if entry.Qty == 0 { ob.asks.Remove(entry) continue } @@ -106,7 +106,7 @@ func (ob *orderbook) update(bids []*obEntry, asks []*obEntry) { } } -func (ob *orderbook) clear() { +func (ob *Orderbook) Clear() { ob.mtx.Lock() defer ob.mtx.Unlock() @@ -114,7 +114,7 @@ func (ob *orderbook) clear() { ob.asks = *skiplist.New(asksComparable) } -func (ob *orderbook) vwap(bids bool, qty uint64) (vwap, extrema uint64, filled bool) { +func (ob *Orderbook) VWAP(bids bool, qty uint64) (vwap, extrema uint64, filled bool) { ob.mtx.RLock() defer ob.mtx.RUnlock() @@ -131,15 +131,15 @@ func (ob *orderbook) vwap(bids bool, qty uint64) (vwap, extrema uint64, filled b if curr == nil { break } - entry := curr.Value.(*obEntry) - extrema = entry.rate - if entry.qty >= remaining { + entry := curr.Value.(*PriceBin) + extrema = entry.Rate + if entry.Qty >= remaining { filled = true weightedSum += remaining * extrema break } - remaining -= entry.qty - weightedSum += entry.qty * extrema + remaining -= entry.Qty + weightedSum += entry.Qty * extrema } if !filled { return 0, 0, false @@ -148,7 +148,7 @@ func (ob *orderbook) vwap(bids bool, qty uint64) (vwap, extrema uint64, filled b return weightedSum / qty, extrema, filled } -func (ob *orderbook) midGap() uint64 { +func (ob *Orderbook) MidGap() uint64 { ob.mtx.RLock() defer ob.mtx.RUnlock() @@ -160,23 +160,23 @@ func (ob *orderbook) midGap() uint64 { if bestSellI == nil { return 0 } - bestBuy, bestSell := bestBuyI.Value.(*obEntry), bestSellI.Value.(*obEntry) - return (bestBuy.rate + bestSell.rate) / 2 + bestBuy, bestSell := bestBuyI.Value.(*PriceBin), bestSellI.Value.(*PriceBin) + return (bestBuy.Rate + bestSell.Rate) / 2 } -// snap generates a snapshot of the book. -func (ob *orderbook) snap() (bids, asks []*obEntry) { +// Snap generates a snapshot of the book. +func (ob *Orderbook) Snap() (bids, asks []*PriceBin) { ob.mtx.RLock() defer ob.mtx.RUnlock() - bids = make([]*obEntry, 0, ob.bids.Len()) + bids = make([]*PriceBin, 0, ob.bids.Len()) for curr := ob.bids.Front(); curr != nil; curr = curr.Next() { - bids = append(bids, curr.Value.(*obEntry)) + bids = append(bids, curr.Value.(*PriceBin)) } - asks = make([]*obEntry, 0, ob.asks.Len()) + asks = make([]*PriceBin, 0, ob.asks.Len()) for curr := ob.asks.Front(); curr != nil; curr = curr.Next() { - asks = append(asks, curr.Value.(*obEntry)) + asks = append(asks, curr.Value.(*PriceBin)) } return bids, asks diff --git a/client/mm/libxc/orderbook_test.go b/client/mm/libxc/orderbook_test.go index 41499ab0bb..429bf89ab4 100644 --- a/client/mm/libxc/orderbook_test.go +++ b/client/mm/libxc/orderbook_test.go @@ -6,14 +6,14 @@ package libxc import "testing" func TestOrderbook(t *testing.T) { - ob := newOrderBook() + ob := NewOrderBook() // Test vwap on empty books - _, _, filled := ob.vwap(true, 1) + _, _, filled := ob.VWAP(true, 1) if filled { t.Fatalf("empty book should not be filled") } - _, _, filled = ob.vwap(false, 1) + _, _, filled = ob.VWAP(false, 1) if filled { t.Fatalf("empty book should not be filled") } @@ -21,18 +21,18 @@ func TestOrderbook(t *testing.T) { // Populate the book with some bids and asks. They both // have the same values, but VWAP for asks should be // calculate from the lower values first. - ob.update([]*obEntry{ - {qty: 30, rate: 4000}, - {qty: 30, rate: 5000}, - {qty: 80, rate: 400}, - {qty: 10, rate: 3000}, - }, []*obEntry{ - {qty: 30, rate: 4000}, - {qty: 30, rate: 5000}, - {qty: 80, rate: 400}, - {qty: 10, rate: 3000}, + ob.Update([]*PriceBin{ + {Qty: 30, Rate: 4000}, + {Qty: 30, Rate: 5000}, + {Qty: 80, Rate: 400}, + {Qty: 10, Rate: 3000}, + }, []*PriceBin{ + {Qty: 30, Rate: 4000}, + {Qty: 30, Rate: 5000}, + {Qty: 80, Rate: 400}, + {Qty: 10, Rate: 3000}, }) - vwap, extrema, filled := ob.vwap(true, 65) + vwap, extrema, filled := ob.VWAP(true, 65) if !filled { t.Fatalf("should be filled") } @@ -44,7 +44,7 @@ func TestOrderbook(t *testing.T) { t.Fatalf("wrong extrema") } - vwap, extrema, filled = ob.vwap(false, 65) + vwap, extrema, filled = ob.VWAP(false, 65) if !filled { t.Fatalf("should be filled") } @@ -57,25 +57,25 @@ func TestOrderbook(t *testing.T) { } // Tests querying more quantity than on books - _, _, filled = ob.vwap(true, 161) + _, _, filled = ob.VWAP(true, 161) if filled { t.Fatalf("should not be filled") } - _, _, filled = ob.vwap(false, 161) + _, _, filled = ob.VWAP(false, 161) if filled { t.Fatalf("should not be filled") } // Update quantities. Setting qty to 0 should delete. - ob.update([]*obEntry{ - {qty: 0, rate: 5000}, - {qty: 50, rate: 4000}, - }, []*obEntry{ - {qty: 0, rate: 400}, - {qty: 35, rate: 4000}, + ob.Update([]*PriceBin{ + {Qty: 0, Rate: 5000}, + {Qty: 50, Rate: 4000}, + }, []*PriceBin{ + {Qty: 0, Rate: 400}, + {Qty: 35, Rate: 4000}, }) - vwap, extrema, filled = ob.vwap(true, 65) + vwap, extrema, filled = ob.VWAP(true, 65) if !filled { t.Fatalf("should be filled") } @@ -87,7 +87,7 @@ func TestOrderbook(t *testing.T) { t.Fatalf("wrong extrema") } - vwap, extrema, filled = ob.vwap(false, 65) + vwap, extrema, filled = ob.VWAP(false, 65) if !filled { t.Fatalf("should be filled") } diff --git a/client/mm/mm.go b/client/mm/mm.go index 0b8a6a9e75..c3f2033ff3 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -14,6 +14,7 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/core" + _ "decred.org/dcrdex/client/mm/binance" "decred.org/dcrdex/client/mm/libxc" "decred.org/dcrdex/client/orderbook" "decred.org/dcrdex/dex"