diff --git a/pkg/adapter/controllers/tv_controller.go b/pkg/adapter/controllers/tv_controller.go index 4d7300a..330ba44 100644 --- a/pkg/adapter/controllers/tv_controller.go +++ b/pkg/adapter/controllers/tv_controller.go @@ -32,12 +32,13 @@ type TVController struct { Interactor usecase.TVInteractor } -func NewTVController(rwDB, roDB *gorm.DB) *TVController { +func NewTVController(rwDB, roDB *gorm.DB, httpClient *http.Client) *TVController { return &TVController{ Interactor: usecase.TVInteractor{ TVRepository: &gateway.TVRepository{ - RWDB: rwDB, - RODB: roDB, + RWDB: rwDB, + RODB: roDB, + HTTPClient: httpClient, }, }, } diff --git a/pkg/adapter/gateway/tv_repository.go b/pkg/adapter/gateway/tv_repository.go index f346cf4..729fadd 100644 --- a/pkg/adapter/gateway/tv_repository.go +++ b/pkg/adapter/gateway/tv_repository.go @@ -19,13 +19,17 @@ along with this program. If not, see . package gateway import ( + "encoding/json" "errors" "fmt" "github.com/frankrap/bybit-api/rest" "github.com/rluisr/tvbit-bot/pkg/domain" + "github.com/rluisr/tvbit-bot/pkg/external/bybit" "github.com/rluisr/tvbit-bot/utils" "gorm.io/gorm" + "io" "math" + "net/http" "strconv" "strings" "time" @@ -33,8 +37,9 @@ import ( type ( TVRepository struct { - RWDB *gorm.DB - RODB *gorm.DB + RWDB *gorm.DB + RODB *gorm.DB + HTTPClient *http.Client } ) @@ -69,52 +74,7 @@ func (r *TVRepository) SaveOrder(req domain.TV, order *rest.Order) error { return r.RWDB.Save(&orderHistory).Error } -// isOK: current time is between "start_time" and "stop_time" -func (r *TVRepository) isOK(req domain.TV) (bool, error) { - var setting domain.Setting - err := r.RODB.Where("api_key = ? and api_secret_key = ?", req.APIKey, req.APISecretKey).Take(&setting).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return true, nil - } - return false, err - } - - if !setting.StartTime.Valid || !setting.StopTime.Valid { - return true, nil - } - - utc := utils.UTC() - now := time.Now().In(utc) - - hour := now.Hour() - minute := now.Minute() - n := fmt.Sprintf("%d%d", hour, minute) - currentTime, err := strconv.Atoi(n) - if err != nil { - return false, err - } - - userStartTimeStr := strings.Replace(setting.StartTime.String, ":", "", 1) - userStopTimeStr := strings.Replace(setting.StopTime.String, ":", "", 1) - - userStartTime, err := strconv.Atoi(userStartTimeStr) - if err != nil { - return false, err - } - userStopTime, err := strconv.Atoi(userStopTimeStr) - if err != nil { - return false, err - } - - if userStartTime < currentTime && currentTime < userStopTime { - return true, nil - } - - return false, nil -} - -func (r *TVRepository) CalculateTPSL(req domain.TV, bybitClient *rest.ByBit, value interface{}, isType string) (float64, error) { +func (r *TVRepository) CalculateTPSL(req domain.TV, value interface{}, isType string) (float64, error) { var err error str, isOK := value.(string) @@ -126,7 +86,7 @@ func (r *TVRepository) CalculateTPSL(req domain.TV, bybitClient *rest.ByBit, val return 0, nil } - currentPrice, err := r.getCurrentPrice(req.Order.Symbol, bybitClient) + currentPrice, err := r.getCurrentPrice(req.Order.Symbol) if err != nil { return 0, err } @@ -169,6 +129,9 @@ func (r *TVRepository) CalculateTPSL(req domain.TV, bybitClient *rest.ByBit, val if err != nil { return 0, fmt.Errorf("convert string to int err %w", err) } + if req.Order.Side == "Sell" { + return currentPrice - float64(inputPrice), nil + } return currentPrice + float64(inputPrice), nil } if strings.Contains(str, "-") { @@ -177,6 +140,9 @@ func (r *TVRepository) CalculateTPSL(req domain.TV, bybitClient *rest.ByBit, val if err != nil { return 0, fmt.Errorf("convert string to int err %w", err) } + if req.Order.Side == "Sell" { + return currentPrice + float64(inputPrice), nil + } return currentPrice - float64(inputPrice), nil } @@ -189,16 +155,78 @@ func (r *TVRepository) CalculateTPSL(req domain.TV, bybitClient *rest.ByBit, val } -// getCurrentPrice returns close price one minute ago -func (r *TVRepository) getCurrentPrice(symbol string, bybitClient *rest.ByBit) (float64, error) { - _, _, resp, err := bybitClient.LinearGetKLine(symbol, "1", time.Now().Unix()-60, 1) +// isOK: current time is between "start_time" and "stop_time" +func (r *TVRepository) isOK(req domain.TV) (bool, error) { + var setting domain.Setting + err := r.RODB.Where("api_key = ? and api_secret_key = ?", req.APIKey, req.APISecretKey).Take(&setting).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, nil + } + return false, err + } + + if !setting.StartTime.Valid || !setting.StopTime.Valid { + return true, nil + } + + utc := utils.UTC() + now := time.Now().In(utc) + + hour := now.Hour() + minute := now.Minute() + n := fmt.Sprintf("%d%d", hour, minute) + currentTime, err := strconv.Atoi(n) + if err != nil { + return false, err + } + + userStartTimeStr := strings.Replace(setting.StartTime.String, ":", "", 1) + userStopTimeStr := strings.Replace(setting.StopTime.String, ":", "", 1) + + userStartTime, err := strconv.Atoi(userStartTimeStr) + if err != nil { + return false, err + } + userStopTime, err := strconv.Atoi(userStopTimeStr) + if err != nil { + return false, err + } + + if userStartTime < currentTime && currentTime < userStopTime { + return true, nil + } + + return false, nil +} + +// getCurrentPrice returns mark price +func (r *TVRepository) getCurrentPrice(symbol string) (float64, error) { + tickersURL := fmt.Sprintf("%sderivatives/v3/public/tickers?category=linear&symbol=%s", bybit.BaseURL, symbol) + + req, _ := http.NewRequest(http.MethodGet, tickersURL, http.NoBody) + resp, err := r.HTTPClient.Do(req) if err != nil { - return 0, fmt.Errorf("failed to get current price err: %w", err) + return 0, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err } - if len(resp) == 0 { - return 0, fmt.Errorf("failed to get current price. invalid query") + var ticker domain.Ticker + err = json.Unmarshal(b, &ticker) + if err != nil { + return 0, err + } + + markPriceStr := ticker.Result.List[0].MarkPrice + markPrice, err := strconv.ParseFloat(markPriceStr, 64) + if err != nil { + return 0, err } - return resp[0].Close, nil + return markPrice, nil } diff --git a/pkg/domain/bybit.go b/pkg/domain/bybit.go new file mode 100644 index 0000000..6df936d --- /dev/null +++ b/pkg/domain/bybit.go @@ -0,0 +1,34 @@ +package domain + +type Ticker struct { + RetCode int `json:"retCode"` + RetMsg string `json:"retMsg"` + Result struct { + Category string `json:"category"` + List []struct { + Symbol string `json:"symbol"` + BidPrice string `json:"bidPrice"` + AskPrice string `json:"askPrice"` + LastPrice string `json:"lastPrice"` + LastTickDirection string `json:"lastTickDirection"` + PrevPrice24H string `json:"prevPrice24h"` + Price24HPcnt string `json:"price24hPcnt"` + HighPrice24H string `json:"highPrice24h"` + LowPrice24H string `json:"lowPrice24h"` + PrevPrice1H string `json:"prevPrice1h"` + MarkPrice string `json:"markPrice"` + IndexPrice string `json:"indexPrice"` + OpenInterest string `json:"openInterest"` + Turnover24H string `json:"turnover24h"` + Volume24H string `json:"volume24h"` + FundingRate string `json:"fundingRate"` + NextFundingTime string `json:"nextFundingTime"` + PredictedDeliveryPrice string `json:"predictedDeliveryPrice"` + BasisRate string `json:"basisRate"` + DeliveryFeeRate string `json:"deliveryFeeRate"` + DeliveryTime string `json:"deliveryTime"` + } `json:"list"` + } `json:"result"` + RetExtInfo interface{} `json:"retExtInfo"` + Time int64 `json:"time"` +} diff --git a/pkg/external/bybit/bybit.go b/pkg/external/bybit/bybit.go index c15e14b..bf5a6a9 100644 --- a/pkg/external/bybit/bybit.go +++ b/pkg/external/bybit/bybit.go @@ -23,11 +23,12 @@ import ( "github.com/rluisr/tvbit-bot/pkg/domain" ) +var BaseURL = "https://api.bybit.com/" + func Init(req domain.TV) *rest.ByBit { - baseURL := "https://api.bybit.com/" if req.IsTestNet { - baseURL = "https://api-testnet.bybit.com/" + BaseURL = "https://api-testnet.bybit.com/" } - return rest.New(nil, baseURL, req.APIKey, req.APISecretKey, false) + return rest.New(nil, BaseURL, req.APIKey, req.APISecretKey, false) } diff --git a/pkg/external/http.go b/pkg/external/http.go new file mode 100644 index 0000000..b81cd80 --- /dev/null +++ b/pkg/external/http.go @@ -0,0 +1,25 @@ +package external + +import ( + "net" + "net/http" + "time" +) + +func NewHTTPClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 128, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + Timeout: 60 * time.Second, + } +} diff --git a/pkg/external/router.go b/pkg/external/router.go index c82bb7a..3b3f194 100644 --- a/pkg/external/router.go +++ b/pkg/external/router.go @@ -42,7 +42,8 @@ func init() { Router = gin.Default() Router.ForwardedByClientIP = true - tvController := controllers.NewTVController(rwDB, roDB) + httpClient := NewHTTPClient() + tvController := controllers.NewTVController(rwDB, roDB, httpClient) settingController := controllers.NewSettingController(rwDB, roDB) Router.GET("/", func(c *gin.Context) { diff --git a/pkg/usecase/interfaces/repositories.go b/pkg/usecase/interfaces/repositories.go index 83069b5..1fec49f 100644 --- a/pkg/usecase/interfaces/repositories.go +++ b/pkg/usecase/interfaces/repositories.go @@ -26,7 +26,7 @@ import ( type TVRepository interface { CreateOrder(domain.TV, *rest.ByBit) (*rest.Order, error) SaveOrder(domain.TV, *rest.Order) error - CalculateTPSL(domain.TV, *rest.ByBit, interface{}, string) (float64, error) + CalculateTPSL(domain.TV, interface{}, string) (float64, error) } type SettingRepository interface { diff --git a/pkg/usecase/tv_interactor.go b/pkg/usecase/tv_interactor.go index 1424e16..eb448f3 100644 --- a/pkg/usecase/tv_interactor.go +++ b/pkg/usecase/tv_interactor.go @@ -32,7 +32,7 @@ type TVInteractor struct { func (i *TVInteractor) CreateOrder(req domain.TV, bybitClient *rest.ByBit) (domain.TVOrderResponse, error) { var err error - req.Order.TP, err = i.TVRepository.CalculateTPSL(req, bybitClient, req.Order.TP, "TP") + req.Order.TP, err = i.TVRepository.CalculateTPSL(req, req.Order.TP, "TP") if err != nil { return domain.TVOrderResponse{ Success: false, @@ -41,7 +41,7 @@ func (i *TVInteractor) CreateOrder(req domain.TV, bybitClient *rest.ByBit) (doma }, err } - req.Order.SL, err = i.TVRepository.CalculateTPSL(req, bybitClient, req.Order.SL, "SL") + req.Order.SL, err = i.TVRepository.CalculateTPSL(req, req.Order.SL, "SL") if err != nil { return domain.TVOrderResponse{ Success: false,