Skip to content

Commit

Permalink
client/{mm, core}: Simple Arbitrage (decred#2480)
Browse files Browse the repository at this point in the history
* client/{mm, core}: Simple Arbitrage

This implements the simple arbitrage opportunity which only places orders
when there is an arbitrage opportunity.

- A `libxc` package is added which contains a `CEX` interface used to
  interact with a centralized exchange's API. It is implemented for
  Binance.
- The new strategy is implemented in `mm_simple_arb.go` and can be run by
  creating a `BotConfig` with a non-nil `ArbCfg`.
- A testbinance command line tool is added which starts a webserver that
  responds to the requests that the Binance testnet does not support.
- A `VWAP` function is added to the client orderbook.
- `client/comms/WSConn` is updated with a `SendRaw` function which sends
  arbitrary byte slices over the websocket connection.
  • Loading branch information
martonp authored Sep 18, 2023
1 parent 9ee538d commit 6a04997
Show file tree
Hide file tree
Showing 18 changed files with 3,895 additions and 90 deletions.
150 changes: 150 additions & 0 deletions client/cmd/testbinance/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package main

/*
* Starts an http server that responds with a hardcoded result to the binance API's
* "/sapi/v1/capital/config/getall" endpoint. Binance's testnet does not support the
* "sapi" endpoints, and this is the only "sapi" endpoint that we use.
*/

import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"

"decred.org/dcrdex/client/websocket"
"decred.org/dcrdex/dex"
)

const (
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
)

var (
log = dex.StdOutLogger("TBNC", dex.LevelDebug)
)

func main() {
if err := mainErr(); err != nil {
fmt.Fprint(os.Stderr, err, "\n")
os.Exit(1)
}
os.Exit(0)
}

func mainErr() error {
f := &fakeBinance{
wsServer: websocket.New(nil, log.SubLogger("WS")),
balances: map[string]*balance{
"eth": {
free: 1000.123432,
locked: 0,
},
"btc": {
free: 1000.21314123,
locked: 0,
},
"ltc": {
free: 1000.8689444,
locked: 0,
},
"bch": {
free: 1000.2358249,
locked: 0,
},
"dcr": {
free: 1000.2358249,
locked: 0,
},
},
}
http.HandleFunc("/sapi/v1/capital/config/getall", f.handleWalletCoinsReq)

return http.ListenAndServe(":37346", nil)
}

type balance struct {
free float64
locked float64
}

type fakeBinance struct {
wsServer *websocket.Server

balanceMtx sync.RWMutex
balances map[string]*balance
}

func (f *fakeBinance) handleWalletCoinsReq(w http.ResponseWriter, r *http.Request) {
ci := f.coinInfo()
writeJSONWithStatus(w, ci, http.StatusOK)
}

type fakeBinanceNetworkInfo struct {
Coin string `json:"coin"`
MinConfirm int `json:"minConfirm"`
Network string `json:"network"`
UnLockConfirm int `json:"unLockConfirm"`
WithdrawEnable bool `json:"withdrawEnable"`
WithdrawFee string `json:"withdrawFee"`
WithdrawIntegerMultiple string `json:"withdrawIntegerMultiple"`
WithdrawMax string `json:"withdrawMax"`
WithdrawMin string `json:"withdrawMin"`
}

type fakeBinanceCoinInfo struct {
Coin string `json:"coin"`
Free string `json:"free"`
Locked string `json:"locked"`
Withdrawing string `json:"withdrawing"`
NetworkList []*fakeBinanceNetworkInfo `json:"networkList"`
}

func (f *fakeBinance) coinInfo() (coins []*fakeBinanceCoinInfo) {
f.balanceMtx.Lock()
for symbol, bal := range f.balances {
bigSymbol := strings.ToUpper(symbol)
coins = append(coins, &fakeBinanceCoinInfo{
Coin: bigSymbol,
Free: strconv.FormatFloat(bal.free, 'f', 8, 64),
Locked: strconv.FormatFloat(bal.locked, 'f', 8, 64),
Withdrawing: "0",
NetworkList: []*fakeBinanceNetworkInfo{
{
Coin: bigSymbol,
Network: bigSymbol,
MinConfirm: 1,
WithdrawEnable: true,
WithdrawFee: strconv.FormatFloat(0.00000800, 'f', 8, 64),
WithdrawIntegerMultiple: strconv.FormatFloat(0.00000001, 'f', 8, 64),
WithdrawMax: strconv.FormatFloat(1000, 'f', 8, 64),
WithdrawMin: strconv.FormatFloat(0.01, 'f', 8, 64),
},
},
})
}
f.balanceMtx.Unlock()
return
}

// writeJSON marshals the provided interface and writes the bytes to the
// ResponseWriter with the specified response code.
func writeJSONWithStatus(w http.ResponseWriter, thing interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
b, err := json.Marshal(thing)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Errorf("JSON encode error: %v", err)
return
}
w.WriteHeader(code)
_, err = w.Write(append(b, byte('\n')))
if err != nil {
log.Errorf("Write error: %v", err)
}
}
14 changes: 8 additions & 6 deletions client/comms/wsconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ type WsCfg struct {
// RawHandler overrides the msgjson parsing and forwards all messages to
// the provided function.
RawHandler func([]byte)

ConnectHeaders http.Header
}

// wsConn represents a client websocket connection.
Expand Down Expand Up @@ -231,7 +233,7 @@ func (conn *wsConn) connect(ctx context.Context) error {
dialer.Proxy = http.ProxyFromEnvironment
}

ws, _, err := dialer.DialContext(ctx, conn.cfg.URL, nil)
ws, _, err := dialer.DialContext(ctx, conn.cfg.URL, conn.cfg.ConnectHeaders)
if err != nil {
if isErrorInvalidCert(err) {
conn.setConnectionStatus(InvalidCert)
Expand Down Expand Up @@ -291,7 +293,6 @@ func (conn *wsConn) connect(ctx context.Context) error {
} else {
conn.read(ctx)
}

}()

return nil
Expand Down Expand Up @@ -357,17 +358,17 @@ func (conn *wsConn) close() {

func (conn *wsConn) readRaw(ctx context.Context) {
for {
if ctx.Err() != nil {
return
}

// Lock since conn.ws may be set by connect.
conn.wsMtx.Lock()
ws := conn.ws
conn.wsMtx.Unlock()

// Block until a message is received or an error occurs.
_, msgBytes, err := ws.ReadMessage()
// Drop the read error on context cancellation.
if ctx.Err() != nil {
return
}
if err != nil {
conn.handleReadError(err)
return
Expand Down Expand Up @@ -601,6 +602,7 @@ func (conn *wsConn) Request(msg *msgjson.Message, f func(*msgjson.Message)) erro
// For example, to wait on a response or timeout:
//
// errChan := make(chan error, 1)
//
// err := conn.RequestWithTimeout(reqMsg, func(msg *msgjson.Message) {
// errChan <- msg.UnmarshalResult(responseStructPointer)
// }, timeout, func() {
Expand Down
1 change: 0 additions & 1 deletion client/core/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,6 @@ func (t *trackedTrade) coreOrderInternal() *Order {
counterConfs, int64(t.metaData.ToSwapConf),
int64(mt.redemptionConfs), int64(mt.redemptionConfsReq)))
}

corder.AllFeesConfirmed = allFeesConfirmed

return corder
Expand Down
17 changes: 11 additions & 6 deletions client/mm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ import (
type MarketMakingWithCEXConfig struct {
}

// ArbitrageConfig is the configuration for an arbitrage bot that only places
// when there is a profitable arbitrage opportunity.
type ArbitrageConfig struct {
}

type BalanceType uint8

const (
Percentage BalanceType = iota
Amount
)

// CEXConfig is a configuration for connecting to a CEX API.
type CEXConfig struct {
// CEXName is the name of the cex.
CEXName string `json:"cexName"`
// APIKey is the API key for the CEX.
APIKey string `json:"apiKey"`
// APISecret is the API secret for the CEX.
APISecret string `json:"apiSecret"`
}

// BotConfig is the configuration for a market making bot.
// The balance fields are the initial amounts that will be reserved to use for
// this bot. As the bot trades, the amounts reserved for it will be updated.
Expand All @@ -40,7 +45,7 @@ type BotConfig struct {
// Only one of the following configs should be set
MMCfg *MarketMakingConfig `json:"marketMakingConfig,omitempty"`
MMWithCEXCfg *MarketMakingWithCEXConfig `json:"marketMakingWithCEXConfig,omitempty"`
ArbCfg *ArbitrageConfig `json:"arbitrageConfig,omitempty"`
ArbCfg *SimpleArbConfig `json:"arbConfig,omitempty"`

Disabled bool `json:"disabled"`
}
Expand Down
Loading

0 comments on commit 6a04997

Please sign in to comment.