diff --git a/README.md b/README.md
index 9d71af6..85e9a12 100644
--- a/README.md
+++ b/README.md
@@ -142,9 +142,10 @@ Twitter [@rarirureluis](https://twitter.com/rarirureluis)
TODO
-----
-- [x] [core/bybit] USDC/Perp
-- [x] [core/bybit] USDT/Deriv
-- [x] [cron/wallet/bybit] USDC
-- [ ] [cron/wallet/bybit] Spot
-- [ ] [cron/wallet/bybit] Futures
+**There are no plans to support bybit/options/spot trading and other DEX.**
+
+- [x] [bybit/core] Futures
+- [x] [bybit/core] USDC
+- [x] [bybit/wallet] Deriv
+- [x] [bybit/wallet] USDC
- [ ] [core/binance]
diff --git a/pkg/adapter/gateway/bybit_repository.go b/pkg/adapter/gateway/bybit_repository.go
index 2d0dc3f..4128a0f 100644
--- a/pkg/adapter/gateway/bybit_repository.go
+++ b/pkg/adapter/gateway/bybit_repository.go
@@ -10,6 +10,8 @@ import (
"strings"
"time"
+ "github.com/shopspring/decimal"
+
"github.com/frankrap/bybit-api/rest"
"github.com/rluisr/tvbit-bot/pkg/domain"
"github.com/rluisr/tvbit-bot/pkg/external/bybit"
@@ -31,89 +33,56 @@ func (r *BybitRepository) Set(req domain.TV) {
r.APISecretKey = req.APISecretKey
}
-func (r *BybitRepository) CreateOrder(req domain.TV) (*domain.TVOrder, error) {
- orderHistory := &domain.TVOrder{
- Type: req.Order.Type,
- Symbol: req.Order.Symbol,
- Side: req.Order.Side,
- QTY: req.Order.QTY,
- TP: req.Order.TP,
- SL: req.Order.SL,
- }
-
- if !strings.Contains(req.Order.Symbol, "PERP") {
- _, _, order, err := r.Client.LinearCreateOrder(req.Order.Side, req.Order.Type, req.Order.Price, req.Order.QTY, "ImmediateOrCancel", req.Order.TP.(float64), req.Order.SL.(float64), false, false, "", req.Order.Symbol)
+func (r *BybitRepository) CreateOrder(req domain.TV) (string, error) {
+ var orderID string
+
+ if strings.Contains(req.Order.Symbol, "PERP") {
+ params := map[string]interface{}{}
+ params["symbol"] = req.Order.Symbol
+ params["orderType"] = req.Order.Type
+ params["orderFilter"] = "Order"
+ params["orderQty"] = req.Order.QTY
+ params["side"] = req.Order.Side
+ params["timeInForce"] = "ImmediateOrCancel"
+ params["takeProfit"] = strconv.FormatFloat(req.Order.TP.(float64), 'f', -1, 64)
+ params["stopLoss"] = strconv.FormatFloat(req.Order.SL.(float64), 'f', -1, 64)
+
+ var order domain.BybitPerpResponseOrder
+ _, resp, err := r.Client.SignedRequest(http.MethodPost, "perpetual/usdc/openapi/private/v1/place-order", params, &order)
if err != nil {
- return nil, err
+ return "", fmt.Errorf("SignedRequest err: %w, body: %s", err, string(resp))
}
-
- entryPrice, err := order.Price.Float64()
+ orderID = order.Result.OrderID
+ } else {
+ _, _, order, err := r.Client.LinearCreateOrder(req.Order.Side, req.Order.Type, req.Order.Price, req.Order.QTY, "ImmediateOrCancel", req.Order.TP.(float64), req.Order.SL.(float64), false, false, "", req.Order.Symbol)
if err != nil {
- return nil, err
+ return "", err
}
- orderHistory.OrderID = order.OrderId
- orderHistory.Price = entryPrice
- return orderHistory, nil
- }
-
- params := map[string]interface{}{}
- params["symbol"] = req.Order.Symbol
- params["orderType"] = req.Order.Type
- params["orderFilter"] = "Order"
- params["orderQty"] = req.Order.QTY
- params["side"] = req.Order.Side
- params["timeInForce"] = "ImmediateOrCancel"
- params["takeProfit"] = strconv.FormatFloat(req.Order.TP.(float64), 'f', -1, 64)
- params["stopLoss"] = strconv.FormatFloat(req.Order.SL.(float64), 'f', -1, 64)
-
- var order domain.BybitPerpResponseOrder
- _, resp, err := r.Client.SignedRequest(http.MethodPost, "perpetual/usdc/openapi/private/v1/place-order", params, &order)
- if err != nil {
- return nil, fmt.Errorf("SignedRequest err: %w, body: %s", err, string(resp))
- }
-
- orderPrice, err := strconv.ParseFloat(order.Result.OrderPrice, 64)
- if err != nil {
- // bybit returns fucking error response sometimes but order is success.
- return nil, err
+ orderID = order.OrderId
}
- orderHistory.OrderID = order.Result.OrderID
- orderHistory.Price = orderPrice
- return orderHistory, nil
+ return orderID, nil
}
-// GetCurrentPrice returns mark price
-func (r *BybitRepository) GetCurrentPrice(symbol string) (float64, error) {
- var isPerp bool
-
- var tickersURL string
- if strings.Contains(symbol, "PERP") {
- isPerp = true
- tickersURL = fmt.Sprintf("perpetual/usdc/openapi/public/v1/tick?symbol=%s", symbol)
- } else {
- tickersURL = fmt.Sprintf("derivatives/v3/public/tickers?category=linear&symbol=%s", symbol)
+// FetchOrder is set entry price
+func (r *BybitRepository) FetchOrder(req *domain.TV, orderID string) error {
+ req.Order = domain.TVOrder{
+ OrderID: orderID,
+ Type: req.Order.Type,
+ Symbol: req.Order.Symbol,
+ Side: req.Order.Side,
+ QTY: req.Order.QTY,
+ TP: req.Order.TP,
+ SL: req.Order.SL,
}
- var markPriceStr string
-
- if !isPerp {
- var ticker domain.BybitDerivTicker
- _, resp, err := r.Client.PublicRequest(http.MethodGet, tickersURL, nil, &ticker)
- if err != nil {
- return 0, fmt.Errorf("PublicRequest err: %w, body: %s", err, string(resp))
- }
- markPriceStr = ticker.Result.List[0].MarkPrice
- } else {
- var ticker domain.BybitPerpTicker
- _, resp, err := r.Client.PublicRequest(http.MethodGet, tickersURL, nil, &ticker)
- if err != nil {
- return 0, fmt.Errorf("PublicRequest err: %w, body: %s", err, string(resp))
- }
- markPriceStr = ticker.Result.MarkPrice
+ entryPrice, err := r.getEntryPrice(req.Order.Symbol, req.Order.Side)
+ if err != nil {
+ return err
}
+ req.Order.EntryPrice = *entryPrice
- return strconv.ParseFloat(markPriceStr, 64)
+ return nil
}
func (r *BybitRepository) CalculateTPSL(req domain.TV, value interface{}, isType string) (float64, error) {
@@ -128,7 +97,7 @@ func (r *BybitRepository) CalculateTPSL(req domain.TV, value interface{}, isType
return 0, nil
}
- currentPrice, err := r.GetCurrentPrice(req.Order.Symbol)
+ currentPrice, err := r.getMarkPrice(req.Order.Symbol)
if err != nil {
return 0, err
}
@@ -196,22 +165,107 @@ func (r *BybitRepository) CalculateTPSL(req domain.TV, value interface{}, isType
return f64, nil
}
+// getMarkPrice returns mark price
+func (r *BybitRepository) getMarkPrice(symbol string) (float64, error) {
+ var isPerp bool
+
+ var tickersURL string
+ if strings.Contains(symbol, "PERP") {
+ isPerp = true
+ tickersURL = fmt.Sprintf("perpetual/usdc/openapi/public/v1/tick?symbol=%s", symbol)
+ } else {
+ tickersURL = fmt.Sprintf("derivatives/v3/public/tickers?category=linear&symbol=%s", symbol)
+ }
+
+ var markPriceStr string
+
+ if !isPerp {
+ var ticker domain.BybitDerivTicker
+ _, resp, err := r.Client.PublicRequest(http.MethodGet, tickersURL, nil, &ticker)
+ if err != nil {
+ return 0, fmt.Errorf("PublicRequest err: %w, body: %s", err, string(resp))
+ }
+ markPriceStr = ticker.Result.List[0].MarkPrice
+ } else {
+ var ticker domain.BybitPerpTicker
+ _, resp, err := r.Client.PublicRequest(http.MethodGet, tickersURL, nil, &ticker)
+ if err != nil {
+ return 0, fmt.Errorf("PublicRequest err: %w, body: %s", err, string(resp))
+ }
+ markPriceStr = ticker.Result.MarkPrice
+ }
+
+ return strconv.ParseFloat(markPriceStr, 64)
+}
+
+func (r *BybitRepository) getEntryPrice(symbol, side string) (*decimal.Decimal, error) {
+ var entryPrice decimal.Decimal
+ var err error
+
+ if strings.Contains(symbol, "PERP") {
+ var positions domain.BybitUSDCPositions
+
+ resp, err := r.signedRequestWithHeader(http.MethodPost, "option/usdc/openapi/private/v1/query-position", []byte("{\"category\":\"PERPETUAL\"}"), &positions)
+ if err != nil {
+ return nil, fmt.Errorf("signedRequest err: %w, body: %s", err, resp)
+ }
+
+ for _, position := range positions.Result.DataList {
+ fmt.Printf("%+v\n", position)
+ entryPrice, err = decimal.NewFromString(position.EntryPrice)
+ if err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ _, _, positions, err := r.Client.LinearGetPosition(symbol)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, position := range positions {
+ if position.Side == side {
+ entryPrice = decimal.NewFromFloat(position.EntryPrice)
+ }
+ }
+ }
+
+ return &entryPrice, err
+}
+
func (r *BybitRepository) GetWalletInfoUSDC() (*domain.BybitWallet, error) {
var wallet domain.BybitWallet
- resp, err := r.signedRequestWithHeaderWithoutBody(http.MethodPost, "option/usdc/openapi/private/v1/query-wallet-balance", &wallet)
+ resp, err := r.signedRequestWithHeader(http.MethodPost, "option/usdc/openapi/private/v1/query-wallet-balance", []byte("{}"), &wallet)
if err != nil {
- return nil, fmt.Errorf("signedRequestWithHeaderWithoutBody err: %w, body: %s", err, string(resp))
+ return nil, fmt.Errorf("signedRequest err: %w, body: %s", err, resp)
}
return &wallet, nil
}
-func (r *BybitRepository) signedRequestWithHeaderWithoutBody(method, path string, result interface{}) (string, error) {
+func (r *BybitRepository) GetWalletInfoDeriv() (*rest.Balance, error) {
+ _, _, wallet, err := r.Client.GetWalletBalance("USDT")
+ if err != nil {
+ return nil, err
+ }
+
+ return &wallet, nil
+}
+
+func (r *BybitRepository) signedRequestWithHeader(method, path string, body []byte, result interface{}) (string, error) {
+ var payload io.Reader
+
nowTimeInMilli := strconv.FormatInt(time.Now().UnixMilli(), 10)
- var hmacSigned []byte
- payload := strings.NewReader(`{}`)
- signInput := nowTimeInMilli + r.APIKey + "5000" + "{}"
+
+ if body == nil {
+ // TODO have to using query params. it's not working when query params are passed.
+ payload = strings.NewReader("")
+ } else {
+ payload = strings.NewReader(string(body))
+ }
+
+ signInput := nowTimeInMilli + r.APIKey + "5000" + string(body)
hmacSigned, err := crypto.GetHMAC(crypto.HashSHA256, []byte(signInput), []byte(r.APISecretKey))
if err != nil {
return "", err
diff --git a/pkg/domain/bybit.go b/pkg/domain/bybit.go
index 5d5122b..bca36ca 100644
--- a/pkg/domain/bybit.go
+++ b/pkg/domain/bybit.go
@@ -96,6 +96,44 @@ type BybitPerpTicker struct {
} `json:"result"`
}
+type BybitUSDCPositions struct {
+ Result struct {
+ Cursor string `json:"cursor"`
+ ResultTotalSize int `json:"resultTotalSize"`
+ DataList []struct {
+ Symbol string `json:"symbol"`
+ Leverage string `json:"leverage"`
+ OccClosingFee string `json:"occClosingFee"`
+ LiqPrice string `json:"liqPrice"`
+ PositionValue string `json:"positionValue"`
+ TakeProfit string `json:"takeProfit"`
+ RiskID string `json:"riskId"`
+ TrailingStop string `json:"trailingStop"`
+ UnrealisedPnl string `json:"unrealisedPnl"`
+ CreatedAt string `json:"createdAt"`
+ MarkPrice string `json:"markPrice"`
+ CumRealisedPnl string `json:"cumRealisedPnl"`
+ PositionMM string `json:"positionMM"`
+ PositionIM string `json:"positionIM"`
+ UpdatedAt string `json:"updatedAt"`
+ TpSLMode string `json:"tpSLMode"`
+ Side string `json:"side"`
+ BustPrice string `json:"bustPrice"`
+ DeleverageIndicator int `json:"deleverageIndicator"`
+ EntryPrice string `json:"entryPrice"`
+ Size string `json:"size"`
+ SessionRPL string `json:"sessionRPL"`
+ PositionStatus string `json:"positionStatus"`
+ SessionUPL string `json:"sessionUPL"`
+ StopLoss string `json:"stopLoss"`
+ OrderMargin string `json:"orderMargin"`
+ SessionAvgPrice string `json:"sessionAvgPrice"`
+ } `json:"dataList"`
+ } `json:"result"`
+ RetCode int `json:"retCode"`
+ RetMsg string `json:"retMsg"`
+}
+
type BybitWallet struct {
Result struct {
WalletBalance string `json:"walletBalance"`
diff --git a/pkg/domain/tv.go b/pkg/domain/tv.go
index b397834..d7e7205 100644
--- a/pkg/domain/tv.go
+++ b/pkg/domain/tv.go
@@ -19,6 +19,7 @@ along with this program. If not, see .
package domain
import (
+ "github.com/shopspring/decimal"
"gorm.io/gorm"
)
@@ -31,16 +32,17 @@ type TV struct {
type TVOrder struct {
gorm.Model
- DEX string `gorm:"type:varchar(255);not null" json:"-"`
- SettingID uint64 `gorm:"type:uint;not null" json:"-"`
- OrderID string `gorm:"type:varchar(255);uniqueIndex:order_id;not null"`
- Type string `gorm:"type:varchar(255)" json:"type" binding:"required"` // "Market" or "Limit"
- Symbol string `gorm:"type:varchar(255)" json:"symbol" binding:"required"` // eg: BTCUSDT
- Side string `gorm:"type:varchar(255)" json:"side" binding:"required"` // "Buy" or "Sell"
- Price float64 `gorm:"type:float" json:"price"` // Set 0 if order_type is Market
- QTY float64 `gorm:"type:float" json:"qty" binding:"required"`
- TP interface{} `gorm:"type:float" json:"tp"`
- SL interface{} `gorm:"type:float" json:"sl"`
+ DEX string `gorm:"type:varchar(255);not null" json:"-"`
+ SettingID uint64 `gorm:"type:uint;not null" json:"-"`
+ OrderID string `gorm:"type:varchar(255);uniqueIndex:order_id;not null"`
+ Type string `gorm:"type:varchar(255)" json:"type" binding:"required"` // "Market" or "Limit"
+ Symbol string `gorm:"type:varchar(255)" json:"symbol" binding:"required"` // eg: BTCUSDT
+ Side string `gorm:"type:varchar(255)" json:"side" binding:"required"` // "Buy" or "Sell"
+ Price float64 `gorm:"-" json:"price"` // Set 0 if order_type is Market
+ EntryPrice decimal.Decimal `gorm:"type:decimal(10,4)" json:"-"`
+ QTY float64 `gorm:"type:float" json:"qty" binding:"required"`
+ TP interface{} `gorm:"type:float" json:"tp"`
+ SL interface{} `gorm:"type:float" json:"sl"`
}
type Tabler interface {
diff --git a/pkg/external/cron.go b/pkg/external/cron.go
index 0bf068e..2aba1c0 100644
--- a/pkg/external/cron.go
+++ b/pkg/external/cron.go
@@ -35,7 +35,7 @@ func cron() {
Verbose: true,
})
- task.Task("0 * * * *", func(ctx context.Context) (int, error) {
+ task.Task("* * * * *", func(ctx context.Context) (int, error) {
settings, err := tvController.Interactor.TVRepository.GetSettings()
if err != nil {
return 0, err
@@ -53,16 +53,16 @@ func cron() {
})
// USDC
- bybitWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoUSDC()
+ bybitUSDCWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoUSDC()
if err != nil {
return 1, err
}
- balance, err := decimal.NewFromString(bybitWallet.Result.WalletBalance)
+ balance, err := decimal.NewFromString(bybitUSDCWallet.Result.WalletBalance)
if err != nil {
return 1, err
}
- totalRPL, err := decimal.NewFromString(bybitWallet.Result.TotalRPL)
+ totalRPL, err := decimal.NewFromString(bybitUSDCWallet.Result.TotalRPL)
if err != nil {
return 1, err
}
@@ -73,8 +73,22 @@ func cron() {
Balance: balance,
TotalRPL: totalRPL,
})
- // TODO spot
- // TODO futures
+
+ // Deriv USDT
+ bybitDerivWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoDeriv()
+ if err != nil {
+ return 1, err
+ }
+
+ balance = decimal.NewFromFloat(bybitDerivWallet.Equity)
+ totalRPL = decimal.NewFromFloat(bybitDerivWallet.CumRealisedPnl)
+
+ walletHistories = append(walletHistories, domain.WalletHistory{
+ SettingID: setting.ID,
+ Type: "usdt",
+ Balance: balance,
+ TotalRPL: totalRPL,
+ })
}
}
diff --git a/pkg/usecase/interfaces/repositories.go b/pkg/usecase/interfaces/repositories.go
index a5fac7b..bbd1955 100644
--- a/pkg/usecase/interfaces/repositories.go
+++ b/pkg/usecase/interfaces/repositories.go
@@ -19,6 +19,7 @@ along with this program. If not, see .
package interfaces
import (
+ "github.com/frankrap/bybit-api/rest"
"github.com/rluisr/tvbit-bot/pkg/domain"
)
@@ -31,10 +32,11 @@ type TVRepository interface {
type BybitRepository interface {
Set(domain.TV)
- CreateOrder(domain.TV) (*domain.TVOrder, error)
- GetCurrentPrice(string) (float64, error)
+ CreateOrder(domain.TV) (string, error)
CalculateTPSL(domain.TV, interface{}, string) (float64, error)
+ FetchOrder(req *domain.TV, orderID string) error
GetWalletInfoUSDC() (*domain.BybitWallet, error)
+ GetWalletInfoDeriv() (*rest.Balance, error)
}
type SettingRepository interface {
diff --git a/pkg/usecase/tv_interactor.go b/pkg/usecase/tv_interactor.go
index 7cbe49b..f5b79a0 100644
--- a/pkg/usecase/tv_interactor.go
+++ b/pkg/usecase/tv_interactor.go
@@ -19,7 +19,7 @@ along with this program. If not, see .
package usecase
import (
- "log"
+ "time"
"github.com/rluisr/tvbit-bot/pkg/domain"
"github.com/rluisr/tvbit-bot/pkg/usecase/interfaces"
@@ -67,7 +67,7 @@ func (i *TVInteractor) CreateOrder(req domain.TV) (domain.TVOrderResponse, error
}, nil
}
- order, err := i.BybitRepository.CreateOrder(req)
+ orderID, err := i.BybitRepository.CreateOrder(req)
if err != nil {
return domain.TVOrderResponse{
Success: false,
@@ -76,15 +76,26 @@ func (i *TVInteractor) CreateOrder(req domain.TV) (domain.TVOrderResponse, error
}, err
}
- if order == nil {
- return domain.TVOrderResponse{Success: true, Reason: "order is cancelled by \"start_time\", \"stop_time\" setting", Order: nil}, nil
+ // Wait for ByBit server side reflection
+ time.Sleep(5 * time.Second)
+
+ err = i.BybitRepository.FetchOrder(&req, orderID)
+ if err != nil {
+ return domain.TVOrderResponse{
+ Success: false,
+ Reason: err.Error(),
+ Order: &req.Order,
+ }, err
}
- err = i.TVRepository.SaveOrder(req, order)
+ err = i.TVRepository.SaveOrder(req, &req.Order)
if err != nil {
- // order is created, do not return err here.
- log.Printf("a order is created but failed to save order history to MySQL err: %s\n", err.Error())
+ return domain.TVOrderResponse{
+ Success: false,
+ Reason: err.Error(),
+ Order: &req.Order,
+ }, err
}
- return domain.TVOrderResponse{Success: true, Order: order}, nil
+ return domain.TVOrderResponse{Success: true, Order: &req.Order}, nil
}