Skip to content

Commit

Permalink
ISSUE-6: Master Bot and Swap configuration API (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreyMashukov authored Mar 5, 2024
1 parent ea339fb commit 5c3160d
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 21 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ init-db-dev:
mysql -u root -pgo_crypto_bot -h 127.0.0.1 -P 3367 -D go_crypto_bot < migrations/migration_11.sql
mysql -u root -pgo_crypto_bot -h 127.0.0.1 -P 3367 -D go_crypto_bot < migrations/migration_12.sql
mysql -u root -pgo_crypto_bot -h 127.0.0.1 -P 3367 -D go_crypto_bot < migrations/migration_13.sql
mysql -u root -pgo_crypto_bot -h 127.0.0.1 -P 3367 -D go_crypto_bot < migrations/migration_14.sql
mysql -u root -pgo_crypto_bot -h 127.0.0.1 -P 3367 -D go_crypto_bot < migrations/migration_15.sql
15 changes: 15 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ docker logs -f {container_id} # see logs
```

#### Using Bot API for setting up trading symbols (trade limits)
UPDATE BOT CONFIG
```bash
curl --location --request PUT 'http://localhost:8090/bot/update?botUuid={BOT_UUID}' \
--header 'Content-Type: application/json' \
--data-raw '{
"isMasterBot": true,
"isSwapEnabled": true
}'
```
**What is Master bot?**
> If you use multiple bots on one machine, you can set one of them as `master bot` - master bot is able to update some extra data which is static and can be used by others bots
**What is swap?**
> We call `SWAP` is triangular arbitrage, if `SWAP` is enabled, bot will try to do triangular arbitrage with negative profit positions (to gain coin amount)
CREATE YOUR FIRST TRADE LIMIT (Symbol) `PERPUSDT`
```bash
curl --location --request POST 'http://localhost:8090/trade/limit/create?botUuid={BOT_UUID}' \
Expand Down
2 changes: 2 additions & 0 deletions migrations/migration_15.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE bots ADD COLUMN is_master_bot TINYINT(1) DEFAULT '0';
ALTER TABLE bots ADD COLUMN is_swap_enabled TINYINT(1) DEFAULT '0';
16 changes: 10 additions & 6 deletions src/config/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,6 @@ func InitServiceContainer() Container {
AutoTradeHost: "https://api.autotrade.cloud",
}

isMasterBot := true
swapEnabled := true

orderRepository := repository.OrderRepository{
DB: db,
RDB: rdb,
Expand Down Expand Up @@ -204,6 +201,11 @@ func InitServiceContainer() Container {
Formatter: &formatter,
}

botService := service.BotService{
CurrentBot: currentBot,
BotRepository: &botRepository,
}

orderExecutor := exchange.OrderExecutor{
TradeStack: &tradeStack,
LossSecurity: &lossSecurity,
Expand All @@ -228,7 +230,7 @@ func InitServiceContainer() Container {
SwapValidator: &swapValidator,
Formatter: &formatter,
SwapSellOrderDays: swapOpenedSellOrderFromHoursOpened,
SwapEnabled: swapEnabled,
BotService: &botService,
SwapProfitPercent: swapOrderOnProfitPercent,
TurboSwapProfitPercent: 20.00,
Lock: make(map[string]bool),
Expand Down Expand Up @@ -337,6 +339,7 @@ func InitServiceContainer() Container {
botController := controller.BotController{
HealthService: &healthService,
CurrentBot: currentBot,
BotRepository: &botRepository,
}

return Container{
Expand Down Expand Up @@ -365,7 +368,7 @@ func InitServiceContainer() Container {
MarketDepthStrategy: &marketDepthStrategy,
OrderBasedStrategy: &orderBasedStrategy,
BaseKLineStrategy: &baseKLineStrategy,
IsMasterBot: isMasterBot,
IsMasterBot: botService.IsMasterBot(),
}
}

Expand Down Expand Up @@ -416,7 +419,8 @@ func (c *Container) StartHttpServer() {
http.HandleFunc("/trade/stack", c.TradeController.GetTradeStackAction)
http.HandleFunc("/trade/limit/create", c.TradeController.CreateTradeLimitAction)
http.HandleFunc("/trade/limit/update", c.TradeController.UpdateTradeLimitAction)
http.HandleFunc("/health/check", c.BotController.GetHealthCheck)
http.HandleFunc("/health/check", c.BotController.GetHealthCheckAction)
http.HandleFunc("/bot/update", c.BotController.PutConfigAction)

// Start HTTP server!
go func() {
Expand Down
46 changes: 45 additions & 1 deletion src/controller/bot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import (
"encoding/json"
"fmt"
"gitlab.com/open-soft/go-crypto-bot/src/model"
"gitlab.com/open-soft/go-crypto-bot/src/repository"
"gitlab.com/open-soft/go-crypto-bot/src/service"
"net/http"
)

type BotController struct {
HealthService *service.HealthService
CurrentBot *model.Bot
BotRepository *repository.BotRepository
}

func (b *BotController) GetHealthCheck(w http.ResponseWriter, req *http.Request) {
func (b *BotController) GetHealthCheckAction(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Content-Type", "application/json")
Expand All @@ -30,3 +32,45 @@ func (b *BotController) GetHealthCheck(w http.ResponseWriter, req *http.Request)
encoded, _ := json.Marshal(health)
fmt.Fprintf(w, string(encoded))
}

func (b *BotController) PutConfigAction(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Content-Type", "application/json")

botUuid := req.URL.Query().Get("botUuid")

if botUuid != b.CurrentBot.BotUuid {
http.Error(w, "Forbidden", http.StatusForbidden)

return
}

if req.Method != "PUT" {
http.Error(w, "Only PUT method is allowed", http.StatusMethodNotAllowed)

return
}

var botUpdate model.BotConfigUpdate

// Try to decode the request body into the struct. If there is an error,
// respond to the client with the error message and a 400 status code.
err := json.NewDecoder(req.Body).Decode(&botUpdate)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)

return
}

bot := b.BotRepository.GetCurrentBot()
bot.IsMasterBot = botUpdate.IsMasterBot
bot.IsSwapEnabled = botUpdate.IsSwapEnabled
err = b.BotRepository.Update(*bot)

if err != nil {
http.Error(w, "Couldn't update bot config.", http.StatusBadRequest)

return
}
}
11 changes: 9 additions & 2 deletions src/model/bot.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package model

type Bot struct {
Id int64 `json:"id"`
BotUuid string `json:"botUuid"`
Id int64 `json:"id"`
BotUuid string `json:"botUuid"`
IsMasterBot bool `json:"isMasterBot"`
IsSwapEnabled bool `json:"isSwapEnabled"`
}

type BotConfigUpdate struct {
IsMasterBot bool `json:"isMasterBot"`
IsSwapEnabled bool `json:"isSwapEnabled"`
}
77 changes: 76 additions & 1 deletion src/repository/bot_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package repository
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
"gitlab.com/open-soft/go-crypto-bot/src/model"
"log"
"os"
"time"
)

type BotRepository struct {
Expand All @@ -15,6 +18,46 @@ type BotRepository struct {
Ctx *context.Context
}

func (b *BotRepository) GetCurrentBotCached(botId int64) model.Bot {
botUuid := os.Getenv("BOT_UUID")

if len(botUuid) == 0 {
panic("'BOT_UUID' variable must be set!")
}

cacheKey := b.GetCacheKey(botUuid)
cachedBot := b.RDB.Get(*b.Ctx, cacheKey).Val()

if len(cachedBot) > 0 {
var bot model.Bot
err := json.Unmarshal([]byte(cachedBot), &bot)
if err == nil {
if bot.Id != botId {
panic(fmt.Sprintf("Bot ID is different! %d != %d", bot.Id, botId))
}

return bot
}
}

bot := b.GetCurrentBot()

if bot == nil {
panic("Current bot is not found!")
}

if bot.Id != botId {
panic(fmt.Sprintf("Bot ID is different! %d != %d", bot.Id, botId))
}

botEncoded, err := json.Marshal(bot)
if err == nil {
b.RDB.Set(*b.Ctx, cacheKey, string(botEncoded), time.Minute)
}

return *bot
}

func (b *BotRepository) GetCurrentBot() *model.Bot {
botUuid := os.Getenv("BOT_UUID")

Expand All @@ -27,12 +70,16 @@ func (b *BotRepository) GetCurrentBot() *model.Bot {
err := b.DB.QueryRow(`
SELECT
b.id as Id,
b.uuid as Uuid
b.uuid as Uuid,
b.is_master_bot as IsMasterBot,
b.is_swap_enabled as IsSwapEnabled
FROM bots b
WHERE b.uuid = ?`, botUuid,
).Scan(
&bot.Id,
&bot.BotUuid,
&bot.IsMasterBot,
&bot.IsSwapEnabled,
)

if err != nil {
Expand All @@ -53,3 +100,31 @@ func (b *BotRepository) Create(bot model.Bot) error {

return nil
}

func (b *BotRepository) Update(bot model.Bot) error {
_, err := b.DB.Exec(`
UPDATE bots b SET
b.is_swap_enabled = ?,
b.is_master_bot = ?
WHERE b.uuid = ? AND b.id = ?
`,
bot.IsSwapEnabled,
bot.IsMasterBot,
bot.BotUuid,
bot.Id,
)

if err != nil {
log.Println(err)
return err
}

// Invalidate cache
b.RDB.Del(*b.Ctx, b.GetCacheKey(bot.BotUuid))

return nil
}

func (b *BotRepository) GetCacheKey(botUuid string) string {
return fmt.Sprintf("bot-cached-%s", botUuid)
}
24 changes: 24 additions & 0 deletions src/service/bot_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package service

import (
"gitlab.com/open-soft/go-crypto-bot/src/model"
"gitlab.com/open-soft/go-crypto-bot/src/repository"
)

type BotServiceInterface interface {
IsSwapEnabled() bool
IsMasterBot() bool
}

type BotService struct {
CurrentBot *model.Bot
BotRepository *repository.BotRepository
}

func (b *BotService) IsSwapEnabled() bool {
return b.BotRepository.GetCurrentBotCached(b.CurrentBot.Id).IsSwapEnabled
}

func (b *BotService) IsMasterBot() bool {
return b.BotRepository.GetCurrentBotCached(b.CurrentBot.Id).IsMasterBot
}
10 changes: 5 additions & 5 deletions src/service/exchange/order_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type OrderExecutor struct {
CallbackManager service.CallbackManagerInterface
Formatter *utils.Formatter
SwapSellOrderDays int64
SwapEnabled bool
BotService service.BotServiceInterface
SwapProfitPercent float64
TurboSwapProfitPercent float64
Lock map[string]bool
Expand Down Expand Up @@ -409,11 +409,11 @@ func (m *OrderExecutor) Sell(tradeLimit model.TradeLimit, opened model.Order, sy
}

func (m *OrderExecutor) ProcessSwap(order model.Order) bool {
if m.SwapEnabled && order.IsSwap() {
if m.BotService.IsSwapEnabled() && order.IsSwap() {
log.Printf("[%s] Swap Order [%d] Mode: processing...", order.Symbol, order.Id)
m.SwapExecutor.Execute(order)
return true
} else if m.SwapEnabled {
} else if m.BotService.IsSwapEnabled() {
swapAction, err := m.SwapRepository.GetActiveSwapAction(order)
if err == nil && swapAction.OrderId == order.Id {
log.Printf("[%s] Swap Recovered for Order [%d] Mode: processing...", order.Symbol, order.Id)
Expand All @@ -427,7 +427,7 @@ func (m *OrderExecutor) ProcessSwap(order model.Order) bool {

func (m *OrderExecutor) TrySwap(order model.Order) {
swapChain := m.SwapRepository.GetSwapChainCache(order.GetBaseAsset())
if swapChain != nil && m.SwapEnabled {
if swapChain != nil && m.BotService.IsSwapEnabled() {
possibleSwaps := m.SwapRepository.GetSwapChains(order.GetBaseAsset())

if len(possibleSwaps) == 0 {
Expand Down Expand Up @@ -549,7 +549,7 @@ func (m *OrderExecutor) waitExecution(binanceOrder model.BinanceOrder, seconds i

openedBuyPosition, openedBuyPositionErr := m.OrderRepository.GetOpenedOrderCached(binanceOrder.Symbol, "BUY")

if kline != nil && binanceOrder.IsSell() && binanceOrder.IsNew() && m.SwapEnabled {
if kline != nil && binanceOrder.IsSell() && binanceOrder.IsNew() && m.BotService.IsSwapEnabled() {
// Try arbitrage for long orders >= 4 hours and with profit < -1.00%
if openedBuyPositionErr == nil {
swapChain := m.SwapRepository.GetSwapChainCache(openedBuyPosition.GetBaseAsset())
Expand Down
13 changes: 13 additions & 0 deletions tests/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,16 @@ func (p *ProfitServiceMock) GetMinProfitPercent(order model.ProfitPositionInterf
args := p.Called(order)
return args.Get(0).(model.Percent)
}

type BotServiceMock struct {
mock.Mock
}

func (b *BotServiceMock) IsSwapEnabled() bool {
args := b.Called()
return args.Get(0).(bool)
}
func (b *BotServiceMock) IsMasterBot() bool {
args := b.Called()
return args.Get(0).(bool)
}
Loading

0 comments on commit 5c3160d

Please sign in to comment.