├── bg.go ├── doc.go ├── main.go ├── misc.go ├── readme.md ├── rest.go ├── run.bat ├── types.go └── ws.go /bg.go: -------------------------------------------------------------------------------- 1 | /* 2 | Бот создан в рамках проекта Bablofil, подробнее тут 3 | https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | или на форуме https://forum.bablofil.ru/ 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "math" 13 | "os" 14 | "os/signal" 15 | "sort" 16 | "strconv" 17 | //"time" 18 | ) 19 | 20 | // Эта функция проверяет имеющиеся цепи на профитность с учетом текущих курсов 21 | // полученных в других потоках, создает ордера, если это нужно 22 | func process_chains() { 23 | 24 | done := make(chan struct{}) 25 | 26 | flag.Parse() 27 | 28 | interrupt := make(chan os.Signal, 1) 29 | signal.Notify(interrupt, os.Interrupt) 30 | 31 | go func() { 32 | defer close(done) 33 | for { 34 | 35 | // Если в данный момент нет открытых ордеров, то проверить, нет ли профитной ситуации 36 | if currentProfitObj == nil { 37 | // Проходим в цикле по каждой возможной цепи 38 | for _, po := range chains { 39 | 40 | s := po.Chain 41 | 42 | var coins float64 = 0 43 | 44 | str := "" 45 | fits := 0 46 | var multy_factor float64 = 0 47 | 48 | // Теперь цикл по каждому звену выбрынной цепи 49 | for i := 0; i < CHAIN_LEN; i++ { 50 | // Берем действущие ограничения для пары 51 | interf := s[i].S.Symbol.Filters[2] 52 | 53 | filter_map := interf.(map[string]interface{}) 54 | stepSize, _ := strconv.ParseFloat(filter_map["stepSize"].(string), 64) 55 | // Для первой сделки считаем кол-во монет к покупке 56 | if i == 0 && s[i].Type == "BUY" { 57 | 58 | coins = BASE_AMOUNT / s[i].S.CurrA 59 | 60 | if s[i].S.CurrAVol >= coins { 61 | fits += 1 62 | multy_factor = s[i].S.CurrAVol / coins 63 | } 64 | coins = math.Floor(coins*(1/stepSize)) / (1 / stepSize) 65 | s[i].MinCoins = coins 66 | 67 | } else { 68 | /* 69 | для каждой последующей сделки предварительно считаем, 70 | сколько монет нужно купить или продать, исходя из ранее полученного кол-ва 71 | */ 72 | if s[i].Type == "SELL" { 73 | 74 | coins = math.Floor(coins*(1/stepSize)) / (1 / stepSize) 75 | s[i].MinCoins = coins 76 | tmp_multy_factor := s[i].S.CurrBVol / coins 77 | 78 | if s[i].S.CurrBVol >= coins { 79 | fits += 1 80 | if tmp_multy_factor < multy_factor { 81 | multy_factor = tmp_multy_factor 82 | } 83 | } 84 | 85 | coins = coins * s[i].S.CurrB 86 | 87 | } else { 88 | 89 | coins = coins / s[i].S.CurrA 90 | 91 | tmp_multy_factor := s[i].S.CurrAVol / coins 92 | 93 | if s[i].S.CurrAVol >= coins { 94 | fits += 1 95 | 96 | if tmp_multy_factor < multy_factor { 97 | multy_factor = tmp_multy_factor 98 | } 99 | } 100 | coins = math.Floor(coins*(1/stepSize)) / (1 / stepSize) 101 | s[i].MinCoins = coins 102 | } 103 | } 104 | 105 | // Формируем строку, которая будет отображена в логе 106 | str += s[i].S.Symbol.Symbol + " " + s[i].Type + " " + fmt.Sprintf("ASK %0.8f BID %0.8f (NEED COINS %0.8f, ASK VOL %0.8f, BID VOL %0.8f) MF (%0.8f) LastUpdated %s\n", s[i].S.CurrA, s[i].S.CurrB, coins, s[i].S.CurrAVol, s[i].S.CurrBVol, multy_factor, s[i].S.LastUpdated) + " " 107 | 108 | } 109 | 110 | // Минимальное кол-во монет, которое отдадим на комисии на первом шаге 111 | min_fee := BASE_AMOUNT * FEE_SIZE 112 | /* 113 | Если денег в стакане больше, чем минимальная ставка, 114 | то умножить мин ставку это значение - должно получиться 115 | число в диапазоне минимальной и максимальной ставок 116 | */ 117 | po.MultyFactor = multy_factor 118 | 119 | invest_diff := MAX_AMOUNT / BASE_AMOUNT 120 | if po.MultyFactor > invest_diff { 121 | po.MultyFactor = invest_diff 122 | } 123 | /* 124 | Предварительный подсчет прибыли за вычетом комиссий 125 | Т.к. на каждом шаге мы платим комиссии от разных объемов разных монет, 126 | приводим все к живым деньгам в валюте начала торгов 127 | */ 128 | coins_prof := coins - coins*FEE_SIZE - min_fee*(CHAIN_LEN-1) - BASE_AMOUNT 129 | po.Profit = (coins_prof / BASE_AMOUNT) * 100 130 | // Дополняем строку, которая пойдет в лог 131 | str += " !!! " + fmt.Sprintf("%0.8f%%", po.Profit) + " (" + fmt.Sprintf(" MF %0.8f, %0.8f", po.MultyFactor, coins_prof) + ") !!! " 132 | // Если объемы торгов на каждом шаге подходят под наши объемы торгов, то эта цепь подходит 133 | po.Fits = fits == CHAIN_LEN-1 134 | po.Description = str 135 | 136 | } 137 | // После прохода по всем цепям сортируем все цепи по профитности 138 | sort.Slice(chains[:], func(i, j int) bool { 139 | if !math.IsInf(chains[i].Profit, 0) && !math.IsInf(chains[j].Profit, 0) && chains[i].Profit > 0 && chains[j].Profit > 0 { 140 | return chains[i].Profit > chains[j].Profit 141 | } else { 142 | return true 143 | } 144 | }) 145 | // Т.к. положительная бесконечность это тоже значение, берем все пары, но оставляем только одну, без бесконечностей 146 | // TODO: ужасный кусок кода, надо переделать 147 | for i, po := range chains { 148 | if i == 0 { 149 | logger.Println(po.Description) 150 | } 151 | if po.Profit >= 0.1 && po.Fits && !math.IsInf(po.Profit, 0) { 152 | /* 153 | Если пара подходит по объемам и профит больше, чем 0.1%, 154 | выводим информацию в лог и создаем первый ордер 155 | */ 156 | logger.Println(po.Description) 157 | 158 | currentProfitObj = po 159 | if !create_order(po.Chain[0].S.Symbol.Symbol, po.Chain[0].MinCoins, "BUY") { 160 | currentProfitObj = nil 161 | } else { 162 | // Если удалось создать ордер, выходим из цикла 163 | break 164 | } 165 | } 166 | 167 | } 168 | 169 | } else { // Ордер был создан, не проверяем цепи, обслуживаем ордера 170 | 171 | currOrderId := currentProfitObj.OrderId 172 | if order_info, ok := orders[currOrderId]; ok { 173 | //Если информация по ордеру присутствует в словаре, значит он был исполнен на бирже и мы получили эту информацию 174 | if currentProfitObj.Phase >= CHAIN_LEN { 175 | // Все ордера во всей цепи были исполнены 176 | currentProfitObj.mx.Lock() 177 | currentProfitObj.Phase = 0 178 | currentProfitObj = nil 179 | //Если есть желание убивать бота после исполнения цепочки ордеров, можно раскомментировать следующую строку 180 | //logger.Panic("Things done") 181 | 182 | } else { 183 | // Исполнен промежуточный ордер, нужно создать следующий 184 | symbol := currentProfitObj.Chain[currentProfitObj.Phase].S.Symbol.Symbol 185 | quantity := order_info.GotQty 186 | 187 | side := currentProfitObj.Chain[currentProfitObj.Phase].Type 188 | if side == "BUY" { 189 | quantity = quantity / symbols[symbol].CurrA 190 | } 191 | 192 | interf := currentProfitObj.Chain[currentProfitObj.Phase].S.Symbol.Filters[2] 193 | filter_map := interf.(map[string]interface{}) 194 | stepSize, _ := strconv.ParseFloat(filter_map["stepSize"].(string), 64) 195 | quantity = math.Floor(quantity*(1/stepSize)) / (1 / stepSize) 196 | 197 | create_order(symbol, quantity, side) 198 | } 199 | } 200 | 201 | } 202 | } 203 | }() 204 | 205 | for { 206 | select { 207 | case <-done: 208 | return 209 | 210 | case <-interrupt: 211 | logger.Println("interrupt CHAINS") 212 | wg.Done() 213 | return 214 | } 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // experiment project doc.go 2 | 3 | /* 4 | experiment document 5 | */ 6 | package main 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Бот создан в рамках проекта Bablofil, подробнее тут 3 | https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | или на форуме https://forum.bablofil.ru/ 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "io" 11 | "log" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | const CHAIN_LEN = 3 // Длина цепи, по которой работаем (не менять) 19 | const BASE_COIN = "BTC" // С этой монеты начинается и заканчивается цепь 20 | const BASE_AMOUNT = 0.003 // Сколько минимально денег вкладывать на первую покупку 21 | const MAX_AMOUNT = 0.005 // Сколько максимально вкладывать (не 22 | const FEE_SIZE = 0.00075 // Размер комиссии 23 | 24 | // Ключи API 25 | const API_KEY = "" 26 | const API_SECRET = "" 27 | 28 | var logger *log.Logger // Логирует (в консоль и в файл) 29 | var symbols map[string]*SymbolObj // Хранит информацию о паре 30 | var exchangeInfo exchangeInfo_struct // Репрезентует структуру ExchangeInfo binance 31 | var wg sync.WaitGroup // Для группировки потоков (ждем окончание каждого) 32 | 33 | var listen_key string // При запуске подпишемся на обновление истории своих торгов через сокеты 34 | 35 | var chains []*ProfitObj // Тут будем хранить всевозможные цепи, и сопутствующую информацию 36 | var orders map[int64]OrderInfo // Тут храним открытые непроверенные ордера 37 | 38 | var currentProfitObj *ProfitObj //Текущая открытая цепь ордеров 39 | 40 | // Точка входа в программу 41 | func main() { 42 | 43 | // В текущей директории создаем файл log.txt 44 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | f, err := os.OpenFile(dir+"/log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 50 | if err != nil { 51 | log.Fatalf("error opening file: %v", err) 52 | } 53 | defer f.Close() 54 | 55 | // Лог будем писать одновременно и в файл, и выводить на консоль 56 | mw := io.MultiWriter(os.Stdout, f) 57 | log.SetOutput(mw) 58 | 59 | // При логировании будем выводить подробную информацию, включая время, название файла, строку и т.п. 60 | logger = log.New(mw, "", -1) 61 | logger.SetFlags(-1) 62 | 63 | logger.Println("started") 64 | // Инициализация начальных объектов 65 | symbols = make(map[string]*SymbolObj) 66 | orders = make(map[int64]OrderInfo) 67 | exchangeInfo = get_exchange_info() 68 | 69 | // Создаем локальное представление пар со всеми свойствами (лимиты, округления и т.п.) 70 | for _, symbol := range exchangeInfo.Symbols { 71 | tmp_s := SymbolObj{CurrB: 0, CurrA: 0, Symbol: symbol, BTree: nil, ATree: nil} 72 | symbols[symbol.Symbol] = &tmp_s 73 | } 74 | 75 | // На старте программы строим цепи для торговли, в дальнейшем будем проверять именно их 76 | // Первой операцией всегда будет покупка, последней продажа, в промежутке тип зависит от пар 77 | for _, v := range exchangeInfo.Symbols { 78 | if v.Status == "TRADING" { 79 | 80 | if strings.HasSuffix(v.Symbol, BASE_COIN) { 81 | pref1 := v.Symbol[:len(v.Symbol)-len(BASE_COIN)] 82 | for _, v2 := range exchangeInfo.Symbols { 83 | if (strings.HasSuffix(v2.Symbol, pref1) || strings.HasPrefix(v2.Symbol, pref1)) && v2.Status == "TRADING" && !strings.HasSuffix(v2.Symbol, BASE_COIN) { 84 | var pref2 string 85 | 86 | middle_type := "SELL" 87 | if strings.HasSuffix(v2.Symbol, pref1) { 88 | pref2 = v2.Symbol[:len(v2.Symbol)-len(pref1)] 89 | middle_type = "BUY" 90 | } else { 91 | if v2.Symbol[:len(v2.Symbol)-len(pref1)] == "BTC" || v2.Symbol[:len(v2.Symbol)-len(pref1)] == "ETH" || v2.Symbol[:len(v2.Symbol)-len(pref1)] == "BNB" || v2.Symbol[:len(v2.Symbol)-len(pref1)] == "USDT" { 92 | pref2 = v2.Symbol[len(pref1):] 93 | } else { 94 | continue 95 | } 96 | } 97 | 98 | for _, v3 := range exchangeInfo.Symbols { 99 | 100 | if pref2+BASE_COIN == v3.Symbol && v3.Status == "TRADING" { 101 | 102 | var ch [CHAIN_LEN]*DealObj 103 | d1 := DealObj{S: symbols[v.Symbol], Type: "BUY", MinCoins: 0} 104 | d2 := DealObj{S: symbols[v2.Symbol], Type: middle_type, MinCoins: 0} 105 | d3 := DealObj{S: symbols[v3.Symbol], Type: "SELL", MinCoins: 0} 106 | 107 | ch[0] = &d1 108 | ch[1] = &d2 109 | ch[2] = &d3 110 | var po ProfitObj = ProfitObj{Chain: ch, Profit: 0, Phase: 0} 111 | chains = append(chains, &po) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | } 120 | 121 | // Получаем ключ, с которым будем подписываться на веб-сокеты изменения ордеров пользователя и аккаунта 122 | get_listen_key() 123 | 124 | // Binance просит ключ продлевать, так что отдельным потоком обновляем полученный ключ с определенным интервалом 125 | go reload_listen_key(&wg) 126 | // Это бесконечный процесс, и основной поток не должен заканчивать работу раньше него 127 | wg.Add(1) 128 | 129 | // Непосредственно подписка на веб-сокеты аккаунта и ордеров 130 | go orders_listener(&wg) 131 | wg.Add(1) 132 | 133 | /* 134 | Для построения локального стакана мы запросим с биржи текущее состояние 135 | по каждой паре, и потом будем обновлять через сокеты 136 | */ 137 | for _, link := range symbols { 138 | go get_depth(link.Symbol.Symbol, &wg) 139 | wg.Add(1) 140 | } 141 | 142 | /* 143 | Это фоновый поток, который работает с полученными стаканами, 144 | проверяет цепи на профитность, создает ордера, если пришло время, 145 | проверяет их состояние, создает новые, если цепь требует 146 | */ 147 | go process_chains() 148 | wg.Add(1) 149 | 150 | // Через сокеты подписываемся на все пары, обновляем стаканы 151 | go ws_listener(&wg) 152 | wg.Add(1) 153 | 154 | wg.Wait() 155 | } 156 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Бот создан в рамках проекта Bablofil, подробнее тут 3 | https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | или на форуме https://forum.bablofil.ru/ 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "math" 11 | "strconv" 12 | ) 13 | 14 | /* Тут в основном идет работа с деревьями */ 15 | 16 | func insert_leaf(l *leaf, price float64, vol float64) *leaf { 17 | /* If the tree is empty, return a new node */ 18 | if l == nil { 19 | return &leaf{left: nil, right: nil, price: price, vol: vol} 20 | } 21 | /* Otherwise, recur down the tree */ 22 | if price < l.price { 23 | l.left = insert_leaf(l.left, price, vol) 24 | } else { 25 | if price > l.price { 26 | l.right = insert_leaf(l.right, price, vol) 27 | } else { 28 | l.vol = vol 29 | } 30 | } 31 | 32 | /* return the (unchanged) node pointer */ 33 | 34 | return l 35 | } 36 | 37 | // A utility function to do inorder traversal of BST 38 | func traverse(l *leaf) { 39 | if l != nil { 40 | traverse(l.left) 41 | traverse(l.right) 42 | } 43 | } 44 | 45 | func get_lowest_price(root *leaf) (float64, float64) { 46 | // Returns maximum value in a given Binary Tree 47 | 48 | // Base case 49 | if root == nil { 50 | return math.Inf(0), math.Inf(0) 51 | } 52 | 53 | // Return maximum of 3 values: 54 | // 1) Root's data 2) Max in Left Subtree 55 | // 3) Max in right subtree 56 | 57 | var res float64 = math.Inf(0) 58 | var vol float64 = 0 59 | 60 | var lres float64 61 | var lvol float64 62 | 63 | var rres float64 64 | var rvol float64 65 | 66 | if root.vol > 0 { 67 | res = root.price 68 | vol = root.vol 69 | } 70 | 71 | lres, lvol = get_lowest_price(root.left) 72 | rres, rvol = get_lowest_price(root.right) 73 | if lres < res { 74 | res = lres 75 | vol = lvol 76 | } 77 | if rres < res { 78 | res = rres 79 | vol = rvol 80 | } 81 | return res, vol 82 | 83 | } 84 | 85 | func get_highest_price(root *leaf) (float64, float64) { 86 | // Returns maximum value in a given Binary Tree 87 | 88 | // Base case 89 | if root == nil { 90 | return 0, 0 91 | } 92 | 93 | // Return maximum of 3 values: 94 | // 1) Root's data 2) Max in Left Subtree 95 | // 3) Max in right subtree 96 | 97 | var res float64 = 0 98 | var vol float64 = 0 99 | 100 | if root.vol > 0 { 101 | res = root.price 102 | vol = root.vol 103 | } 104 | var lres float64 105 | var lvol float64 106 | 107 | var rres float64 108 | var rvol float64 109 | 110 | lres, lvol = get_highest_price(root.left) 111 | rres, rvol = get_highest_price(root.right) 112 | if lres > res { 113 | res = lres 114 | vol = lvol 115 | } 116 | if rres > res { 117 | res = rres 118 | vol = rvol 119 | } 120 | return res, vol 121 | 122 | } 123 | 124 | func update_tree(dataset [][]string, l *leaf) *leaf { 125 | var root *leaf = nil 126 | for _, v := range dataset { 127 | price, _ := strconv.ParseFloat(v[0], 32) 128 | vol, _ := strconv.ParseFloat(v[1], 32) 129 | new_l := insert_leaf(l, price, vol) 130 | if root == nil { 131 | root = new_l 132 | } 133 | } 134 | return root 135 | } 136 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Бот для Binance 2 | Написан на Golang, использует сокеты для синхронизации стаканов и обновления информации об ордерах пользователя. 3 | Подробнее описано тут https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | 5 | Требования 6 | go.1.11.1 7 | 8 | Как пользоваться 9 | 1. Установить Go (Проверялась работа под Windows и Linux) 10 | 2. Установить зависимости (go get -u github.com/gorilla/websocket) 11 | 3. В файле main.go указать ключи API 12 | 13 | // Ключи API 14 | const API_KEY = "" 15 | const API_SECRET = "" 16 | 17 | 4. go build -o arbitrage.exe 18 | 5. ./arbitrage.exe 19 | 20 | Возможны ситуации, когда например рвется коннект или биржа откзывает в обслуживании - в этом случае программа экстренно прекращает работу. Для этого можно запускать его во внешнем watchdog - см. приложенный .bat файл для примере. 21 | В linux это будет watch или другой, привычный вам инструмент. 22 | 23 | Так же скорее всего большую часть времени бот будет выслеживать - такие ситуации на Binance редки, а если кодом будут пользоваться многие, то ситуация усугубится. 24 | 25 | Что бы проверить работу, вы можете на бирже создать/отменить ордер, и в запущенной консоли бота увидите информацию об этом - бот должен поймать событие через сокеты. 26 | 27 | Ну и, разумеется, вы можете добавить вызовов logger.Println("...") тут и там, всю информацию бот выводит на экран и пишет в log.txt в папке с ботом. 28 | 29 | 30 | -------------------------------------------------------------------------------- /rest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Бот создан в рамках проекта Bablofil, подробнее тут 3 | https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | или на форуме https://forum.bablofil.ru/ 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "crypto/hmac" 12 | "crypto/sha256" 13 | "encoding/json" 14 | "flag" 15 | "fmt" 16 | "io/ioutil" 17 | "math" 18 | "net/http" 19 | "net/url" 20 | "os" 21 | "os/signal" 22 | "strconv" 23 | "sync" 24 | "time" 25 | ) 26 | 27 | // Получить информацию по всем парам - список, лимиты, ограничения и т.п. 28 | func get_exchange_info() exchangeInfo_struct { 29 | //запросить страницу 30 | url := "https://api.binance.com/api/v1/exchangeInfo" 31 | resp, err := http.Get(url) 32 | if err != nil { 33 | logger.Print(err) 34 | } 35 | defer resp.Body.Close() 36 | 37 | //Получить тело ответа 38 | body, err := ioutil.ReadAll(resp.Body) 39 | if err != nil { 40 | logger.Print(err) 41 | } 42 | 43 | //структура для хранения 44 | var exch_local exchangeInfo_struct 45 | 46 | //парсим JSON в структуру 47 | err = json.Unmarshal(body, &exch_local) 48 | if err != nil { 49 | logger.Print(err) 50 | } 51 | 52 | return exch_local 53 | } 54 | 55 | // На старте программы получить текущие стаканы по каждой паре 56 | func get_depth(pair string, wg *sync.WaitGroup) { 57 | 58 | defer wg.Done() 59 | 60 | //запросить страницу 61 | url := "https://api.binance.com/api/v1/depth?symbol=" + pair 62 | resp, err := http.Get(url) 63 | if err != nil { 64 | logger.Print(err) 65 | } 66 | defer resp.Body.Close() 67 | 68 | //Получить тело ответа 69 | body, err := ioutil.ReadAll(resp.Body) 70 | if err != nil { 71 | logger.Print(err) 72 | } 73 | 74 | //структура для хранения 75 | var depth_local Depth 76 | 77 | //парсим JSON в структуру 78 | err = json.Unmarshal(body, &depth_local) 79 | if err != nil { 80 | //logger.Print(err) 81 | } 82 | 83 | curr_s := symbols[pair] 84 | 85 | //fill bids tree 86 | root := update_tree(depth_local.Bids, curr_s.BTree) 87 | if curr_s.BTree == nil { 88 | curr_s.BTree = root 89 | } 90 | curr_s.CurrB, curr_s.CurrBVol = get_highest_price(curr_s.BTree) 91 | 92 | //fill asks tree 93 | root = update_tree(depth_local.Asks, curr_s.ATree) 94 | if curr_s.ATree == nil { 95 | curr_s.ATree = root 96 | } 97 | curr_s.CurrA, curr_s.CurrAVol = get_lowest_price(curr_s.ATree) 98 | 99 | currentTime := time.Now() 100 | curr_s.LastUpdated = currentTime.Format("2006-01-02 15:04:05.000000000") 101 | } 102 | 103 | // Подписаться на обновления по состоянию своих ордеров через сокеты 104 | func get_listen_key() { 105 | 106 | ds_url := "https://api.binance.com/api/v1/userDataStream" 107 | 108 | client := &http.Client{} 109 | req, _ := http.NewRequest("POST", ds_url, nil) 110 | req.Header.Set("X-MBX-APIKEY", API_KEY) 111 | 112 | resp, err := client.Do(req) 113 | 114 | if nil != err { 115 | logger.Panic("errorination happened getting the response", err) 116 | } 117 | 118 | defer resp.Body.Close() 119 | body, err := ioutil.ReadAll(resp.Body) 120 | 121 | if nil != err { 122 | logger.Panic("errorination happened reading the body", err) 123 | 124 | } 125 | 126 | //структура для хранения 127 | var lk_local ListenKeyObj 128 | 129 | //парсим JSON в структуру 130 | err = json.Unmarshal(body, &lk_local) 131 | if err != nil { 132 | logger.Panic(err) 133 | } 134 | 135 | listen_key = lk_local.ListenKey 136 | //logger.Println("LK", listen_key) 137 | } 138 | 139 | // Время от времени нужно переподключаться к сокету своих ордеров 140 | func reload_listen_key(wg *sync.WaitGroup) { 141 | defer wg.Done() 142 | 143 | done := make(chan struct{}) 144 | 145 | flag.Parse() 146 | 147 | interrupt := make(chan os.Signal, 1) 148 | signal.Notify(interrupt, os.Interrupt) 149 | 150 | //cnt := 0 151 | go func() { 152 | defer close(done) 153 | for { 154 | time.Sleep(1 * time.Minute) 155 | 156 | ds_url := "https://api.binance.com/api/v1/userDataStream" 157 | 158 | buffer := new(bytes.Buffer) 159 | params := url.Values{} 160 | params.Set("listenKey", listen_key) 161 | 162 | buffer.WriteString(params.Encode()) 163 | 164 | client := &http.Client{} 165 | req, _ := http.NewRequest("PUT", ds_url, buffer) 166 | req.Header.Set("X-MBX-APIKEY", API_KEY) 167 | 168 | resp, err := client.Do(req) 169 | 170 | if nil != err { 171 | logger.Panic("errorination happened getting the response", err) 172 | } 173 | 174 | defer resp.Body.Close() 175 | _, err = ioutil.ReadAll(resp.Body) 176 | 177 | if nil != err { 178 | logger.Panic("errorination happened reading the body", err) 179 | } 180 | } 181 | }() 182 | 183 | select { 184 | case <-done: 185 | return 186 | 187 | case <-interrupt: 188 | logger.Println("interrupt LK") 189 | wg.Done() 190 | return 191 | } 192 | 193 | } 194 | 195 | // Закрыть поток получения данных по ордерам 196 | func close_listen_key() { 197 | ds_url := "https://api.binance.com/api/v1/userDataStream" 198 | 199 | buffer := new(bytes.Buffer) 200 | params := url.Values{} 201 | params.Set("listenKey", listen_key) 202 | 203 | buffer.WriteString(params.Encode()) 204 | 205 | client := &http.Client{} 206 | req, _ := http.NewRequest("DELETE", ds_url, buffer) 207 | req.Header.Set("X-MBX-APIKEY", API_KEY) 208 | 209 | resp, err := client.Do(req) 210 | 211 | if nil != err { 212 | logger.Panic("errorination happened getting the response", err) 213 | } 214 | 215 | defer resp.Body.Close() 216 | body, err := ioutil.ReadAll(resp.Body) 217 | 218 | if nil != err { 219 | logger.Panic("errorination happened reading the body", err) 220 | } 221 | 222 | fmt.Println("LK %s", string(body[:])) 223 | 224 | } 225 | 226 | //Генерация криптографической подписи для rest запросов 227 | func get_signature(message string) string { 228 | mac := hmac.New(sha256.New, []byte(API_SECRET)) 229 | mac.Write([]byte(message)) 230 | return fmt.Sprintf("%x", (mac.Sum(nil))) 231 | } 232 | 233 | //Текущее время 234 | func currentTimestamp() int64 { 235 | return int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond) - 2000 236 | } 237 | 238 | //Непосредственно создание ордера 239 | func create_order(pair string, quantity float64, side string) bool { 240 | 241 | logger.Println(fmt.Sprintf("Going to make %s order: symbol %s, quantity:%0.8f", side, pair, quantity)) 242 | currentProfitObj.mx.Lock() 243 | defer currentProfitObj.mx.Unlock() 244 | 245 | var order_created = false 246 | 247 | curr_ts := currentTimestamp() 248 | 249 | buffer := new(bytes.Buffer) 250 | 251 | params := url.Values{} 252 | params.Set("symbol", pair) 253 | params.Set("side", side) 254 | 255 | params.Set("type", "MARKET") 256 | 257 | params.Set("newOrderRespType", "RESULT") 258 | params.Set("quantity", fmt.Sprintf("%0.8f", quantity)) 259 | params.Set("timestamp", fmt.Sprintf("%d", curr_ts)) 260 | 261 | signature := get_signature(params.Encode()) 262 | params.Set("signature", signature) 263 | 264 | order_url := fmt.Sprintf("https://api.binance.com/api/v3/order") 265 | 266 | logger.Println(params) 267 | buffer.WriteString(params.Encode()) 268 | 269 | client := &http.Client{} 270 | req, _ := http.NewRequest("POST", order_url, buffer) 271 | req.Header.Set("X-MBX-APIKEY", API_KEY) 272 | 273 | resp, err := client.Do(req) 274 | 275 | if nil != err { 276 | logger.Panic("errorination happened getting the response", err) 277 | } 278 | 279 | defer resp.Body.Close() 280 | body, err := ioutil.ReadAll(resp.Body) 281 | 282 | if nil != err { 283 | logger.Panic("errorination happened reading the body", err) 284 | } 285 | 286 | fmt.Println("LK %s", string(body[:])) 287 | 288 | var order_obj OrderResult 289 | err = json.Unmarshal(body, &order_obj) 290 | if err != nil { 291 | logger.Println("Error at creation", err, body) 292 | } 293 | 294 | logger.Println("OrderObj", order_obj.OrderId, order_obj.Status, order_obj) 295 | 296 | if order_obj.OrderId != 0 { 297 | executed, _ := strconv.ParseFloat(order_obj.ExecutedQty, 32) 298 | spent, _ := strconv.ParseFloat(order_obj.CummulativeQuoteQty, 32) 299 | 300 | currentProfitObj.OrderId = order_obj.OrderId 301 | 302 | logger.Println("Order created", executed, order_obj) 303 | //Может получиться так, что ордер исполнится до того, как вернется REST ответ, поэтому можно сразу загнать его в словарь 304 | //НО! Может получиться так же так, что информация по исполненному ордеру прилетит по сокету до ответа через REST и к моменту получения ответа ордер уже будет в словаре 305 | if order_obj.Status == "FILLED" { 306 | if _, ok := orders[order_obj.OrderId]; !ok { 307 | var order_info = OrderInfo{OrderId: order_obj.OrderId, Symbol: order_obj.Symbol, Side: order_obj.Side, SpentQty: spent, GotQty: executed} 308 | orders[order_obj.OrderId] = order_info 309 | logger.Println("OrderInfo", order_info) 310 | if currentProfitObj.Phase+1 < CHAIN_LEN { 311 | if currentProfitObj.Chain[currentProfitObj.Phase+1].Type == "SELL" { 312 | currentProfitObj.Chain[currentProfitObj.Phase+1].MinCoins = executed 313 | } else { 314 | 315 | interf := currentProfitObj.Chain[currentProfitObj.Phase+1].S.Symbol.Filters[2] 316 | filter_map := interf.(map[string]interface{}) 317 | stepSize, _ := strconv.ParseFloat(filter_map["stepSize"].(string), 64) 318 | 319 | coins := executed / currentProfitObj.Chain[currentProfitObj.Phase+1].S.CurrA 320 | coins = math.Floor(coins*(1/stepSize)) / (1 / stepSize) 321 | currentProfitObj.Chain[currentProfitObj.Phase+1].MinCoins = coins 322 | 323 | } 324 | } 325 | currentProfitObj.Phase += 1 326 | } 327 | 328 | } 329 | 330 | order_created = true 331 | } else { 332 | logger.Println("Failed to create order") 333 | } 334 | 335 | return order_created 336 | } 337 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | :loop 2 | 3 | arb.exe 4 | 5 | goto loop -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Бот создан в рамках проекта Bablofil, подробнее тут 3 | https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | или на форуме https://forum.bablofil.ru/ 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "sync" 11 | ) 12 | 13 | /* Структуры для деревьев хранения стаканов */ 14 | type leaf struct { 15 | left *leaf 16 | price float64 // Depth level price 17 | vol float64 // Depth level volume 18 | right *leaf 19 | } 20 | 21 | /* По каждой паре храним два стакана, текущие лучшие цены из каждого, 22 | объемы текущих лучших цен, пару и время последнего обновления 23 | */ 24 | type SymbolObj struct { 25 | BTree *leaf // Bid depth 26 | ATree *leaf // Ask depth 27 | CurrB float64 //Best bid price 28 | CurrA float64 // Best ask price 29 | 30 | CurrBVol float64 // Best bid position volume 31 | CurrAVol float64 //Best ask position volume 32 | 33 | Symbol symbol_struct // Pair 34 | LastUpdated string // Last update DT 35 | } 36 | 37 | /* 38 | Ссылка на потенциальную сделку 39 | */ 40 | type DealObj struct { 41 | S *SymbolObj // Pointer to SymbolObj - depth, rates etc. 42 | Type string // Deal type (buy, sell) 43 | MinCoins float64 // Min coins amount to buy|sell (calculates on demand) 44 | } 45 | 46 | /* 47 | Эта структура данных олицетворяет цепь сделок, прибыль по ним и прочее. 48 | Когда приходит время торговать, специальный указатель ссылается на экзмепляр такой структуры, и код создает/проверяет ордера в соответствии с ней. 49 | */ 50 | type ProfitObj struct { 51 | Chain [CHAIN_LEN]*DealObj // Цепь сделок, которые нужно провести 52 | Profit float64 // Потенциальная прибыль после выполнения сделок 53 | Description string // Сформированная строка, которая выводится в лог для отладки и информации 54 | Fits bool // Признак того, что объем каждой сделки в цепи содержит минимально нужный объем 55 | MultyFactor float64 // Если объем сделок выше нашего минимального, то увеличить объем покупок до нашего максимального 56 | Phase int32 // Какое звено chain сейчас исполняется 57 | OrderId int64 // Какой ордер нужно отслеживать (если есть) 58 | mx sync.Mutex // Для блокировки параллельного доступа 59 | } 60 | 61 | /* exchange info - структуры, репрезентующие exchangeInfo binance */ 62 | type limit_struct struct { 63 | RateLimitType string `json:rateLimitType` 64 | Interval string `json:interval` 65 | Limit int32 `json:limit` 66 | } 67 | 68 | type symbol_struct struct { 69 | Symbol string `json:symbol` 70 | Status string `json:status` 71 | BaseAsset string `json:baseAsset` 72 | BaseAssetPrecision int32 `json:baseAssetPrecision` 73 | QuoteAsset string `json:quoteAsset` 74 | QuotePrecision int32 `json:quotePrecision` 75 | OrderTypes []string `json:orderTypes` 76 | IcebergAllowed bool `json:icebergAllowed` 77 | Filters []interface{} `json:filters` 78 | } 79 | 80 | type exchangeInfo_struct struct { 81 | Timezone string `json:timezone` 82 | ServerTime int64 `json:serverTime` 83 | RateLimits []limit_struct `json:rateLimits` 84 | ExchangeFilters []string `json:exchangeFilters` 85 | Symbols []symbol_struct `json:symbols` 86 | } 87 | 88 | /* Для парсинга данных о стаканах из сокетов*/ 89 | type partial_depth struct { 90 | e string `json:"e"` 91 | E int64 `json:"E"` 92 | Symbol string `json:"s"` 93 | U int64 `json:"U"` 94 | Bids [][]string `json:"b"` 95 | Asks [][]string `json:"a"` 96 | } 97 | 98 | /* Для парсинга rest depth*/ 99 | type Depth struct { 100 | LastUpdateId int32 `json:"lastUpdateId"` 101 | Bids [][]string `json:"bids"` 102 | Asks [][]string `json:"asks"` 103 | } 104 | 105 | /* Получение ключа для подписки на user-data-stream */ 106 | type ListenKeyObj struct { 107 | ListenKey string `json:"listenKey"` 108 | } 109 | 110 | /* Информация об созданном ордере, полученная через rest*/ 111 | type OrderResult struct { 112 | Symbol string `json: "symbol"` 113 | OrderId int64 `json: "orderId"` 114 | ClientOrderId string `json: "clientOrderId"` 115 | TransactTime int64 `json:"transactTime"` 116 | Price string `json:"price"` 117 | OrigQty string `json:"origQty"` 118 | ExecutedQty string `json: "executedQty"` 119 | CummulativeQuoteQty string `json: "cummulativeQuoteQty"` 120 | Status string `json:"status"` 121 | TimeInForce string `json:"timeInForce"` 122 | Type string `json:"type"` 123 | Side string `json:"side"` 124 | } 125 | 126 | /* Информация об созданном ордере, полученная через сокеты*/ 127 | type ExecutionReport struct { 128 | eventType string `json:"e"` // Event type 129 | EventTime int64 `json:"E"` // Event time 130 | Symbol string `json:"s"` // Symbol 131 | ClientOrderId string `json:"c"` // Client order ID 132 | Side string `json:"S"` // Side 133 | OrderType string `json:"o"` // Order type 134 | TimeInForce string `json:"f"` // Time in force 135 | Quantity string `json: "q"` // Order quantity 136 | Price string `json: "p"` // Order price 137 | StopPrice string `json:"P"` // Stop price 138 | IcebergQuantity string `json:"F"` // Iceberg quantity 139 | Dummy int64 `json:"g"` // Ignore 140 | OriginalClientOrderId string `json:"C"` // Original client order ID; This is the ID of the order being canceled 141 | ExecutionType string `json:"x"` // Current execution type 142 | Status string `json:"X"` // Current order status 143 | RejectReason string `json: "r"` // Order reject reason; will be an error code. 144 | OrderId int64 `json:"i"` // Order ID 145 | LastExecutedQuantity string `json: "l"` // Last executed quantity 146 | CumulativeFilledQuantity string `json:"z"` // Cumulative filled quantity 147 | LastExecutedPrice string `json:"L"` // Last executed price 148 | ComissionAmount string `json:"n"` // Commission amount 149 | ComissionAsset string `json: "N"` // Commission asset 150 | TransactionTime int64 `json:"T"` // Transaction time 151 | TradeId int64 `json:"t"` // Trade ID 152 | Dummy2 int64 `json:"I"` // Ignore 153 | IsWorking bool `json: "w"` // Is the order working? Stops will have 154 | IsMaker bool `json:"m"` // Is this trade the maker side? 155 | Dummy3 bool `json:"M"` // Ignore 156 | OrderCreated int64 `json:"O"` // Order creation time 157 | CumulativeQuoteQuantity string `json:"Z"` // Cumulative quote asset transacted quantity 158 | LastQuoteQuantity string `json:"Y"` // Last quote asset transacted quantity (i.e. lastPrice * lastQty) 159 | } 160 | 161 | /* Внутренняя структура для хранения некоторых полей ордера */ 162 | type OrderInfo struct { 163 | OrderId int64 164 | Symbol string 165 | Side string 166 | SpentQty float64 167 | GotQty float64 168 | } 169 | -------------------------------------------------------------------------------- /ws.go: -------------------------------------------------------------------------------- 1 | /* 2 | Бот создан в рамках проекта Bablofil, подробнее тут 3 | https://bablofil.ru/vnutrenniy-arbitraj-chast-2/ 4 | или на форуме https://forum.bablofil.ru/ 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "encoding/json" 11 | "flag" 12 | "math" 13 | "net/url" 14 | "os" 15 | "os/signal" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "time" 20 | 21 | "github.com/gorilla/websocket" 22 | ) 23 | 24 | var addr = flag.String("addr", "stream.binance.com:9443", "http service address") 25 | 26 | /* 27 | Подписаться на обновления стаканов сразу по всем парам, обновления заносятся в деревья соответствующих объектов 28 | */ 29 | func ws_listener(wg *sync.WaitGroup) { 30 | defer wg.Done() 31 | 32 | flag.Parse() 33 | 34 | interrupt := make(chan os.Signal, 1) 35 | signal.Notify(interrupt, os.Interrupt) 36 | 37 | //pairs := "/ws/" 38 | pairs := []string{} 39 | for _, v := range exchangeInfo.Symbols { 40 | //pairs = pairs + strings.ToLower(v.Symbol) + "@depth/" 41 | pairs = append(pairs, strings.ToLower(v.Symbol)) 42 | } 43 | 44 | //u := url.URL{Scheme: "wss", Host: *addr, Path: pairs} 45 | u := url.URL{Scheme: "wss", Host: *addr, Path: "/ws/" + strings.Join(pairs, "/")} 46 | 47 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 48 | if err != nil { 49 | logger.Fatal("dial:", err) 50 | } 51 | defer c.Close() 52 | 53 | done := make(chan struct{}) 54 | 55 | go func() { 56 | 57 | defer close(done) 58 | for { 59 | _, message, err := c.ReadMessage() 60 | 61 | if err != nil { 62 | logger.Println("read:", err) 63 | break 64 | } 65 | 66 | //структура для хранения 67 | var depth partial_depth 68 | 69 | //парсим JSON в структуру 70 | err = json.Unmarshal(message, &depth) 71 | if err != nil { 72 | //logger.Print(err) 73 | } 74 | 75 | curr_s := symbols[depth.Symbol] 76 | 77 | //fill bids tree 78 | root := update_tree(depth.Bids, curr_s.BTree) 79 | if curr_s.BTree == nil { 80 | curr_s.BTree = root 81 | } 82 | curr_s.CurrB, curr_s.CurrBVol = get_highest_price(curr_s.BTree) 83 | 84 | //fill asks tree 85 | root = update_tree(depth.Asks, curr_s.ATree) 86 | if curr_s.ATree == nil { 87 | curr_s.ATree = root 88 | } 89 | curr_s.CurrA, curr_s.CurrAVol = get_lowest_price(curr_s.ATree) 90 | currentTime := time.Now() 91 | curr_s.LastUpdated = currentTime.Format("2006-01-02 15:04:05.000000000") 92 | 93 | } 94 | }() 95 | 96 | for { 97 | select { 98 | case <-done: 99 | return 100 | 101 | case <-interrupt: 102 | logger.Println("interrupt WS") 103 | 104 | return 105 | } 106 | } 107 | } 108 | 109 | /* 110 | Подписка на user-data-stream, когда на бирже происходит обновление пользовательских 111 | ордеров или иная информация по аккаунту, приходит обновление по сокету. 112 | Используем только то, что касается ордеров (executionReport), остальное отбрасываем 113 | Учитывается, что информация по ордеру могла быть получена через rest в момент создания, 114 | так же это мог быть последний ордер в цепочке, и другой процесс мог занулить указатель на текущую структуру, если получил информацию раньше. 115 | */ 116 | func orders_listener(wg *sync.WaitGroup) { 117 | defer wg.Done() 118 | 119 | flag.Parse() 120 | logger.SetFlags(-1) 121 | 122 | interrupt := make(chan os.Signal, 1) 123 | signal.Notify(interrupt, os.Interrupt) 124 | 125 | params := "/ws/" + listen_key 126 | 127 | u := url.URL{Scheme: "wss", Host: *addr, Path: params} 128 | 129 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 130 | if err != nil { 131 | logger.Fatal("dial:", err) 132 | } 133 | defer c.Close() 134 | 135 | done := make(chan struct{}) 136 | 137 | go func() { 138 | 139 | defer close(done) 140 | for { 141 | _, message, err := c.ReadMessage() 142 | 143 | if err != nil { 144 | logger.Println("read:", err) 145 | break 146 | } 147 | 148 | var execution_report ExecutionReport 149 | 150 | if strings.Contains(string(message), "executionReport") { 151 | logger.Printf("recv: %s", message) 152 | 153 | err = json.Unmarshal(message, &execution_report) 154 | if err != nil { 155 | //logger.Print(err) 156 | } 157 | 158 | logger.Println("Exec rep", execution_report.OrderId, execution_report.Symbol, execution_report.CumulativeFilledQuantity, execution_report.CumulativeQuoteQuantity, execution_report) 159 | 160 | if execution_report.Status == "FILLED" { 161 | if currentProfitObj != nil { 162 | currentProfitObj.mx.Lock() 163 | } 164 | if _, ok := orders[execution_report.OrderId]; !ok { 165 | 166 | spent, _ := strconv.ParseFloat(execution_report.CumulativeFilledQuantity, 32) 167 | executed, _ := strconv.ParseFloat(execution_report.CumulativeQuoteQuantity, 32) 168 | 169 | var order_info = OrderInfo{OrderId: execution_report.OrderId, Symbol: execution_report.Symbol, Side: execution_report.Side, SpentQty: spent, GotQty: executed} 170 | orders[execution_report.OrderId] = order_info 171 | 172 | logger.Println("OrderInfo", order_info) 173 | 174 | if currentProfitObj != nil && currentProfitObj.Phase+1 < CHAIN_LEN { 175 | if currentProfitObj != nil && currentProfitObj.Chain[currentProfitObj.Phase+1].Type == "SELL" { 176 | if currentProfitObj != nil { 177 | currentProfitObj.Chain[currentProfitObj.Phase+1].MinCoins = executed 178 | } 179 | } else { 180 | if currentProfitObj != nil { 181 | interf := currentProfitObj.Chain[currentProfitObj.Phase+1].S.Symbol.Filters[2] 182 | filter_map := interf.(map[string]interface{}) 183 | stepSize, _ := strconv.ParseFloat(filter_map["stepSize"].(string), 64) 184 | 185 | coins := executed / currentProfitObj.Chain[currentProfitObj.Phase+1].S.CurrA 186 | coins = math.Floor(coins*(1/stepSize)) / (1 / stepSize) 187 | currentProfitObj.Chain[currentProfitObj.Phase+1].MinCoins = coins 188 | } 189 | } 190 | } 191 | 192 | currentProfitObj.Phase += 1 193 | } 194 | if currentProfitObj != nil { 195 | currentProfitObj.mx.Unlock() 196 | } 197 | } 198 | 199 | } 200 | 201 | } 202 | }() 203 | 204 | for { 205 | select { 206 | case <-done: 207 | return 208 | 209 | case <-interrupt: 210 | logger.Println("interrupt OL") 211 | wg.Done() 212 | 213 | return 214 | } 215 | } 216 | } 217 | --------------------------------------------------------------------------------