mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
fix: initial balance calculation and UI improvements
- Fix initial balance using available_balance instead of total_equity - Fix WSMonitor nil pointer by starting market monitor before loading traders - Add strategy name display on traders list and dashboard pages - Various position sync and trading improvements
This commit is contained in:
@@ -2,12 +2,15 @@ package trader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"nofx/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,7 +21,9 @@ import (
|
||||
|
||||
// BybitTrader Bybit USDT Perpetual Futures Trader
|
||||
type BybitTrader struct {
|
||||
client *bybit.Client
|
||||
client *bybit.Client
|
||||
apiKey string
|
||||
secretKey string
|
||||
|
||||
// Balance cache
|
||||
cachedBalance map[string]interface{}
|
||||
@@ -59,6 +64,8 @@ func NewBybitTrader(apiKey, secretKey string) *BybitTrader {
|
||||
|
||||
trader := &BybitTrader{
|
||||
client: client,
|
||||
apiKey: apiKey,
|
||||
secretKey: secretKey,
|
||||
cacheDuration: 15 * time.Second,
|
||||
qtyStepCache: make(map[string]float64),
|
||||
}
|
||||
@@ -856,3 +863,149 @@ func (t *BybitTrader) cancelConditionalOrders(symbol string, orderType string) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClosedPnL retrieves closed position PnL records from Bybit via direct HTTP API
|
||||
func (t *BybitTrader) GetClosedPnL(startTime time.Time, limit int) ([]ClosedPnLRecord, error) {
|
||||
// The Bybit SDK doesn't expose the closed-pnl endpoint, use direct HTTP call
|
||||
return t.getClosedPnLViaHTTP(startTime, limit)
|
||||
}
|
||||
|
||||
// getClosedPnLViaHTTP makes direct HTTP call to Bybit API for closed PnL with proper signing
|
||||
func (t *BybitTrader) getClosedPnLViaHTTP(startTime time.Time, limit int) ([]ClosedPnLRecord, error) {
|
||||
// Build query string
|
||||
queryParams := fmt.Sprintf("category=linear&startTime=%d&limit=%d", startTime.UnixMilli(), limit)
|
||||
url := "https://api.bybit.com/v5/position/closed-pnl?" + queryParams
|
||||
|
||||
// Generate timestamp
|
||||
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
recvWindow := "5000"
|
||||
|
||||
// Build signature payload: timestamp + api_key + recv_window + queryString
|
||||
signPayload := timestamp + t.apiKey + recvWindow + queryParams
|
||||
|
||||
// Generate HMAC-SHA256 signature
|
||||
h := hmac.New(sha256.New, []byte(t.secretKey))
|
||||
h.Write([]byte(signPayload))
|
||||
signature := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Add Bybit V5 API headers
|
||||
req.Header.Set("X-BAPI-API-KEY", t.apiKey)
|
||||
req.Header.Set("X-BAPI-SIGN", signature)
|
||||
req.Header.Set("X-BAPI-SIGN-TYPE", "2")
|
||||
req.Header.Set("X-BAPI-TIMESTAMP", timestamp)
|
||||
req.Header.Set("X-BAPI-RECV-WINDOW", recvWindow)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Use http.DefaultClient for the request
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call Bybit API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
RetCode int `json:"retCode"`
|
||||
RetMsg string `json:"retMsg"`
|
||||
Result map[string]interface{} `json:"result"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if result.RetCode != 0 {
|
||||
return nil, fmt.Errorf("Bybit API error: %s", result.RetMsg)
|
||||
}
|
||||
|
||||
return t.parseClosedPnLResult(result.Result)
|
||||
}
|
||||
|
||||
// parseClosedPnLResult parses the closed PnL result from Bybit API
|
||||
func (t *BybitTrader) parseClosedPnLResult(resultData interface{}) ([]ClosedPnLRecord, error) {
|
||||
data, ok := resultData.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid result format")
|
||||
}
|
||||
|
||||
list, _ := data["list"].([]interface{})
|
||||
var records []ClosedPnLRecord
|
||||
|
||||
for _, item := range list {
|
||||
pnl, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
symbol, _ := pnl["symbol"].(string)
|
||||
side, _ := pnl["side"].(string)
|
||||
orderId, _ := pnl["orderId"].(string)
|
||||
|
||||
avgEntryPriceStr, _ := pnl["avgEntryPrice"].(string)
|
||||
avgExitPriceStr, _ := pnl["avgExitPrice"].(string)
|
||||
qtyStr, _ := pnl["qty"].(string)
|
||||
closedPnLStr, _ := pnl["closedPnl"].(string)
|
||||
cumEntryValueStr, _ := pnl["cumEntryValue"].(string)
|
||||
cumExitValueStr, _ := pnl["cumExitValue"].(string)
|
||||
leverageStr, _ := pnl["leverage"].(string)
|
||||
createdTimeStr, _ := pnl["createdTime"].(string)
|
||||
updatedTimeStr, _ := pnl["updatedTime"].(string)
|
||||
|
||||
avgEntryPrice, _ := strconv.ParseFloat(avgEntryPriceStr, 64)
|
||||
avgExitPrice, _ := strconv.ParseFloat(avgExitPriceStr, 64)
|
||||
qty, _ := strconv.ParseFloat(qtyStr, 64)
|
||||
closedPnL, _ := strconv.ParseFloat(closedPnLStr, 64)
|
||||
leverage, _ := strconv.ParseInt(leverageStr, 10, 64)
|
||||
createdTime, _ := strconv.ParseInt(createdTimeStr, 10, 64)
|
||||
updatedTime, _ := strconv.ParseInt(updatedTimeStr, 10, 64)
|
||||
|
||||
// Calculate approximate fee from value difference
|
||||
cumEntryValue, _ := strconv.ParseFloat(cumEntryValueStr, 64)
|
||||
cumExitValue, _ := strconv.ParseFloat(cumExitValueStr, 64)
|
||||
expectedPnL := cumExitValue - cumEntryValue
|
||||
if side == "Sell" {
|
||||
expectedPnL = cumEntryValue - cumExitValue
|
||||
}
|
||||
fee := expectedPnL - closedPnL
|
||||
if fee < 0 {
|
||||
fee = 0
|
||||
}
|
||||
|
||||
// Normalize side
|
||||
normalizedSide := "long"
|
||||
if side == "Sell" {
|
||||
normalizedSide = "short"
|
||||
}
|
||||
|
||||
record := ClosedPnLRecord{
|
||||
Symbol: symbol,
|
||||
Side: normalizedSide,
|
||||
EntryPrice: avgEntryPrice,
|
||||
ExitPrice: avgExitPrice,
|
||||
Quantity: qty,
|
||||
RealizedPnL: closedPnL,
|
||||
Fee: fee,
|
||||
Leverage: int(leverage),
|
||||
EntryTime: time.UnixMilli(createdTime),
|
||||
ExitTime: time.UnixMilli(updatedTime),
|
||||
OrderID: orderId,
|
||||
CloseType: "unknown", // Bybit doesn't provide close type directly
|
||||
ExchangeID: orderId, // Use orderId as exchange ID
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user