From cd89b8425a28a9cf22eb5b85372c8163f8473595 Mon Sep 17 00:00:00 2001 From: Hasegawa Takuya Date: Thu, 29 Sep 2022 01:39:14 +0900 Subject: [PATCH] Fetch closed PnL --- pkg/adapter/gateway/bybit_repository.go | 15 +++ pkg/adapter/gateway/tv_repository.go | 10 ++ pkg/domain/bybit.go | 36 +++++++ pkg/domain/tv.go | 1 + pkg/external/cron.go | 124 +++++++++++++++++------- pkg/usecase/interfaces/repositories.go | 3 + 6 files changed, 153 insertions(+), 36 deletions(-) diff --git a/pkg/adapter/gateway/bybit_repository.go b/pkg/adapter/gateway/bybit_repository.go index 34fca78..6d23d84 100644 --- a/pkg/adapter/gateway/bybit_repository.go +++ b/pkg/adapter/gateway/bybit_repository.go @@ -105,6 +105,21 @@ func (r *BybitRepository) FetchOrder(req *domain.TV, orderID string) error { return nil } +func (r *BybitRepository) GetClosedOrderLast(symbol string) (*domain.BybitLinearClosedPnLResponse, error) { + params := map[string]interface{}{} + params["symbol"] = symbol + params["exec_type"] = "Trade" + params["limit"] = 1 + + var result domain.BybitLinearClosedPnLResponse + _, resp, err := r.Client.SignedRequest(http.MethodGet, "private/linear/trade/closed-pnl/list", params, &result) + if err != nil { + return nil, fmt.Errorf("SignedRequest err: %w, body: %s", err, string(resp)) + } + + return &result, nil +} + func (r *BybitRepository) GetActiveOrderCount(req *domain.TV, positions *[]rest.LinearPosition) int { var activeOrder int diff --git a/pkg/adapter/gateway/tv_repository.go b/pkg/adapter/gateway/tv_repository.go index ea327f6..8d625a0 100644 --- a/pkg/adapter/gateway/tv_repository.go +++ b/pkg/adapter/gateway/tv_repository.go @@ -33,6 +33,10 @@ type ( } ) +func (r *TVRepository) UpdateOrder(order *domain.TVOrder) error { + return r.RWDB.Save(&order).Error +} + func (r *TVRepository) SaveOrder(req domain.TV, order *domain.TVOrder) error { var setting domain.Setting err := r.RODB.Where("api_key = ? AND api_secret_key = ?", req.APIKey, req.APISecretKey).First(&setting).Error @@ -71,3 +75,9 @@ func (r *TVRepository) GetSettings() ([]domain.Setting, error) { func (r *TVRepository) SaveWalletHistories(histories []domain.WalletHistory) error { return r.RWDB.Create(&histories).Error } + +func (r *TVRepository) GetPLNullOrders(settingID uint64) (*[]domain.TVOrder, error) { + var orders []domain.TVOrder + err := r.RODB.Where("setting_id = ? AND pl IS NULL", settingID).Order("id desc").Find(&orders).Error + return &orders, err +} diff --git a/pkg/domain/bybit.go b/pkg/domain/bybit.go index bca36ca..b959d58 100644 --- a/pkg/domain/bybit.go +++ b/pkg/domain/bybit.go @@ -134,6 +134,42 @@ type BybitUSDCPositions struct { RetMsg string `json:"retMsg"` } +type BybitLinearClosedPnLResponse struct { + RetCode int `json:"ret_code"` + RetMsg string `json:"ret_msg"` + Result struct { + Data []BybitLinearClosedPnL `json:"data"` + CurrentPage int `json:"current_page"` + } `json:"result"` + ExtCode string `json:"ext_code"` + ExtInfo string `json:"ext_info"` + TimeNow string `json:"time_now"` + RateLimitStatus int `json:"rate_limit_status"` + RateLimitResetMs int64 `json:"rate_limit_reset_ms"` + RateLimit int `json:"rate_limit"` +} + +type BybitLinearClosedPnL struct { + ID int `json:"id"` + UserID int `json:"user_id"` + Symbol string `json:"symbol"` + OrderID string `json:"order_id"` + Side string `json:"side"` + Qty float64 `json:"qty"` + OrderPrice float64 `json:"order_price"` + OrderType string `json:"order_type"` + ExecType string `json:"exec_type"` + ClosedSize float64 `json:"closed_size"` + CumEntryValue float64 `json:"cum_entry_value"` + AvgEntryPrice float64 `json:"avg_entry_price"` + CumExitValue float64 `json:"cum_exit_value"` + AvgExitPrice float64 `json:"avg_exit_price"` + ClosedPnl float64 `json:"closed_pnl"` + FillCount int `json:"fill_count"` + Leverage int `json:"leverage"` + CreatedAt int64 `json:"created_at"` +} + type BybitWallet struct { Result struct { WalletBalance string `json:"walletBalance"` diff --git a/pkg/domain/tv.go b/pkg/domain/tv.go index b5f26d4..8f9f25b 100644 --- a/pkg/domain/tv.go +++ b/pkg/domain/tv.go @@ -44,6 +44,7 @@ type TVOrder struct { QTY float64 `gorm:"type:float" json:"qty" binding:"required"` TP interface{} `gorm:"type:float" json:"tp"` SL interface{} `gorm:"type:float" json:"sl"` + PL decimal.Decimal `gorm:"type:decimal(10,4)" json:"-"` } type Tabler interface { diff --git a/pkg/external/cron.go b/pkg/external/cron.go index ff60393..43be71f 100644 --- a/pkg/external/cron.go +++ b/pkg/external/cron.go @@ -22,6 +22,7 @@ package external import ( "context" + "os" "github.com/rluisr/tvbit-bot/pkg/domain" @@ -35,14 +36,18 @@ func cron() { Verbose: true, }) - task.Task("0 * * * *", func(ctx context.Context) (int, error) { + /* + Set PnL to an order history. + Update a record if createdAt of saved order history on DB is lower than last closed order(GetClosedOrderLast)'s createdAt. + Why this implementation? + An order_id of open and close is difference, so we can't fetch a closed PnL with open order_id. + */ + task.Task("* * * * *", func(ctx context.Context) (int, error) { settings, err := tvController.Interactor.TVRepository.GetSettings() if err != nil { return 0, err } - var walletHistories []domain.WalletHistory - for _, setting := range settings { switch setting.CEX { case "bybit": @@ -52,48 +57,95 @@ func cron() { APISecretKey: setting.APISecretKey, }) - // USDC - bybitUSDCWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoUSDC() - if err != nil { - return 1, err - } - - balance, err := decimal.NewFromString(bybitUSDCWallet.Result.WalletBalance) - if err != nil { - return 1, err - } - totalRPL, err := decimal.NewFromString(bybitUSDCWallet.Result.TotalRPL) + savedPLNullOrders, err := tvController.Interactor.TVRepository.GetPLNullOrders(setting.ID) if err != nil { - return 1, err + return 0, err } - walletHistories = append(walletHistories, domain.WalletHistory{ - SettingID: setting.ID, - Type: "usdc", - Balance: balance, - TotalRPL: totalRPL, - }) + for _, savedOrder := range *savedPLNullOrders { + closedOrder, err := tvController.Interactor.BybitRepository.GetClosedOrderLast(savedOrder.Symbol) + if err != nil { + return 0, err + } - // Deriv USDT - bybitDerivWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoDeriv() - if err != nil { - return 1, err + if savedOrder.CreatedAt.Unix() < closedOrder.Result.Data[0].CreatedAt { + savedOrder.PL = decimal.NewFromFloat(closedOrder.Result.Data[0].ClosedPnl) + err = tvController.Interactor.TVRepository.UpdateOrder(&savedOrder) + if err != nil { + return 0, err + } + break + } } - walletHistories = append(walletHistories, domain.WalletHistory{ - SettingID: setting.ID, - Type: "usdt", - Balance: decimal.NewFromFloat(bybitDerivWallet.Equity), - TotalRPL: decimal.NewFromFloat(bybitDerivWallet.CumRealisedPnl), - }) } } - - err = tvController.Interactor.TVRepository.SaveWalletHistories(walletHistories) - if err != nil { - return 1, err - } return 0, nil }) + if os.Getenv("SERVER_ENV") != "local" { + + /* + Save wallet balance + */ + task.Task("0 * * * *", func(ctx context.Context) (int, error) { + settings, err := tvController.Interactor.TVRepository.GetSettings() + if err != nil { + return 0, err + } + + var walletHistories []domain.WalletHistory + + for _, setting := range settings { + switch setting.CEX { + case "bybit": + tvController.Bybit(domain.TV{ + IsTestNet: setting.IsTestnet, + APIKey: setting.APIKey, + APISecretKey: setting.APISecretKey, + }) + + // USDC + bybitUSDCWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoUSDC() + if err != nil { + return 1, err + } + + balance, err := decimal.NewFromString(bybitUSDCWallet.Result.WalletBalance) + if err != nil { + return 1, err + } + totalRPL, err := decimal.NewFromString(bybitUSDCWallet.Result.TotalRPL) + if err != nil { + return 1, err + } + + walletHistories = append(walletHistories, domain.WalletHistory{ + SettingID: setting.ID, + Type: "usdc", + Balance: balance, + TotalRPL: totalRPL, + }) + + // Deriv USDT + bybitDerivWallet, err := tvController.Interactor.BybitRepository.GetWalletInfoDeriv() + if err != nil { + return 1, err + } + walletHistories = append(walletHistories, domain.WalletHistory{ + SettingID: setting.ID, + Type: "usdt", + Balance: decimal.NewFromFloat(bybitDerivWallet.Equity), + TotalRPL: decimal.NewFromFloat(bybitDerivWallet.CumRealisedPnl), + }) + } + } + + err = tvController.Interactor.TVRepository.SaveWalletHistories(walletHistories) + if err != nil { + return 1, err + } + return 0, nil + }) + } task.Run() } diff --git a/pkg/usecase/interfaces/repositories.go b/pkg/usecase/interfaces/repositories.go index 4983552..b50cc8c 100644 --- a/pkg/usecase/interfaces/repositories.go +++ b/pkg/usecase/interfaces/repositories.go @@ -25,9 +25,11 @@ import ( type TVRepository interface { SaveOrder(domain.TV, *domain.TVOrder) error + UpdateOrder(order *domain.TVOrder) error GetSetting(apiKey, apiSecretKey string) (*domain.Setting, error) GetSettings() ([]domain.Setting, error) SaveWalletHistories([]domain.WalletHistory) error + GetPLNullOrders(settingID uint64) (*[]domain.TVOrder, error) } type BybitRepository interface { @@ -39,6 +41,7 @@ type BybitRepository interface { GetWalletInfoDeriv() (*rest.Balance, error) GetActiveOrderCount(req *domain.TV, positions *[]rest.LinearPosition) int GetPositions(symbol string) (*[]rest.LinearPosition, error) + GetClosedOrderLast(symbol string) (*domain.BybitLinearClosedPnLResponse, error) } type SettingRepository interface {