From e2291d80dd279f900de394250eadb1eb7c6a3d3d Mon Sep 17 00:00:00 2001 From: Tomas Karasek Date: Thu, 18 Jan 2024 19:29:08 +0200 Subject: [PATCH] arbs --- README.md | 43 ++++++++---- arbs.go | 129 ++++++++++++++++++++++++++++++++++++ consts.go | 4 ++ main.go | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 356 insertions(+), 14 deletions(-) create mode 100644 arbs.go diff --git a/README.md b/README.md index bb88394..577fb86 100644 --- a/README.md +++ b/README.md @@ -94,19 +94,36 @@ Getting prices... [ binance-RPLUSDT] 29.94 ``` +You can also check arbitrages - same ticker traded on different exchanges. `pri` will report highest and lowest price and spread. +``` +./pri a! btc-usdt +coinbase-BTC-USDT +huobi-btcusdt +kucoin-BTC-USDT +binance-BTCUSDT +gateio-BTC_USDT +bitfinex-BTCUST +bybit-BTCUSDT +kraken-XBTUSDT +okx-BTC-USDT +Getting prices... +[bitfinex-BTCUST] 41,893.00 +[coinbase-BTC-USDT] 41,883.71 +[ bybit-BTCUSDT] 41,894.25 +[ kraken-XBTUSDT] 41,889.45 +[ okx-BTC-USDT] 41,887.95 +[ huobi-btcusdt] 41,894.04 +[kucoin-BTC-USDT] 41,891.95 +[gateio-BTC_USDT] 41,892.05 +[binance-BTCUSDT] 41,892.10 +name: btc-usdt +min: [coinbase-BTC-USDT] 41,883.71 +max: [ bybit-BTCUSDT] 41,894.25 +spread: 0.025% +``` + + + ## TODO - automate asset list compilation -- make arbitrage groups, like -```golang -var btcUsdtTickers = []ExTick{ - {"coinbase", "BTC-USD"}, - {"binance", "BTCUSDT"}, - {"kraken", "XBTUSDT"}, - {"bitstamp", "btcusdt"}, - {"huobi", "btcusdt"}, - {"kucoin", "BTC-USDT"}, - {"gateio", "BTC_USDT"}, - {"bitfinex", "btcust"}, -} -``` diff --git a/arbs.go b/arbs.go new file mode 100644 index 0000000..22cbf88 --- /dev/null +++ b/arbs.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "strings" +) + +func ExTicksFromSlice(ticks []string) ([]ExTick, error) { + var exTicks []ExTick + for _, tick := range ticks { + split := strings.SplitN(tick, "-", 2) + if len(split) != 2 { + return nil, fmt.Errorf("invalid tick: %s", tick) + } + exchange := split[0] + ticker := split[1] + exTick := ExTick{Exchange(exchange), ticker} + exTicks = append(exTicks, exTick) + } + return exTicks, nil +} + + +var arbs = map[string][]string{ + "eth-btc": { + "binance-ETHBTC", + "bitfinex-ETHBTC", + "coinbase-ETH-BTC", + "gateio-ETH_BTC", + "huobi-ethbtc", + "kraken-XETHXXBT", + "kucoin-ETH-BTC", + "okx-ETH-BTC", + //"bitstamp-ETH/BTC", + }, + "btc-usdt": { + "coinbase-BTC-USDT", + //"bitstamp-BTC/USDT", + "huobi-btcusdt", + "kucoin-BTC-USDT", + "binance-BTCUSDT", + "gateio-BTC_USDT", + "bitfinex-BTCUST", + "bybit-BTCUSDT", + "kraken-XBTUSDT", + "okx-BTC-USDT", + }, + "eth-usdt": { + "binance-ETHUSDT", + "bitfinex-ETHUST", + // bitstamp-ETH/USDT, + "bybit-ETHUSDT", + "coinbase-ETH-USDT", + "gateio-ETH_USDT", + "huobi-ethusdt", + "kraken-ETHUSDT", + "kucoin-ETH-USDT", + "okx-ETH-USDT", + }, + "bnb-btc": { + "binance-BNBBTC", + "kucoin-BNB-BTC", + "gateio-BNB_BTC", + }, + "bnb-usdt": { + "kucoin-BNB-USDT", + "bybit-BNBUSDT", + "okx-BNB-USDT", + "binance-BNBUSDT", + "huobi-bnbusdt", + "gateio-BNB_USDT", + }, + "sol-btc": { + "coinbase-SOL-BTC", + "huobi-solbtc", + "binance-SOLBTC", + "bitfinex-SOLBTC", + "okx-SOL-BTC", + }, + "sol-usdt": { + "kraken-SOLUSDT", + "kucoin-SOL-USDT", + "gateio-SOL_USDT", + "okx-SOL-USDT", + "bitfinex-SOLUST", + "bybit-SOLUSDT", + "coinbase-SOL-USDT", + "binance-SOLUSDT", + "huobi-solusdt", + }, + "sol-eth": { + "coinbase-SOL-ETH", + "kraken-SOLETH", + "okx-SOL-ETH", + "binance-SOLETH", + }, + "xrp-btc": { + //"bitstamp-XRP/BTC", + "huobi-xrpbtc", + "kucoin-XRP-BTC", + "gateio-XRP_BTC", + "bitfinex-XRPBTC", + "binance-XRPBTC", + "okx-XRP-BTC", + }, + "xrp-usdt": { + "binance-XRPUSDT", + "huobi-xrpusdt", + "kucoin-XRP-USDT", + "okx-XRP-USDT", + "coinbase-XRP-USDT", + //"bitstamp-XRP/USDT", + "gateio-XRP_USDT", + "bitfinex-XRPUST", + "bybit-XRPUSDT", + "kraken-XRPUSDT", + }, + "btc-usdc": {}, + "ada-btc": {}, + "ada-usdt": {}, + "avax-btc": {}, + "avax-usdt": {}, + "doge-btc": {}, + "doge-usdt": {}, + "trx-btc": {}, + "trx-usdt": {}, + "dot-btc": {}, + "dot-usdt": {}, +} diff --git a/consts.go b/consts.go index db71322..92785a8 100644 --- a/consts.go +++ b/consts.go @@ -3,3 +3,7 @@ package main var findCommands = []string{ "f", "fi", "fin", "find", "f!", "fi!", "fin!", "find!", } + +var arbitrageCommands = []string{ + "a", "ar", "arb", "a!", "ar!", "arb!", +} diff --git a/main.go b/main.go index d5ac6ba..a30cf59 100644 --- a/main.go +++ b/main.go @@ -18,11 +18,112 @@ type ExTick struct { Ticker string } +type ExTickSet struct { + Name string + ExTicks []ExTick +} + type ExTickPri struct { ExTick Price float64 } +type ExTickPris []ExTickPri + +func (etps ExTickPris) Min() ExTickPri { + min := etps[0] + for _, v := range etps { + if v.Price < min.Price { + min = v + } + } + return min +} + +func (etps ExTickPris) String() string { + str := "" + for _, v := range etps { + str += fmt.Sprintf("%s\n", v) + } + return str +} + +func ExTickPrisToArbResult(n string, etps ExTickPris) ArbResult { + return ArbResult{ + Name: n, + TickPris: etps, + Min: etps.Min(), + Max: etps.Max(), + SpreadPercent: SpreadType(etps.SpreadPercent()), + } +} + +type SpreadType float64 + +func (st SpreadType) String() string { + return fmt.Sprintf("%.3f%%", st) +} + +type ArbResult struct { + Name string + TickPris ExTickPris + Min ExTickPri + Max ExTickPri + SpreadPercent SpreadType +} + +func (ar ArbResult) String() string { + return fmt.Sprintf("name: %s\nmin: %s\nmax: %s\nspread: %s", ar.Name, ar.Min, ar.Max, ar.SpreadPercent) +} + +type ArbResults []ArbResult + +func (ars ArbResults) HighestSpread() ArbResult { + max := ars[0] + for _, v := range ars { + if v.SpreadPercent > max.SpreadPercent { + max = v + } + } + return max +} + +func (ars ArbResults) LowestSpread() ArbResult { + min := ars[0] + for _, v := range ars { + if v.SpreadPercent < min.SpreadPercent { + min = v + } + } + return min +} + +func (ars ArbResults) Report() { + for _, v := range ars { + fmt.Println(v.Name, v.SpreadPercent) + } + fmt.Println("Highest spread:") + fmt.Println(ars.HighestSpread()) + fmt.Println("Lowest spread:") + fmt.Println(ars.LowestSpread()) +} + +func (etps ExTickPris) Max() ExTickPri { + max := etps[0] + for _, v := range etps { + if v.Price > max.Price { + max = v + } + } + return max +} + +func (etps ExTickPris) SpreadPercent() float64 { + min := etps.Min() + max := etps.Max() + return (max.Price - min.Price) / min.Price * 100 +} + func (etp ExTickPri) String() string { et := etp.ExTick return fmt.Sprintf("[%15s]\t%s", et, formatFloat(etp.Price)) @@ -178,6 +279,33 @@ func main() { panic("usage: pri [find] ") } if len(os.Args) == 2 { + if stringIsInSlice(os.Args[1], arbitrageCommands) { + fmt.Println("arbitrage groups:") + for k, v := range arbs { + fmt.Printf("%s: %s\n", k, v) + } + return + } + if os.Args[1] == "aa" { + exTickSets := []ExTickSet{} + for name, a := range arbs { + arbTicks, err := ExTicksFromSlice(a) + if err != nil { + panic(err) + } + if len(arbTicks) < 2 { + fmt.Printf("Arbitrage group %s has less than 2 tickers, skipping\n", a) + continue + } + exTickSets = append(exTickSets, ExTickSet{name, arbTicks}) + } + arbResultChannel := getArbResultAsync(exTickSets) + ars := CollectArbResultsFromChannel(arbResultChannel, len(exTickSets)) + + ars.Report() + return + } + xt, err := findExTick(os.Args[1]) if err != nil { panic(err) @@ -211,7 +339,8 @@ func main() { } if strings.Contains(cmd, "!") { fmt.Println("Getting prices...") - tickerChannel := make(chan *ExTickPri) + tickerChannel := getExTickPriAsync(found) + for _, v := range found { go getExchangeTickerPriceAsync(v, tickerChannel) } @@ -220,7 +349,70 @@ func main() { fmt.Println(etp) } } + } else if stringIsInSlice(cmd, arbitrageCommands) { + arbName := os.Args[2] + arb, ok := arbs[arbName] + if !ok { + fmt.Printf("Arbitrage group %s not found\n", arbName) + return + } + for _, v := range arb { + fmt.Println(v) + } + if strings.Contains(cmd, "!") { + fmt.Println("Getting prices...") + arbTicks, err := ExTicksFromSlice(arb) + if err != nil { + panic(err) + } + arbResult := ExTicksToArbResult(ExTickSet{arbName, arbTicks}) + fmt.Println(arbResult) + } } } } + +func getArbResultAsync(exTickSets []ExTickSet) chan ArbResult { + arbResultChannel := make(chan ArbResult) + for _, v := range exTickSets { + go func(ets ExTickSet) { + arbResultChannel <- ExTicksToArbResult(ets) + }(v) + } + return arbResultChannel +} + +func CollectArbResultsFromChannel(channel chan ArbResult, n int) ArbResults { + result := ArbResults{} + for i := 0; i < n; i++ { + ar := <-channel + result = append(result, ar) + } + return result +} + +func ExTicksToArbResult(ets ExTickSet) ArbResult { + tickerChannel := getExTickPriAsync(ets.ExTicks) + result := CollectExTickPrisFromChannel(tickerChannel, len(ets.ExTicks)) + arbResult := ExTickPrisToArbResult(ets.Name, result) + return arbResult +} + +func getExTickPriAsync(tickers []ExTick) chan *ExTickPri { + tickerChannel := make(chan *ExTickPri) + for _, v := range tickers { + go getExchangeTickerPriceAsync(v, tickerChannel) + } + return tickerChannel +} + +func CollectExTickPrisFromChannel(channel chan *ExTickPri, n int) ExTickPris { + result := ExTickPris{} + for i := 0; i < n; i++ { + etp := <-channel + fmt.Println(etp) + result = append(result, *etp) + } + return result +}