decred.org/dcrdex@v1.0.5/client/cmd/testbinance/main.go (about)

     1  package main
     2  
     3  /*
     4   * Starts an http server that responds to some of the binance api's endpoints.
     5   * The "runserver" command starts the server, and other commands are used to
     6   * update the server's state.
     7   */
     8  
     9  import (
    10  	"context"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"flag"
    14  	"fmt"
    15  	"io"
    16  	"math"
    17  	"math/rand"
    18  	"net/http"
    19  	"os"
    20  	"os/signal"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	"decred.org/dcrdex/client/mm/libxc/bntypes"
    28  	"decred.org/dcrdex/dex"
    29  	"decred.org/dcrdex/dex/encode"
    30  	"decred.org/dcrdex/dex/fiatrates"
    31  	"decred.org/dcrdex/dex/msgjson"
    32  	"decred.org/dcrdex/dex/utils"
    33  	"decred.org/dcrdex/dex/ws"
    34  	"decred.org/dcrdex/server/comms"
    35  	"github.com/go-chi/chi/v5"
    36  )
    37  
    38  const (
    39  	pongWait     = 60 * time.Second
    40  	pingPeriod   = (pongWait * 9) / 10
    41  	depositConfs = 3
    42  
    43  	// maxWalkingSpeed is that maximum amount the mid-gap can change per shuffle.
    44  	// Default about 3% of the basis price, but can be scaled by walkingspeed
    45  	// flag. The actual mid-gap shift during a shuffle is randomized in the
    46  	// range [0, defaultWalkingSpeed*walkingSpeedAdj].
    47  	defaultWalkingSpeed = 0.03
    48  )
    49  
    50  var (
    51  	log dex.Logger
    52  
    53  	walkingSpeedAdj float64
    54  	gapRange        float64
    55  	flappyWS        bool
    56  
    57  	xcInfo = &bntypes.ExchangeInfo{
    58  		Timezone:   "UTC",
    59  		ServerTime: time.Now().Unix(),
    60  		RateLimits: []*bntypes.RateLimit{},
    61  		Symbols: []*bntypes.Market{
    62  			makeMarket("dcr", "btc"),
    63  			makeMarket("eth", "btc"),
    64  			makeMarket("dcr", "usdc"),
    65  			makeMarket("zec", "btc"),
    66  		},
    67  	}
    68  
    69  	coinInfos = []*bntypes.CoinInfo{
    70  		makeCoinInfo("BTC", "BTC", true, true, 0.00000610, 0.0007),
    71  		makeCoinInfo("ETH", "ETH", true, true, 0.00035, 0.008),
    72  		makeCoinInfo("DCR", "DCR", true, true, 0.00001000, 0.05),
    73  		makeCoinInfo("USDC", "MATIC", true, true, 0.01000, 10),
    74  		makeCoinInfo("ZEC", "ZEC", true, true, 0.00500000, 0.01000000),
    75  	}
    76  
    77  	coinpapAssets = []*fiatrates.CoinpaprikaAsset{
    78  		makeCoinpapAsset(0, "btc", "Bitcoin"),
    79  		makeCoinpapAsset(42, "dcr", "Decred"),
    80  		makeCoinpapAsset(60, "eth", "Ethereum"),
    81  		makeCoinpapAsset(966001, "usdc.polygon", "USDC"),
    82  		makeCoinpapAsset(133, "zec", "Zcash"),
    83  	}
    84  
    85  	initialBalances = []*bntypes.Balance{
    86  		makeBalance("btc", 1.5),
    87  		makeBalance("dcr", 10000),
    88  		makeBalance("eth", 5),
    89  		makeBalance("usdc", 1152),
    90  		makeBalance("zec", 10000),
    91  	}
    92  )
    93  
    94  func parseAssetID(asset string) uint32 {
    95  	symbol := strings.ToLower(asset)
    96  	switch symbol {
    97  	case "usdc":
    98  		symbol = "usdc.polygon"
    99  	}
   100  	assetID, _ := dex.BipSymbolID(symbol)
   101  	return assetID
   102  }
   103  
   104  func makeMarket(baseSymbol, quoteSymbol string) *bntypes.Market {
   105  	baseSymbol, quoteSymbol = strings.ToUpper(baseSymbol), strings.ToUpper(quoteSymbol)
   106  	return &bntypes.Market{
   107  		Symbol:              baseSymbol + quoteSymbol,
   108  		Status:              "TRADING",
   109  		BaseAsset:           baseSymbol,
   110  		BaseAssetPrecision:  8,
   111  		QuoteAsset:          quoteSymbol,
   112  		QuoteAssetPrecision: 8,
   113  		OrderTypes: []string{
   114  			"LIMIT",
   115  			"LIMIT_MAKER",
   116  			"MARKET",
   117  			"STOP_LOSS",
   118  			"STOP_LOSS_LIMIT",
   119  			"TAKE_PROFIT",
   120  			"TAKE_PROFIT_LIMIT",
   121  		},
   122  	}
   123  }
   124  
   125  func makeBalance(symbol string, bal float64) *bntypes.Balance {
   126  	return &bntypes.Balance{
   127  		Asset: strings.ToUpper(symbol),
   128  		Free:  bal,
   129  	}
   130  }
   131  
   132  func makeCoinInfo(coin, network string, withdrawsEnabled, depositsEnabled bool, withdrawFee, withdrawMin float64) *bntypes.CoinInfo {
   133  	return &bntypes.CoinInfo{
   134  		Coin: coin,
   135  		NetworkList: []*bntypes.NetworkInfo{{
   136  			Coin:           coin,
   137  			Network:        network,
   138  			WithdrawEnable: withdrawsEnabled,
   139  			DepositEnable:  depositsEnabled,
   140  			WithdrawFee:    withdrawFee,
   141  			WithdrawMin:    withdrawMin,
   142  		}},
   143  	}
   144  }
   145  
   146  func makeCoinpapAsset(assetID uint32, symbol, name string) *fiatrates.CoinpaprikaAsset {
   147  	return &fiatrates.CoinpaprikaAsset{
   148  		AssetID: assetID,
   149  		Symbol:  symbol,
   150  		Name:    name,
   151  	}
   152  }
   153  
   154  // sendBalanceUpdateRequest sends a balance update request to the testbinance server
   155  // running in another process.
   156  func sendBalanceUpdateRequest(coin string, balanceUpdate float64) {
   157  	if coin == "" || balanceUpdate == 0 {
   158  		fmt.Printf("Invalid balance update request: coin = %q, balanceUpdate = %f\n", coin, balanceUpdate)
   159  		return
   160  	}
   161  
   162  	url := fmt.Sprintf("http://localhost:37346/testbinance/updatebalance?coin=%s&amt=%f",
   163  		coin, balanceUpdate)
   164  	resp, err := http.Get(url)
   165  	if err != nil {
   166  		log.Errorf("Error sending balance update request: %v", err)
   167  		return
   168  	}
   169  
   170  	defer resp.Body.Close()
   171  	if resp.StatusCode != http.StatusOK {
   172  		body, _ := io.ReadAll(resp.Body)
   173  		fmt.Println("Balance update request failed:", string(body))
   174  		return
   175  	}
   176  
   177  	fmt.Println("Balance update request sent")
   178  }
   179  
   180  func main() {
   181  	var logDebug, logTrace bool
   182  	var coin string
   183  	var balanceUpdate float64
   184  	flag.Float64Var(&walkingSpeedAdj, "walkspeed", 1.0, "scale the maximum walking speed. default scale of 1.0 is about 3%")
   185  	flag.Float64Var(&gapRange, "gaprange", 0.04, "a ratio of how much the gap can vary. default is 0.04 => 4%")
   186  	flag.BoolVar(&logDebug, "debug", false, "use debug logging")
   187  	flag.BoolVar(&logTrace, "trace", false, "use trace logging")
   188  	flag.BoolVar(&flappyWS, "flappyws", false, "periodically drop websocket clients and delete subscriptions")
   189  	flag.Float64Var(&balanceUpdate, "balupdate", 0, "update the balance of an asset on a testbinance server running as another process")
   190  	flag.StringVar(&coin, "coin", "", "coin for testbinance admin update")
   191  	flag.Parse()
   192  
   193  	if balanceUpdate != 0 {
   194  		sendBalanceUpdateRequest(coin, balanceUpdate)
   195  		return
   196  	}
   197  
   198  	switch {
   199  	case logTrace:
   200  		log = dex.StdOutLogger("TB", dex.LevelTrace)
   201  		comms.UseLogger(dex.StdOutLogger("C", dex.LevelTrace))
   202  	case logDebug:
   203  		log = dex.StdOutLogger("TB", dex.LevelDebug)
   204  		comms.UseLogger(dex.StdOutLogger("C", dex.LevelDebug))
   205  	default:
   206  		log = dex.StdOutLogger("TB", dex.LevelInfo)
   207  		comms.UseLogger(dex.StdOutLogger("C", dex.LevelInfo))
   208  	}
   209  
   210  	if err := mainErr(); err != nil {
   211  		fmt.Fprint(os.Stderr, err)
   212  		os.Exit(1)
   213  	}
   214  	os.Exit(0)
   215  }
   216  
   217  func mainErr() error {
   218  	if walkingSpeedAdj > 10 {
   219  		return fmt.Errorf("invalid walkspeed must be in < 10")
   220  	}
   221  
   222  	ctx, cancel := context.WithCancel(context.Background())
   223  	defer cancel()
   224  
   225  	killChan := make(chan os.Signal, 1)
   226  	signal.Notify(killChan, os.Interrupt)
   227  	go func() {
   228  		<-killChan
   229  		log.Info("Shutting down...")
   230  		cancel()
   231  	}()
   232  
   233  	bnc, err := newFakeBinanceServer(ctx)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	bnc.run(ctx)
   239  
   240  	return nil
   241  }
   242  
   243  type withdrawal struct {
   244  	amt     float64
   245  	txID    atomic.Value // string
   246  	coin    string
   247  	network string
   248  	address string
   249  	apiKey  string
   250  }
   251  
   252  type marketSubscriber struct {
   253  	*ws.WSLink
   254  
   255  	// markets is protected by the fakeBinance.marketsMtx.
   256  	markets map[string]struct{}
   257  }
   258  
   259  type userOrder struct {
   260  	slug   string
   261  	sell   bool
   262  	rate   float64
   263  	qty    float64
   264  	apiKey string
   265  	stamp  time.Time
   266  	status string
   267  }
   268  
   269  type fakeBinance struct {
   270  	ctx       context.Context
   271  	srv       *comms.Server
   272  	fiatRates map[uint32]float64
   273  
   274  	withdrawalHistoryMtx sync.RWMutex
   275  	withdrawalHistory    map[string]*withdrawal
   276  
   277  	balancesMtx sync.RWMutex
   278  	balances    map[string]*bntypes.Balance
   279  
   280  	accountSubscribersMtx sync.RWMutex
   281  	accountSubscribers    map[string]*ws.WSLink
   282  
   283  	marketsMtx        sync.RWMutex
   284  	markets           map[string]*market
   285  	marketSubscribers map[string]*marketSubscriber
   286  
   287  	walletMtx sync.RWMutex
   288  	wallets   map[string]Wallet
   289  
   290  	bookedOrdersMtx sync.RWMutex
   291  	bookedOrders    map[string]*userOrder
   292  }
   293  
   294  func newFakeBinanceServer(ctx context.Context) (*fakeBinance, error) {
   295  	log.Trace("Fetching coinpaprika prices")
   296  	fiatRates := fiatrates.FetchCoinpaprikaRates(ctx, coinpapAssets, dex.StdOutLogger("CP", dex.LevelDebug))
   297  	if len(fiatRates) < len(coinpapAssets) {
   298  		return nil, fmt.Errorf("not enough coinpap assets. wanted %d, got %d", len(coinpapAssets), len(fiatRates))
   299  	}
   300  
   301  	srv, err := comms.NewServer(&comms.RPCConfig{
   302  		ListenAddrs: []string{":37346"},
   303  		NoTLS:       true,
   304  	})
   305  	if err != nil {
   306  		return nil, fmt.Errorf("Error creating server: %w", err)
   307  	}
   308  
   309  	balances := make(map[string]*bntypes.Balance, len(initialBalances))
   310  	for _, bal := range initialBalances {
   311  		balances[bal.Asset] = bal
   312  	}
   313  
   314  	f := &fakeBinance{
   315  		ctx:                ctx,
   316  		srv:                srv,
   317  		withdrawalHistory:  make(map[string]*withdrawal, 0),
   318  		balances:           balances,
   319  		accountSubscribers: make(map[string]*ws.WSLink),
   320  		wallets:            make(map[string]Wallet),
   321  		fiatRates:          fiatRates,
   322  		markets:            make(map[string]*market),
   323  		marketSubscribers:  make(map[string]*marketSubscriber),
   324  		bookedOrders:       make(map[string]*userOrder),
   325  	}
   326  
   327  	mux := srv.Mux()
   328  
   329  	mux.Route("/sapi/v1/capital", func(r chi.Router) {
   330  		r.Get("/config/getall", f.handleWalletCoinsReq)
   331  		r.Get("/deposit/hisrec", f.handleConfirmDeposit)
   332  		r.Get("/deposit/address", f.handleGetDepositAddress)
   333  		r.Post("/withdraw/apply", f.handleWithdrawal)
   334  		r.Get("/withdraw/history", f.handleWithdrawalHistory)
   335  
   336  	})
   337  	mux.Route("/api/v3", func(r chi.Router) {
   338  		r.Get("/exchangeInfo", f.handleExchangeInfo)
   339  		r.Get("/account", f.handleAccount)
   340  		r.Get("/depth", f.handleDepth)
   341  		r.Get("/order", f.handleGetOrder)
   342  		r.Post("/order", f.handlePostOrder)
   343  		r.Post("/userDataStream", f.handleListenKeyRequest)
   344  		r.Put("/userDataStream", f.streamExtend)
   345  		r.Delete("/order", f.handleDeleteOrder)
   346  		r.Get("/ticker/24hr", f.handleMarketTicker24)
   347  	})
   348  
   349  	mux.Get("/ws/{listenKey}", f.handleAccountSubscription)
   350  	mux.Get("/stream", f.handleMarketStream)
   351  	mux.Route("/testbinance", func(r chi.Router) {
   352  		r.Get("/updatebalance", f.handleUpdateBalance)
   353  	})
   354  
   355  	return f, nil
   356  }
   357  
   358  func (f *fakeBinance) handleUpdateBalance(w http.ResponseWriter, r *http.Request) {
   359  	coin := r.URL.Query().Get("coin")
   360  	amtStr := r.URL.Query().Get("amt")
   361  	amt, err := strconv.ParseFloat(amtStr, 64)
   362  	if err != nil {
   363  		http.Error(w, fmt.Sprintf("invalid amt %q: %v", amtStr, err), http.StatusBadRequest)
   364  		return
   365  	}
   366  
   367  	balUpdate := f.updateBalance(coin, amt)
   368  	if balUpdate == nil {
   369  		http.Error(w, fmt.Sprintf("no balance to update for %q", coin), http.StatusBadRequest)
   370  		return
   371  	}
   372  
   373  	f.sendBalanceUpdates([]*bntypes.WSBalance{balUpdate})
   374  	w.WriteHeader(http.StatusOK)
   375  }
   376  
   377  func (f *fakeBinance) run(ctx context.Context) {
   378  	// Start a ticker to do book shuffles.
   379  
   380  	go func() {
   381  		runMarketTick := func() {
   382  			f.marketsMtx.RLock()
   383  			defer f.marketsMtx.RUnlock()
   384  			updates := make(map[string]json.RawMessage)
   385  			for mktID, mkt := range f.markets {
   386  				mkt.bookMtx.Lock()
   387  				buys, sells := mkt.shuffle()
   388  				firstUpdateID := mkt.updateID + 1
   389  				mkt.updateID += uint64(len(buys) + len(sells))
   390  				update, _ := json.Marshal(&bntypes.BookNote{
   391  					StreamName: mktID + "@depth",
   392  					Data: &bntypes.BookUpdate{
   393  						Bids:          buys,
   394  						Asks:          sells,
   395  						FirstUpdateID: firstUpdateID,
   396  						LastUpdateID:  mkt.updateID,
   397  					},
   398  				})
   399  				updates[mktID] = update
   400  				mkt.bookMtx.Unlock()
   401  			}
   402  
   403  			if len(f.marketSubscribers) > 0 {
   404  				log.Tracef("Sending %d market updates to %d subscribers", len(updates), len(f.marketSubscribers))
   405  			}
   406  			for _, sub := range f.marketSubscribers {
   407  				for symbol := range updates {
   408  					if _, found := sub.markets[symbol]; found {
   409  						sub.SendRaw(updates[symbol])
   410  					}
   411  				}
   412  			}
   413  		}
   414  		const marketMinTick, marketTickRange = time.Second * 5, time.Second * 25
   415  		for {
   416  			delay := marketMinTick + time.Duration(rand.Float64()*float64(marketTickRange))
   417  			select {
   418  			case <-time.After(delay):
   419  			case <-ctx.Done():
   420  				return
   421  			}
   422  			runMarketTick()
   423  		}
   424  	}()
   425  
   426  	// Start a ticker to fill booked orders
   427  	go func() {
   428  		// 50% chance of filling all booked orders every 5 to 30 seconds.
   429  		const minFillTick, fillTickRange = 5 * time.Second, 25 * time.Second
   430  		for {
   431  			select {
   432  			case <-time.After(minFillTick + time.Duration(rand.Float64()*float64(fillTickRange))):
   433  			case <-ctx.Done():
   434  				return
   435  			}
   436  			if rand.Float32() < 0.5 {
   437  				continue
   438  			}
   439  			type filledOrder struct {
   440  				bntypes.StreamUpdate
   441  				apiKey string
   442  			}
   443  
   444  			f.bookedOrdersMtx.Lock()
   445  			fills := make([]*filledOrder, 0)
   446  			for tradeID, ord := range f.bookedOrders {
   447  				if ord.status == "FILLED" {
   448  					if time.Since(ord.stamp) > time.Hour {
   449  						delete(f.bookedOrders, tradeID)
   450  					}
   451  					continue
   452  				}
   453  				ord.status = "FILLED"
   454  				fills = append(fills, &filledOrder{
   455  					StreamUpdate: bntypes.StreamUpdate{
   456  						EventType:          "executionReport",
   457  						CurrentOrderStatus: "FILLED",
   458  						// CancelledOrderID
   459  						ClientOrderID: tradeID,
   460  						Filled:        ord.qty,
   461  						QuoteFilled:   ord.qty * ord.rate,
   462  					},
   463  					apiKey: ord.apiKey,
   464  				})
   465  			}
   466  			f.bookedOrdersMtx.Unlock()
   467  			if len(fills) > 0 {
   468  				log.Tracef("Filling %d booked user orders", len(fills))
   469  			}
   470  			for _, ord := range fills {
   471  				f.accountSubscribersMtx.RLock()
   472  				sub, found := f.accountSubscribers[ord.apiKey]
   473  				f.accountSubscribersMtx.RUnlock()
   474  				if !found {
   475  					continue
   476  				}
   477  				respB, _ := json.Marshal(ord)
   478  				sub.SendRaw(respB)
   479  			}
   480  		}
   481  	}()
   482  
   483  	// Start a ticker to complete withdrawals.
   484  	go func() {
   485  		for {
   486  			tick := time.After(time.Second * 30)
   487  			select {
   488  			case <-tick:
   489  			case <-ctx.Done():
   490  				return
   491  			}
   492  
   493  			f.withdrawalHistoryMtx.Lock()
   494  			for transferID, withdraw := range f.withdrawalHistory {
   495  				if withdraw.txID.Load() != nil {
   496  					continue
   497  				}
   498  				wallet, err := f.getWallet(withdraw.network)
   499  				if err != nil {
   500  					log.Errorf("No wallet for withdraw coin %s", withdraw.coin)
   501  					delete(f.withdrawalHistory, transferID)
   502  					continue
   503  				}
   504  				txID, err := wallet.Send(ctx, withdraw.address, withdraw.coin, withdraw.amt)
   505  				if err != nil {
   506  					log.Errorf("Error sending %s: %v", withdraw.coin, err)
   507  					delete(f.withdrawalHistory, transferID)
   508  					continue
   509  				}
   510  				log.Debug("Sent withdraw of %.8f to user %s, coin = %s, txid = %s", withdraw.amt, withdraw.apiKey, withdraw.coin, txID)
   511  				withdraw.txID.Store(txID)
   512  			}
   513  			f.withdrawalHistoryMtx.Unlock()
   514  		}
   515  	}()
   516  
   517  	if flappyWS {
   518  		go func() {
   519  			tick := func() <-chan time.Time {
   520  				const minDelay = time.Minute
   521  				const delayRange = time.Minute * 5
   522  				return time.After(minDelay + time.Duration(rand.Float64()*float64(delayRange)))
   523  			}
   524  			for {
   525  				select {
   526  				case <-tick():
   527  					f.marketsMtx.Lock()
   528  					for addr, sub := range f.marketSubscribers {
   529  						sub.Disconnect()
   530  						delete(f.marketSubscribers, addr)
   531  					}
   532  					f.marketsMtx.Unlock()
   533  					f.accountSubscribersMtx.Lock()
   534  					for apiKey, sub := range f.accountSubscribers {
   535  						sub.Disconnect()
   536  						delete(f.accountSubscribers, apiKey)
   537  					}
   538  					f.accountSubscribersMtx.Unlock()
   539  				case <-ctx.Done():
   540  					return
   541  				}
   542  			}
   543  		}()
   544  	}
   545  
   546  	f.srv.Run(ctx)
   547  }
   548  
   549  func (f *fakeBinance) newWSLink(w http.ResponseWriter, r *http.Request, handler func([]byte)) (_ *ws.WSLink, _ *dex.ConnectionMaster) {
   550  	wsConn, err := ws.NewConnection(w, r, pongWait)
   551  	if err != nil {
   552  		log.Errorf("ws.NewConnection error: %v", err)
   553  		http.Error(w, "error initializing connection", http.StatusInternalServerError)
   554  		return
   555  	}
   556  
   557  	ip := dex.NewIPKey(r.RemoteAddr)
   558  
   559  	conn := ws.NewWSLink(ip.String(), wsConn, pingPeriod, func(msg *msgjson.Message) *msgjson.Error {
   560  		return nil
   561  	}, dex.StdOutLogger(fmt.Sprintf("CL[%s]", ip), dex.LevelDebug))
   562  	conn.RawHandler = handler
   563  
   564  	cm := dex.NewConnectionMaster(conn)
   565  	if err = cm.ConnectOnce(f.ctx); err != nil {
   566  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   567  		return
   568  	}
   569  
   570  	return conn, cm
   571  }
   572  
   573  func (f *fakeBinance) handleAccountSubscription(w http.ResponseWriter, r *http.Request) {
   574  	apiKey := extractAPIKey(r)
   575  
   576  	conn, cm := f.newWSLink(w, r, func(b []byte) {
   577  		log.Errorf("Message received from api key %s over account update channel: %s", apiKey, string(b))
   578  	})
   579  	if conn == nil { // Already logged.
   580  		return
   581  	}
   582  
   583  	log.Tracef("User subscribed to account stream with API key %s", apiKey)
   584  
   585  	f.accountSubscribersMtx.Lock()
   586  	f.accountSubscribers[apiKey] = conn
   587  	f.accountSubscribersMtx.Unlock()
   588  
   589  	go func() {
   590  		cm.Wait()
   591  		f.accountSubscribersMtx.Lock()
   592  		delete(f.accountSubscribers, apiKey)
   593  		f.accountSubscribersMtx.Unlock()
   594  		log.Tracef("Account stream connection ended for API key %s", apiKey)
   595  	}()
   596  }
   597  
   598  type listSubsResp struct {
   599  	ID     uint64   `json:"id"`
   600  	Result []string `json:"result"`
   601  }
   602  
   603  func (f *fakeBinance) handleMarketStream(w http.ResponseWriter, r *http.Request) {
   604  	streamsStr := r.URL.Query().Get("streams")
   605  	if streamsStr == "" {
   606  		log.Error("Client connected to market stream without providing a 'streams' query parameter")
   607  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   608  		return
   609  	}
   610  	rawStreams := strings.Split(streamsStr, "/")
   611  	marketIDs := make(map[string]struct{}, len(rawStreams))
   612  	for _, raw := range rawStreams {
   613  		parts := strings.Split(raw, "@")
   614  		if len(parts) < 2 {
   615  			http.Error(w, fmt.Sprintf("stream encoding incorrect %q", raw), http.StatusBadRequest)
   616  			return
   617  		}
   618  		marketIDs[strings.ToUpper(parts[0])] = struct{}{}
   619  	}
   620  
   621  	cl := &marketSubscriber{
   622  		markets: marketIDs,
   623  	}
   624  
   625  	subscribe := func(streamIDs []string) {
   626  		f.marketsMtx.Lock()
   627  		defer f.marketsMtx.Unlock()
   628  		for _, streamID := range streamIDs {
   629  			parts := strings.Split(streamID, "@")
   630  			if len(parts) < 2 {
   631  				log.Errorf("SUBSCRIBE stream encoding incorrect: %q", streamID)
   632  				return
   633  			}
   634  			cl.markets[strings.ToUpper(parts[0])] = struct{}{}
   635  		}
   636  	}
   637  
   638  	unsubscribe := func(streamIDs []string) {
   639  		f.marketsMtx.Lock()
   640  		defer f.marketsMtx.Unlock()
   641  		for _, streamID := range streamIDs {
   642  			parts := strings.Split(streamID, "@")
   643  			if len(parts) < 2 {
   644  				log.Errorf("UNSUBSCRIBE stream encoding incorrect: %q", streamID)
   645  				return
   646  			}
   647  			delete(cl.markets, strings.ToUpper(parts[0]))
   648  		}
   649  		f.cleanMarkets()
   650  	}
   651  
   652  	listSubscriptions := func(id uint64) {
   653  		f.marketsMtx.Lock()
   654  		defer f.marketsMtx.Unlock()
   655  		var streams []string
   656  		for mktID := range cl.markets {
   657  			streams = append(streams, fmt.Sprintf("%s@depth", mktID))
   658  		}
   659  		resp := listSubsResp{
   660  			ID:     id,
   661  			Result: streams,
   662  		}
   663  		b, err := json.Marshal(resp)
   664  		if err != nil {
   665  			log.Errorf("LIST_SUBSCRIBE marshal error: %v", err)
   666  		}
   667  		cl.WSLink.SendRaw(b)
   668  		f.cleanMarkets()
   669  	}
   670  
   671  	conn, cm := f.newWSLink(w, r, func(b []byte) {
   672  		var req bntypes.StreamSubscription
   673  		if err := json.Unmarshal(b, &req); err != nil {
   674  			log.Errorf("Error unmarshalling markets stream message: %v", err)
   675  			return
   676  		}
   677  		switch req.Method {
   678  		case "SUBSCRIBE":
   679  			subscribe(req.Params)
   680  		case "UNSUBSCRIBE":
   681  			unsubscribe(req.Params)
   682  		case "LIST_SUBSCRIPTIONS":
   683  			listSubscriptions(req.ID)
   684  		}
   685  	})
   686  	if conn == nil {
   687  		return
   688  	}
   689  
   690  	cl.WSLink = conn
   691  
   692  	addr := conn.Addr()
   693  	log.Tracef("Websocket client %s connected to market stream for markets %+v", addr, marketIDs)
   694  
   695  	f.marketsMtx.Lock()
   696  	f.marketSubscribers[addr] = cl
   697  	f.marketsMtx.Unlock()
   698  
   699  	go func() {
   700  		cm.Wait()
   701  		log.Tracef("Market stream client %s disconnected", addr)
   702  		f.marketsMtx.Lock()
   703  		delete(f.marketSubscribers, addr)
   704  		f.cleanMarkets()
   705  		f.marketsMtx.Unlock()
   706  	}()
   707  }
   708  
   709  // Call with f.marketsMtx locked
   710  func (f *fakeBinance) cleanMarkets() {
   711  	marketSubCount := make(map[string]int)
   712  	for _, cl := range f.marketSubscribers {
   713  		for mktID := range cl.markets {
   714  			marketSubCount[mktID]++
   715  		}
   716  	}
   717  	for mktID := range f.markets {
   718  		if marketSubCount[mktID] == 0 {
   719  			delete(f.markets, mktID)
   720  		}
   721  	}
   722  }
   723  
   724  func (f *fakeBinance) handleWalletCoinsReq(w http.ResponseWriter, r *http.Request) {
   725  	respB, _ := json.Marshal(coinInfos)
   726  	writeBytesWithStatus(w, respB, http.StatusOK)
   727  }
   728  
   729  func (f *fakeBinance) sendBalanceUpdates(bals []*bntypes.WSBalance) {
   730  	update := &bntypes.StreamUpdate{
   731  		EventType: "outboundAccountPosition",
   732  		Balances:  bals,
   733  	}
   734  	updateB, _ := json.Marshal(update)
   735  	f.accountSubscribersMtx.Lock()
   736  	if len(f.accountSubscribers) > 0 {
   737  		log.Tracef("Sending balance updates to %d subscribers", len(f.accountSubscribers))
   738  	}
   739  	for _, sub := range f.accountSubscribers {
   740  		sub.SendRaw(updateB)
   741  	}
   742  	f.accountSubscribersMtx.Unlock()
   743  }
   744  
   745  func (f *fakeBinance) handleConfirmDeposit(w http.ResponseWriter, r *http.Request) {
   746  	q := r.URL.Query()
   747  	txID := q.Get("txid")
   748  	amtStr := q.Get("amt")
   749  	amt, err := strconv.ParseFloat(amtStr, 64)
   750  	if err != nil {
   751  		log.Errorf("Error parsing deposit amount string %q: %v", amtStr, err)
   752  		http.Error(w, "error parsing amount", http.StatusBadRequest)
   753  		return
   754  	}
   755  	coin := q.Get("coin")
   756  	network := q.Get("network")
   757  	wallet, err := f.getWallet(network)
   758  	if err != nil {
   759  		log.Errorf("Error creating deposit wallet for %s: %v", coin, err)
   760  		http.Error(w, "error creating wallet", http.StatusBadRequest)
   761  		return
   762  	}
   763  	confs, err := wallet.Confirmations(f.ctx, txID)
   764  	if err != nil {
   765  		log.Errorf("Error getting deposit confirmations for %s -> %s: %v", coin, txID, err)
   766  		http.Error(w, "error getting confirmations", http.StatusInternalServerError)
   767  		return
   768  	}
   769  	apiKey := extractAPIKey(r)
   770  	status := bntypes.DepositStatusPending
   771  	if confs >= depositConfs {
   772  		status = bntypes.DepositStatusCredited
   773  		log.Debugf("Confirmed deposit for %s of %.8f %s", apiKey, amt, coin)
   774  		f.balancesMtx.Lock()
   775  		var bal *bntypes.WSBalance
   776  		for _, b := range f.balances {
   777  			if b.Asset == coin {
   778  				bal = (*bntypes.WSBalance)(b)
   779  				b.Free += amt
   780  				break
   781  			}
   782  		}
   783  		f.balancesMtx.Unlock()
   784  		if bal != nil {
   785  			f.sendBalanceUpdates([]*bntypes.WSBalance{bal})
   786  		}
   787  	} else {
   788  		log.Tracef("Updating user %s on deposit status for %.8f %s. Confs = %d", apiKey, amt, coin, confs)
   789  	}
   790  	resp := []*bntypes.PendingDeposit{{
   791  		Amount:  amt,
   792  		Status:  status,
   793  		TxID:    txID,
   794  		Coin:    coin,
   795  		Network: network,
   796  	}}
   797  	writeJSONWithStatus(w, resp, http.StatusOK)
   798  }
   799  
   800  func (f *fakeBinance) getWallet(network string) (Wallet, error) {
   801  	symbol := strings.ToLower(network)
   802  	f.walletMtx.Lock()
   803  	defer f.walletMtx.Unlock()
   804  	wallet, exists := f.wallets[symbol]
   805  	if exists {
   806  		return wallet, nil
   807  	}
   808  	wallet, err := newWallet(f.ctx, symbol)
   809  	if err != nil {
   810  		return nil, err
   811  	}
   812  	f.wallets[symbol] = wallet
   813  	return wallet, nil
   814  }
   815  
   816  func (f *fakeBinance) handleGetDepositAddress(w http.ResponseWriter, r *http.Request) {
   817  	coin := r.URL.Query().Get("coin")
   818  	network := r.URL.Query().Get("network")
   819  
   820  	wallet, err := f.getWallet(network)
   821  	if err != nil {
   822  		log.Errorf("Error creating wallet for %s: %v", coin, err)
   823  		http.Error(w, "error creating wallet", http.StatusBadRequest)
   824  		return
   825  	}
   826  
   827  	resp := struct {
   828  		Address string `json:"address"`
   829  	}{
   830  		Address: wallet.DepositAddress(),
   831  	}
   832  
   833  	log.Tracef("User %s requested deposit address %s", extractAPIKey(r), resp.Address)
   834  
   835  	writeJSONWithStatus(w, resp, http.StatusOK)
   836  }
   837  
   838  func (f *fakeBinance) updateBalance(coin string, amt float64) *bntypes.WSBalance {
   839  	f.balancesMtx.Lock()
   840  	defer f.balancesMtx.Unlock()
   841  
   842  	var balUpdate *bntypes.WSBalance
   843  
   844  	for _, b := range f.balances {
   845  		if b.Asset == coin {
   846  			if amt+b.Free < 0 {
   847  				b.Free = 0
   848  			} else {
   849  				b.Free += amt
   850  			}
   851  			balUpdate = (*bntypes.WSBalance)(b)
   852  			break
   853  		}
   854  	}
   855  
   856  	return balUpdate
   857  }
   858  
   859  func (f *fakeBinance) handleWithdrawal(w http.ResponseWriter, r *http.Request) {
   860  	defer r.Body.Close()
   861  	apiKey := extractAPIKey(r)
   862  	err := r.ParseForm()
   863  	if err != nil {
   864  		log.Errorf("Error parsing form for user %s: ", apiKey, err)
   865  		http.Error(w, "Error parsing form", http.StatusBadRequest)
   866  		return
   867  	}
   868  
   869  	amountStr := r.Form.Get("amount")
   870  	amt, err := strconv.ParseFloat(amountStr, 64)
   871  	if err != nil {
   872  		log.Errorf("Error parsing amount for user %s: ", apiKey, err)
   873  		http.Error(w, "Error parsing amount", http.StatusBadRequest)
   874  		return
   875  	}
   876  
   877  	coin := r.Form.Get("coin")
   878  	network := r.Form.Get("network")
   879  	address := r.Form.Get("address")
   880  
   881  	withdrawalID := hex.EncodeToString(encode.RandomBytes(32))
   882  	log.Debugf("Withdraw of %.8f %s initiated for user %s", amt, coin, apiKey)
   883  
   884  	f.withdrawalHistoryMtx.Lock()
   885  	f.withdrawalHistory[withdrawalID] = &withdrawal{
   886  		amt:     amt * 0.99,
   887  		coin:    coin,
   888  		network: network,
   889  		address: address,
   890  		apiKey:  apiKey,
   891  	}
   892  	f.withdrawalHistoryMtx.Unlock()
   893  
   894  	balUpdate := f.updateBalance(coin, -amt)
   895  	f.sendBalanceUpdates([]*bntypes.WSBalance{balUpdate})
   896  
   897  	resp := struct {
   898  		ID string `json:"id"`
   899  	}{withdrawalID}
   900  	writeJSONWithStatus(w, resp, http.StatusOK)
   901  }
   902  
   903  type withdrawalHistoryStatus struct {
   904  	ID     string  `json:"id"`
   905  	Amount float64 `json:"amount,string"`
   906  	Status int     `json:"status"`
   907  	TxID   string  `json:"txId"`
   908  }
   909  
   910  func (f *fakeBinance) handleWithdrawalHistory(w http.ResponseWriter, r *http.Request) {
   911  	defer r.Body.Close()
   912  
   913  	const withdrawalCompleteStatus = 6
   914  	withdrawalHistory := make([]*withdrawalHistoryStatus, 0)
   915  
   916  	f.withdrawalHistoryMtx.RLock()
   917  	for transferID, w := range f.withdrawalHistory {
   918  		var status int
   919  		txIDPtr := w.txID.Load()
   920  		var txID string
   921  		if txIDPtr == nil {
   922  			status = 2
   923  		} else {
   924  			txID = txIDPtr.(string)
   925  			status = withdrawalCompleteStatus
   926  		}
   927  		withdrawalHistory = append(withdrawalHistory, &withdrawalHistoryStatus{
   928  			ID:     transferID,
   929  			Amount: w.amt,
   930  			Status: status,
   931  			TxID:   txID,
   932  		})
   933  	}
   934  	f.withdrawalHistoryMtx.RUnlock()
   935  
   936  	log.Tracef("Sending %d withdraws to user %s", len(withdrawalHistory), extractAPIKey(r))
   937  	writeJSONWithStatus(w, withdrawalHistory, http.StatusOK)
   938  }
   939  
   940  func (f *fakeBinance) handleExchangeInfo(w http.ResponseWriter, r *http.Request) {
   941  	writeJSONWithStatus(w, xcInfo, http.StatusOK)
   942  }
   943  
   944  func (f *fakeBinance) handleAccount(w http.ResponseWriter, r *http.Request) {
   945  	f.balancesMtx.RLock()
   946  	defer f.balancesMtx.RUnlock()
   947  	writeJSONWithStatus(w, &bntypes.Account{Balances: utils.MapItems(f.balances)}, http.StatusOK)
   948  }
   949  
   950  func (f *fakeBinance) handleDepth(w http.ResponseWriter, r *http.Request) {
   951  	slug := r.URL.Query().Get("symbol")
   952  	var mkt *bntypes.Market
   953  	for _, m := range xcInfo.Symbols {
   954  		if m.Symbol == slug {
   955  			mkt = m
   956  			break
   957  		}
   958  	}
   959  	if mkt == nil {
   960  		log.Errorf("No market definition found for market %q", slug)
   961  		http.Error(w, "no market "+slug, http.StatusBadRequest)
   962  		return
   963  	}
   964  	f.marketsMtx.Lock()
   965  	m, found := f.markets[slug]
   966  	if !found {
   967  		baseFiatRate := f.fiatRates[parseAssetID(mkt.BaseAsset)]
   968  		quoteFiatRate := f.fiatRates[parseAssetID(mkt.QuoteAsset)]
   969  		m = newMarket(slug, mkt.BaseAsset, mkt.QuoteAsset, baseFiatRate, quoteFiatRate)
   970  		f.markets[slug] = m
   971  	}
   972  	f.marketsMtx.Unlock()
   973  
   974  	var resp bntypes.OrderbookSnapshot
   975  	m.bookMtx.RLock()
   976  	for _, ord := range m.buys {
   977  		resp.Bids = append(resp.Bids, [2]json.Number{json.Number(floatString(ord.rate)), json.Number(floatString(ord.qty))})
   978  	}
   979  	for _, ord := range m.sells {
   980  		resp.Asks = append(resp.Asks, [2]json.Number{json.Number(floatString(ord.rate)), json.Number(floatString(ord.qty))})
   981  	}
   982  	resp.LastUpdateID = m.updateID
   983  	m.bookMtx.RUnlock()
   984  	writeJSONWithStatus(w, &resp, http.StatusOK)
   985  }
   986  
   987  func (f *fakeBinance) handleGetOrder(w http.ResponseWriter, r *http.Request) {
   988  	tradeID := r.URL.Query().Get("origClientOrderId")
   989  	var status string
   990  	f.bookedOrdersMtx.RLock()
   991  	ord, found := f.bookedOrders[tradeID]
   992  	if found {
   993  		status = ord.status
   994  	}
   995  	f.bookedOrdersMtx.RUnlock()
   996  	if !found {
   997  		log.Errorf("User %s requested unknown order %s", extractAPIKey(r), tradeID)
   998  		http.Error(w, "order not found", http.StatusBadRequest)
   999  		return
  1000  	}
  1001  	resp := &bntypes.BookedOrder{
  1002  		Symbol: ord.slug,
  1003  		// OrderID:            ,
  1004  		ClientOrderID:      tradeID,
  1005  		Price:              ord.rate,
  1006  		OrigQty:            ord.qty,
  1007  		ExecutedQty:        0,
  1008  		CumulativeQuoteQty: 0,
  1009  		Status:             status,
  1010  		TimeInForce:        "GTC",
  1011  	}
  1012  	writeJSONWithStatus(w, &resp, http.StatusOK)
  1013  }
  1014  
  1015  func (f *fakeBinance) updateOrderBalances(symbol string, sell bool, qty, rate float64) {
  1016  	f.marketsMtx.RLock()
  1017  	mkt := f.markets[symbol]
  1018  	f.marketsMtx.RUnlock()
  1019  	fromSlug, toSlug, fromQty, toQty := mkt.quoteSlug, mkt.baseSlug, qty*rate, qty
  1020  	if sell {
  1021  		fromSlug, toSlug, fromQty, toQty = toSlug, fromSlug, toQty, fromQty
  1022  	}
  1023  	f.balancesMtx.Lock()
  1024  	f.balances[toSlug].Free += toQty
  1025  	f.balances[fromSlug].Free -= fromQty
  1026  	f.balancesMtx.Unlock()
  1027  }
  1028  
  1029  func (f *fakeBinance) handlePostOrder(w http.ResponseWriter, r *http.Request) {
  1030  	apiKey := extractAPIKey(r)
  1031  	q := r.URL.Query()
  1032  	slug := q.Get("symbol")
  1033  	side := q.Get("side")
  1034  	tradeID := q.Get("newClientOrderId")
  1035  	qty, err := strconv.ParseFloat(q.Get("quantity"), 64)
  1036  	if err != nil {
  1037  		log.Errorf("Error parsing quantity %q for order from user %s: %v", q.Get("quantity"), apiKey, err)
  1038  		http.Error(w, "Bad quantity formatting", http.StatusBadRequest)
  1039  		return
  1040  	}
  1041  	price, err := strconv.ParseFloat(q.Get("price"), 64)
  1042  	if err != nil {
  1043  		log.Errorf("Error parsing price %q for order from user %s: %v", q.Get("price"), apiKey, err)
  1044  		http.Error(w, "Missing price formatting", http.StatusBadRequest)
  1045  		return
  1046  	}
  1047  
  1048  	resp := &bntypes.OrderResponse{
  1049  		Symbol:       slug,
  1050  		Price:        price,
  1051  		OrigQty:      qty,
  1052  		OrigQuoteQty: qty * price,
  1053  	}
  1054  
  1055  	bookIt := rand.Float32() < 0.2
  1056  	if bookIt {
  1057  		resp.Status = "NEW"
  1058  		log.Tracef("Booking %s order on %s for %.8f for user %s", side, slug, qty, apiKey)
  1059  
  1060  	} else {
  1061  		log.Tracef("Filled %s order on %s for %.8f for user %s", side, slug, qty, apiKey)
  1062  		resp.Status = "FILLED"
  1063  		resp.ExecutedQty = qty
  1064  		resp.CumulativeQuoteQty = qty * price
  1065  	}
  1066  
  1067  	f.bookedOrdersMtx.Lock()
  1068  	f.bookedOrders[tradeID] = &userOrder{
  1069  		slug:   slug,
  1070  		sell:   side == "SELL",
  1071  		rate:   price,
  1072  		qty:    qty,
  1073  		apiKey: apiKey,
  1074  		stamp:  time.Now(),
  1075  		status: resp.Status,
  1076  	}
  1077  	f.bookedOrdersMtx.Unlock()
  1078  
  1079  	writeJSONWithStatus(w, &resp, http.StatusOK)
  1080  }
  1081  
  1082  func (f *fakeBinance) streamExtend(w http.ResponseWriter, r *http.Request) {
  1083  	w.WriteHeader(http.StatusOK)
  1084  }
  1085  
  1086  func (f *fakeBinance) handleListenKeyRequest(w http.ResponseWriter, r *http.Request) {
  1087  	resp := &bntypes.DataStreamKey{
  1088  		ListenKey: extractAPIKey(r),
  1089  	}
  1090  	writeJSONWithStatus(w, resp, http.StatusOK)
  1091  }
  1092  
  1093  func (f *fakeBinance) handleDeleteOrder(w http.ResponseWriter, r *http.Request) {
  1094  	tradeID := r.URL.Query().Get("origClientOrderId")
  1095  	apiKey := extractAPIKey(r)
  1096  	f.bookedOrdersMtx.Lock()
  1097  	ord, found := f.bookedOrders[tradeID]
  1098  	if found {
  1099  		if ord.status == "CANCELED" {
  1100  			log.Errorf("Detected cancellation of an already cancelled order %s", tradeID)
  1101  		}
  1102  		ord.status = "CANCELED"
  1103  	}
  1104  	f.bookedOrdersMtx.Unlock()
  1105  	writeJSONWithStatus(w, &struct{}{}, http.StatusOK)
  1106  	if !found {
  1107  		log.Errorf("DELETE request received from user %s for unknown order %s", apiKey, tradeID)
  1108  		return
  1109  	}
  1110  
  1111  	log.Tracef("Deleting order %s on %s for user %s", tradeID, ord.slug, apiKey)
  1112  	f.accountSubscribersMtx.RLock()
  1113  	sub, found := f.accountSubscribers[ord.apiKey]
  1114  	f.accountSubscribersMtx.RUnlock()
  1115  	if !found {
  1116  		return
  1117  	}
  1118  	update := &bntypes.StreamUpdate{
  1119  		EventType:          "executionReport",
  1120  		CurrentOrderStatus: "CANCELED",
  1121  		ClientOrderID:      hex.EncodeToString(encode.RandomBytes(20)),
  1122  		CancelledOrderID:   tradeID,
  1123  		Filled:             0,
  1124  		QuoteFilled:        0,
  1125  	}
  1126  	updateB, _ := json.Marshal(update)
  1127  	sub.SendRaw(updateB)
  1128  }
  1129  
  1130  func (f *fakeBinance) handleMarketTicker24(w http.ResponseWriter, r *http.Request) {
  1131  	resp := make([]*bntypes.MarketTicker24, 0, len(xcInfo.Symbols))
  1132  	for _, mkt := range xcInfo.Symbols {
  1133  		baseFiatRate := f.fiatRates[parseAssetID(mkt.BaseAsset)]
  1134  		quoteFiatRate := f.fiatRates[parseAssetID(mkt.QuoteAsset)]
  1135  		m := newMarket(mkt.Symbol, mkt.BaseAsset, mkt.QuoteAsset, baseFiatRate, quoteFiatRate)
  1136  		var buyPrice, sellPrice float64
  1137  		if len(m.buys) > 0 {
  1138  			buyPrice = m.buys[0].rate
  1139  		}
  1140  		if len(m.sells) > 0 {
  1141  			sellPrice = m.sells[0].rate
  1142  		}
  1143  		vol24USD := math.Pow(10, float64(rand.Intn(4)+2))
  1144  		vol24Base := vol24USD / baseFiatRate
  1145  		vol24Quote := vol24USD / quoteFiatRate
  1146  		lastPrice := m.basisRate
  1147  		highPrice := lastPrice * (1 + rand.Float64()*0.15)
  1148  		lowPrice := lastPrice / (1 + rand.Float64()*0.15)
  1149  		openPrice := lowPrice + ((highPrice - lowPrice) * rand.Float64())
  1150  		priceChange := lastPrice - openPrice
  1151  		priceChangePct := priceChange / openPrice * 100
  1152  
  1153  		avgPrice := (openPrice + lastPrice + highPrice + lowPrice) / 4
  1154  
  1155  		resp = append(resp, &bntypes.MarketTicker24{
  1156  			Symbol:             mkt.Symbol,
  1157  			PriceChange:        priceChange,
  1158  			PriceChangePercent: priceChangePct,
  1159  			BidPrice:           buyPrice,
  1160  			AskPrice:           sellPrice,
  1161  			Volume:             vol24Base,
  1162  			QuoteVolume:        vol24Quote,
  1163  			WeightedAvgPrice:   avgPrice,
  1164  			LastPrice:          lastPrice,
  1165  			OpenPrice:          openPrice,
  1166  			HighPrice:          highPrice,
  1167  			LowPrice:           lowPrice,
  1168  		})
  1169  	}
  1170  	writeJSONWithStatus(w, &resp, http.StatusOK)
  1171  }
  1172  
  1173  type rateQty struct {
  1174  	rate float64
  1175  	qty  float64
  1176  }
  1177  
  1178  type market struct {
  1179  	symbol, baseSlug, quoteSlug            string
  1180  	baseFiatRate, quoteFiatRate, basisRate float64
  1181  	minRate, maxRate                       float64
  1182  
  1183  	rate atomic.Uint64
  1184  
  1185  	bookMtx     sync.RWMutex
  1186  	updateID    uint64
  1187  	buys, sells []*rateQty
  1188  }
  1189  
  1190  func newMarket(symbol, baseSlug, quoteSlug string, baseFiatRate, quoteFiatRate float64) *market {
  1191  	const maxVariation = 0.1
  1192  	basisRate := baseFiatRate / quoteFiatRate
  1193  	minRate, maxRate := basisRate*(1/(1+maxVariation)), basisRate*(1+maxVariation)
  1194  	m := &market{
  1195  		symbol:        symbol,
  1196  		baseSlug:      baseSlug,
  1197  		quoteSlug:     quoteSlug,
  1198  		baseFiatRate:  baseFiatRate,
  1199  		quoteFiatRate: quoteFiatRate,
  1200  		basisRate:     basisRate,
  1201  		minRate:       minRate,
  1202  		maxRate:       maxRate,
  1203  		buys:          make([]*rateQty, 0),
  1204  		sells:         make([]*rateQty, 0),
  1205  	}
  1206  	m.rate.Store(math.Float64bits(basisRate))
  1207  	log.Tracef("Market %s intitialized with base fiat rate = %.4f, quote fiat rate = %.4f "+
  1208  		"basis rate = %.8f. Mid-gap rate will randomly walk between %.8f and %.8f",
  1209  		symbol, baseFiatRate, quoteFiatRate, basisRate, minRate, maxRate)
  1210  	m.shuffle()
  1211  	return m
  1212  }
  1213  
  1214  // Randomize the order book. booksMtx must be locked.
  1215  func (m *market) shuffle() (buys, sells [][2]json.Number) {
  1216  	maxChangeRatio := defaultWalkingSpeed * walkingSpeedAdj
  1217  	maxShift := m.basisRate * maxChangeRatio
  1218  	oldRate := math.Float64frombits(m.rate.Load())
  1219  	if rand.Float64() < 0.5 {
  1220  		maxShift *= -1
  1221  	}
  1222  	shiftRoll := rand.Float64()
  1223  	shift := maxShift * shiftRoll
  1224  	newRate := oldRate + shift
  1225  
  1226  	if newRate < m.minRate {
  1227  		newRate = m.minRate
  1228  	}
  1229  	if newRate > m.maxRate {
  1230  		newRate = m.maxRate
  1231  	}
  1232  
  1233  	m.rate.Store(math.Float64bits(newRate))
  1234  	log.Tracef("%s: A randomized (max %.1f%%) shift of %.8f (%.3f%%) was applied to the old rate of %.8f, "+
  1235  		"resulting in a new mid-gap of %.8f",
  1236  		m.symbol, maxChangeRatio*100, shift, shiftRoll*maxChangeRatio*100, oldRate, newRate,
  1237  	)
  1238  
  1239  	halfGapRoll := rand.Float64()
  1240  	const minHalfGap = 0.002 // 0.2%
  1241  	halfGapRange := gapRange / 2
  1242  	halfGapFactor := minHalfGap + halfGapRoll*halfGapRange
  1243  	bestBuy, bestSell := newRate/(1+halfGapFactor), newRate*(1+halfGapFactor)
  1244  
  1245  	levelSpacingRoll := rand.Float64()
  1246  	const minLevelSpacing, levelSpacingRange = 0.002, 0.01
  1247  	levelSpacing := (minLevelSpacing + levelSpacingRoll*levelSpacingRange) * newRate
  1248  
  1249  	log.Tracef("%s: Half-gap roll of %.4f%% resulted in a half-gap factor of %.4f%%, range %.8f to %0.8f. "+
  1250  		"Level-spacing roll of %.4f%% resulted in a level spacing of %.8f",
  1251  		m.symbol, halfGapRoll*100, halfGapFactor*100, bestBuy, bestSell, levelSpacingRoll*100, levelSpacing,
  1252  	)
  1253  
  1254  	zeroBookSide := func(ords []*rateQty) map[string]string {
  1255  		bin := make(map[string]string, len(ords))
  1256  		for _, ord := range ords {
  1257  			bin[floatString(ord.rate)] = "0"
  1258  		}
  1259  		return bin
  1260  	}
  1261  	jsBuys, jsSells := zeroBookSide(m.buys), zeroBookSide(m.sells)
  1262  
  1263  	makeOrders := func(bestRate, direction float64, jsSide map[string]string) []*rateQty {
  1264  		nLevels := rand.Intn(20) + 5
  1265  		ords := make([]*rateQty, nLevels)
  1266  		for i := 0; i < nLevels; i++ {
  1267  			rate := bestRate + levelSpacing*direction*float64(i)
  1268  			// Each level has between 1 and 10,001 USD equivalent.
  1269  			const minQtyUSD, qtyUSDRange = 1, 10_000
  1270  			qtyUSD := minQtyUSD + qtyUSDRange*rand.Float64()
  1271  			qty := qtyUSD / m.baseFiatRate
  1272  			jsSide[floatString(rate)] = floatString(qty)
  1273  			ords[i] = &rateQty{
  1274  				rate: rate,
  1275  				qty:  qty,
  1276  			}
  1277  		}
  1278  		return ords
  1279  	}
  1280  	m.buys = makeOrders(bestBuy, -1, jsBuys)
  1281  	m.sells = makeOrders(bestSell, 1, jsSells)
  1282  
  1283  	log.Tracef("%s: Shuffle resulted in %d buy orders and %d sell orders being placed", m.symbol, len(m.buys), len(m.sells))
  1284  
  1285  	convertSide := func(side map[string]string) [][2]json.Number {
  1286  		updates := make([][2]json.Number, 0, len(side))
  1287  		for r, q := range side {
  1288  			updates = append(updates, [2]json.Number{json.Number(r), json.Number(q)})
  1289  		}
  1290  		return updates
  1291  	}
  1292  
  1293  	return convertSide(jsBuys), convertSide(jsSells)
  1294  }
  1295  
  1296  // writeJSON marshals the provided interface and writes the bytes to the
  1297  // ResponseWriter with the specified response code.
  1298  func writeJSONWithStatus(w http.ResponseWriter, thing interface{}, code int) {
  1299  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1300  	b, err := json.Marshal(thing)
  1301  	if err != nil {
  1302  		w.WriteHeader(http.StatusInternalServerError)
  1303  		log.Errorf("JSON encode error: %v", err)
  1304  		return
  1305  	}
  1306  	writeBytesWithStatus(w, b, code)
  1307  }
  1308  
  1309  func writeBytesWithStatus(w http.ResponseWriter, b []byte, code int) {
  1310  	w.WriteHeader(code)
  1311  	_, err := w.Write(append(b, byte('\n')))
  1312  	if err != nil {
  1313  		log.Errorf("Write error: %v", err)
  1314  	}
  1315  }
  1316  
  1317  func extractAPIKey(r *http.Request) string {
  1318  	return r.Header.Get("X-MBX-APIKEY")
  1319  }
  1320  
  1321  func floatString(v float64) string {
  1322  	return strconv.FormatFloat(v, 'f', 8, 64)
  1323  }