mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 23:07:01 +08:00
Merge branch 'dev' into fix/bug-fixes-collection-v2
This commit is contained in:
@@ -438,13 +438,23 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 🔍 调试:打印原始API响应
|
||||
log.Printf("🔍 Aster API原始响应: %s", string(body))
|
||||
|
||||
// 查找USDT余额
|
||||
totalBalance := 0.0
|
||||
availableBalance := 0.0
|
||||
crossUnPnl := 0.0
|
||||
|
||||
for _, bal := range balances {
|
||||
// 🔍 调试:打印每条余额记录
|
||||
log.Printf("🔍 余额记录: %+v", bal)
|
||||
|
||||
if asset, ok := bal["asset"].(string); ok && asset == "USDT" {
|
||||
// 🔍 调试:打印USDT余额详情
|
||||
log.Printf("🔍 USDT余额详情: balance=%v, availableBalance=%v, crossUnPnl=%v",
|
||||
bal["balance"], bal["availableBalance"], bal["crossUnPnl"])
|
||||
|
||||
if wb, ok := bal["balance"].(string); ok {
|
||||
totalBalance, _ = strconv.ParseFloat(wb, 64)
|
||||
}
|
||||
@@ -458,11 +468,25 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Aster API完全兼容Binance API格式
|
||||
// balance字段 = wallet balance(不包含未实现盈亏)
|
||||
// crossUnPnl = unrealized profit(未实现盈亏)
|
||||
// crossWalletBalance = balance + crossUnPnl(全仓钱包余额,包含盈亏)
|
||||
//
|
||||
// 参考Binance官方文档:
|
||||
// - Account Information V2: marginBalance = walletBalance + unrealizedProfit
|
||||
// - Balance V3: crossWalletBalance = balance + crossUnPnl
|
||||
|
||||
log.Printf("✓ Aster API返回: 钱包余额=%.2f, 未实现盈亏=%.2f, 可用余额=%.2f",
|
||||
totalBalance,
|
||||
crossUnPnl,
|
||||
availableBalance)
|
||||
|
||||
// 返回与Binance相同的字段名,确保AutoTrader能正确解析
|
||||
return map[string]interface{}{
|
||||
"totalWalletBalance": totalBalance,
|
||||
"totalWalletBalance": totalBalance, // 钱包余额(不含未实现盈亏)
|
||||
"availableBalance": availableBalance,
|
||||
"totalUnrealizedProfit": crossUnPnl,
|
||||
"totalUnrealizedProfit": crossUnPnl, // 未实现盈亏
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -981,6 +1005,61 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
|
||||
func (t *AsterTrader) CancelStopOrders(symbol string) error {
|
||||
// 获取该币种的所有未完成订单
|
||||
params := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
}
|
||||
|
||||
body, err := t.request("GET", "/fapi/v3/openOrders", params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取未完成订单失败: %w", err)
|
||||
}
|
||||
|
||||
var orders []map[string]interface{}
|
||||
if err := json.Unmarshal(body, &orders); err != nil {
|
||||
return fmt.Errorf("解析订单数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 过滤出止盈止损单并取消
|
||||
canceledCount := 0
|
||||
for _, order := range orders {
|
||||
orderType, _ := order["type"].(string)
|
||||
|
||||
// 只取消止损和止盈订单
|
||||
if orderType == "STOP_MARKET" ||
|
||||
orderType == "TAKE_PROFIT_MARKET" ||
|
||||
orderType == "STOP" ||
|
||||
orderType == "TAKE_PROFIT" {
|
||||
|
||||
orderID, _ := order["orderId"].(float64)
|
||||
cancelParams := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
"orderId": int64(orderID),
|
||||
}
|
||||
|
||||
_, err := t.request("DELETE", "/fapi/v3/order", cancelParams)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
|
||||
symbol, int64(orderID), orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatQuantity 格式化数量(实现Trader接口)
|
||||
func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
||||
formatted, err := t.formatQuantity(symbol, quantity)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"nofx/decision"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
@@ -258,9 +259,9 @@ func (at *AutoTrader) Stop() {
|
||||
func (at *AutoTrader) runCycle() error {
|
||||
at.callCount++
|
||||
|
||||
log.Printf("\n" + strings.Repeat("=", 70))
|
||||
log.Print("\n" + strings.Repeat("=", 70) + "\n")
|
||||
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
|
||||
log.Printf(strings.Repeat("=", 70))
|
||||
log.Println(strings.Repeat("=", 70))
|
||||
|
||||
// 创建决策记录
|
||||
record := &logger.DecisionRecord{
|
||||
@@ -347,19 +348,19 @@ func (at *AutoTrader) runCycle() error {
|
||||
// 打印系统提示词和AI思维链(即使有错误,也要输出以便调试)
|
||||
if decision != nil {
|
||||
if decision.SystemPrompt != "" {
|
||||
log.Printf("\n" + strings.Repeat("=", 70))
|
||||
log.Print("\n" + strings.Repeat("=", 70) + "\n")
|
||||
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
|
||||
log.Println(strings.Repeat("=", 70))
|
||||
log.Println(decision.SystemPrompt)
|
||||
log.Printf(strings.Repeat("=", 70) + "\n")
|
||||
log.Println(strings.Repeat("=", 70))
|
||||
}
|
||||
|
||||
if decision.CoTTrace != "" {
|
||||
log.Printf("\n" + strings.Repeat("-", 70))
|
||||
log.Print("\n" + strings.Repeat("-", 70) + "\n")
|
||||
log.Println("💭 AI思维链分析(错误情况):")
|
||||
log.Println(strings.Repeat("-", 70))
|
||||
log.Println(decision.CoTTrace)
|
||||
log.Printf(strings.Repeat("-", 70) + "\n")
|
||||
log.Println(strings.Repeat("-", 70))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,6 +601,12 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, act
|
||||
return at.executeCloseLongWithRecord(decision, actionRecord)
|
||||
case "close_short":
|
||||
return at.executeCloseShortWithRecord(decision, actionRecord)
|
||||
case "update_stop_loss":
|
||||
return at.executeUpdateStopLossWithRecord(decision, actionRecord)
|
||||
case "update_take_profit":
|
||||
return at.executeUpdateTakeProfitWithRecord(decision, actionRecord)
|
||||
case "partial_close":
|
||||
return at.executePartialCloseWithRecord(decision, actionRecord)
|
||||
case "hold", "wait":
|
||||
// 无需执行,仅记录
|
||||
return nil
|
||||
@@ -778,6 +785,201 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, a
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeUpdateStopLossWithRecord 执行调整止损并记录详细信息
|
||||
func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
|
||||
log.Printf(" 🎯 调整止损: %s → %.2f", decision.Symbol, decision.NewStopLoss)
|
||||
|
||||
// 获取当前价格
|
||||
marketData, err := market.Get(decision.Symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actionRecord.Price = marketData.CurrentPrice
|
||||
|
||||
// 获取当前持仓
|
||||
positions, err := at.trader.GetPositions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取持仓失败: %w", err)
|
||||
}
|
||||
|
||||
// 查找目标持仓
|
||||
var targetPosition map[string]interface{}
|
||||
for _, pos := range positions {
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
posAmt, _ := pos["positionAmt"].(float64)
|
||||
if symbol == decision.Symbol && posAmt != 0 {
|
||||
targetPosition = pos
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetPosition == nil {
|
||||
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
|
||||
}
|
||||
|
||||
// 获取持仓方向和数量
|
||||
side, _ := targetPosition["side"].(string)
|
||||
positionSide := strings.ToUpper(side)
|
||||
positionAmt, _ := targetPosition["positionAmt"].(float64)
|
||||
|
||||
// 验证新止损价格合理性
|
||||
if positionSide == "LONG" && decision.NewStopLoss >= marketData.CurrentPrice {
|
||||
return fmt.Errorf("多单止损必须低于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
|
||||
}
|
||||
if positionSide == "SHORT" && decision.NewStopLoss <= marketData.CurrentPrice {
|
||||
return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
|
||||
}
|
||||
|
||||
// 取消旧的止损单(避免多个止损单共存)
|
||||
if err := at.trader.CancelStopOrders(decision.Symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧止损单失败: %v", err)
|
||||
// 不中断执行,继续设置新止损
|
||||
}
|
||||
|
||||
// 调用交易所 API 修改止损
|
||||
quantity := math.Abs(positionAmt)
|
||||
err = at.trader.SetStopLoss(decision.Symbol, positionSide, quantity, decision.NewStopLoss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("修改止损失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 止损已调整: %.2f (当前价格: %.2f)", decision.NewStopLoss, marketData.CurrentPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeUpdateTakeProfitWithRecord 执行调整止盈并记录详细信息
|
||||
func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
|
||||
log.Printf(" 🎯 调整止盈: %s → %.2f", decision.Symbol, decision.NewTakeProfit)
|
||||
|
||||
// 获取当前价格
|
||||
marketData, err := market.Get(decision.Symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actionRecord.Price = marketData.CurrentPrice
|
||||
|
||||
// 获取当前持仓
|
||||
positions, err := at.trader.GetPositions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取持仓失败: %w", err)
|
||||
}
|
||||
|
||||
// 查找目标持仓
|
||||
var targetPosition map[string]interface{}
|
||||
for _, pos := range positions {
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
posAmt, _ := pos["positionAmt"].(float64)
|
||||
if symbol == decision.Symbol && posAmt != 0 {
|
||||
targetPosition = pos
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetPosition == nil {
|
||||
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
|
||||
}
|
||||
|
||||
// 获取持仓方向和数量
|
||||
side, _ := targetPosition["side"].(string)
|
||||
positionSide := strings.ToUpper(side)
|
||||
positionAmt, _ := targetPosition["positionAmt"].(float64)
|
||||
|
||||
// 验证新止盈价格合理性
|
||||
if positionSide == "LONG" && decision.NewTakeProfit <= marketData.CurrentPrice {
|
||||
return fmt.Errorf("多单止盈必须高于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
|
||||
}
|
||||
if positionSide == "SHORT" && decision.NewTakeProfit >= marketData.CurrentPrice {
|
||||
return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
|
||||
}
|
||||
|
||||
// 取消旧的止盈单(避免多个止盈单共存)
|
||||
if err := at.trader.CancelStopOrders(decision.Symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧止盈单失败: %v", err)
|
||||
// 不中断执行,继续设置新止盈
|
||||
}
|
||||
|
||||
// 调用交易所 API 修改止盈
|
||||
quantity := math.Abs(positionAmt)
|
||||
err = at.trader.SetTakeProfit(decision.Symbol, positionSide, quantity, decision.NewTakeProfit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("修改止盈失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 止盈已调整: %.2f (当前价格: %.2f)", decision.NewTakeProfit, marketData.CurrentPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePartialCloseWithRecord 执行部分平仓并记录详细信息
|
||||
func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
|
||||
log.Printf(" 📊 部分平仓: %s %.1f%%", decision.Symbol, decision.ClosePercentage)
|
||||
|
||||
// 验证百分比范围
|
||||
if decision.ClosePercentage <= 0 || decision.ClosePercentage > 100 {
|
||||
return fmt.Errorf("平仓百分比必须在 0-100 之间,当前: %.1f", decision.ClosePercentage)
|
||||
}
|
||||
|
||||
// 获取当前价格
|
||||
marketData, err := market.Get(decision.Symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actionRecord.Price = marketData.CurrentPrice
|
||||
|
||||
// 获取当前持仓
|
||||
positions, err := at.trader.GetPositions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取持仓失败: %w", err)
|
||||
}
|
||||
|
||||
// 查找目标持仓
|
||||
var targetPosition map[string]interface{}
|
||||
for _, pos := range positions {
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
posAmt, _ := pos["positionAmt"].(float64)
|
||||
if symbol == decision.Symbol && posAmt != 0 {
|
||||
targetPosition = pos
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetPosition == nil {
|
||||
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
|
||||
}
|
||||
|
||||
// 获取持仓方向和数量
|
||||
side, _ := targetPosition["side"].(string)
|
||||
positionSide := strings.ToUpper(side)
|
||||
positionAmt, _ := targetPosition["positionAmt"].(float64)
|
||||
|
||||
// 计算平仓数量
|
||||
totalQuantity := math.Abs(positionAmt)
|
||||
closeQuantity := totalQuantity * (decision.ClosePercentage / 100.0)
|
||||
actionRecord.Quantity = closeQuantity
|
||||
|
||||
// 执行平仓
|
||||
var order map[string]interface{}
|
||||
if positionSide == "LONG" {
|
||||
order, err = at.trader.CloseLong(decision.Symbol, closeQuantity)
|
||||
} else {
|
||||
order, err = at.trader.CloseShort(decision.Symbol, closeQuantity)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("部分平仓失败: %w", err)
|
||||
}
|
||||
|
||||
// 记录订单ID
|
||||
if orderID, ok := order["orderId"].(int64); ok {
|
||||
actionRecord.OrderID = orderID
|
||||
}
|
||||
|
||||
remainingQuantity := totalQuantity - closeQuantity
|
||||
log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f",
|
||||
closeQuantity, decision.ClosePercentage, remainingQuantity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID 获取trader ID
|
||||
func (at *AutoTrader) GetID() string {
|
||||
return at.id
|
||||
@@ -991,12 +1193,14 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision
|
||||
// 定义优先级
|
||||
getActionPriority := func(action string) int {
|
||||
switch action {
|
||||
case "close_long", "close_short":
|
||||
return 1 // 最高优先级:先平仓
|
||||
case "close_long", "close_short", "partial_close":
|
||||
return 1 // 最高优先级:先平仓(包括部分平仓)
|
||||
case "update_stop_loss", "update_take_profit":
|
||||
return 2 // 调整持仓止盈止损
|
||||
case "open_long", "open_short":
|
||||
return 2 // 次优先级:后开仓
|
||||
return 3 // 次优先级:后开仓
|
||||
case "hold", "wait":
|
||||
return 3 // 最低优先级:观望
|
||||
return 4 // 最低优先级:观望
|
||||
default:
|
||||
return 999 // 未知动作放最后
|
||||
}
|
||||
|
||||
@@ -425,6 +425,53 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
|
||||
func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||||
// 获取该币种的所有未完成订单
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取未完成订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 过滤出止盈止损单并取消
|
||||
canceledCount := 0
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
|
||||
// 只取消止损和止盈订单
|
||||
if orderType == futures.OrderTypeStopMarket ||
|
||||
orderType == futures.OrderTypeTakeProfitMarket ||
|
||||
orderType == futures.OrderTypeStop ||
|
||||
orderType == futures.OrderTypeTakeProfit {
|
||||
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
|
||||
symbol, order.OrderID, orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMarketPrice 获取市场价格
|
||||
func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||
prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background())
|
||||
|
||||
@@ -2,10 +2,12 @@ package trader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/sonirico/go-hyperliquid"
|
||||
@@ -22,6 +24,9 @@ type HyperliquidTrader struct {
|
||||
|
||||
// NewHyperliquidTrader 创建Hyperliquid交易器
|
||||
func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) {
|
||||
// 去掉私钥的 0x 前缀(如果有,不区分大小写)
|
||||
privateKeyHex = strings.TrimPrefix(strings.ToLower(privateKeyHex), "0x")
|
||||
|
||||
// 解析私钥
|
||||
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
||||
if err != nil {
|
||||
@@ -34,13 +39,18 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
apiURL = hyperliquid.TestnetAPIURL
|
||||
}
|
||||
|
||||
// // 从私钥生成钱包地址
|
||||
// pubKey := privateKey.Public()
|
||||
// publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
||||
// if !ok {
|
||||
// return nil, fmt.Errorf("无法转换公钥")
|
||||
// }
|
||||
// walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
||||
// 从私钥生成钱包地址(如果未提供)
|
||||
if walletAddr == "" {
|
||||
pubKey := privateKey.Public()
|
||||
publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("无法转换公钥")
|
||||
}
|
||||
walletAddr = crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
||||
log.Printf("✓ 从私钥自动生成钱包地址: %s", walletAddr)
|
||||
} else {
|
||||
log.Printf("✓ 使用提供的钱包地址: %s", walletAddr)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -76,23 +86,54 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
log.Printf("🔄 正在调用Hyperliquid API获取账户余额...")
|
||||
|
||||
// 获取账户状态
|
||||
// ✅ Step 1: 查询 Spot 现货账户余额
|
||||
spotState, err := t.exchange.Info().SpotUserState(t.ctx, t.walletAddr)
|
||||
var spotUSDCBalance float64 = 0.0
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 查询 Spot 余额失败(可能无现货资产): %v", err)
|
||||
} else if spotState != nil && len(spotState.Balances) > 0 {
|
||||
for _, balance := range spotState.Balances {
|
||||
if balance.Coin == "USDC" {
|
||||
spotUSDCBalance, _ = strconv.ParseFloat(balance.Total, 64)
|
||||
log.Printf("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Step 2: 查询 Perpetuals 合约账户状态
|
||||
accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
log.Printf("❌ Hyperliquid API调用失败: %v", err)
|
||||
log.Printf("❌ Hyperliquid Perpetuals API调用失败: %v", err)
|
||||
return nil, fmt.Errorf("获取账户信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析余额信息(MarginSummary字段都是string)
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// 🔍 调试:打印API返回的完整CrossMarginSummary结构
|
||||
summaryJSON, _ := json.MarshalIndent(accountState.MarginSummary, " ", " ")
|
||||
log.Printf("🔍 [DEBUG] Hyperliquid API CrossMarginSummary完整数据:")
|
||||
log.Printf("%s", string(summaryJSON))
|
||||
// ✅ Step 3: 根据保证金模式动态选择正确的摘要(CrossMarginSummary 或 MarginSummary)
|
||||
var accountValue, totalMarginUsed float64
|
||||
var summaryType string
|
||||
var summary interface{}
|
||||
|
||||
accountValue, _ := strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ := strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64)
|
||||
if t.isCrossMargin {
|
||||
// 全仓模式:使用 CrossMarginSummary
|
||||
accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
||||
summaryType = "CrossMarginSummary (全仓)"
|
||||
summary = accountState.CrossMarginSummary
|
||||
} else {
|
||||
// 逐仓模式:使用 MarginSummary
|
||||
accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64)
|
||||
summaryType = "MarginSummary (逐仓)"
|
||||
summary = accountState.MarginSummary
|
||||
}
|
||||
|
||||
// 🔍 调试:打印API返回的完整摘要结构
|
||||
summaryJSON, _ := json.MarshalIndent(summary, " ", " ")
|
||||
log.Printf("🔍 [DEBUG] Hyperliquid API %s 完整数据:", summaryType)
|
||||
log.Printf("%s", string(summaryJSON))
|
||||
|
||||
// ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏
|
||||
totalUnrealizedPnl := 0.0
|
||||
@@ -109,16 +150,47 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
// 需要返回"不包含未实现盈亏的钱包余额"
|
||||
walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl
|
||||
|
||||
result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏)
|
||||
result["availableBalance"] = accountValue - totalMarginUsed // 可用余额(总净值 - 占用保证金)
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
|
||||
// ✅ Step 4: 使用 Withdrawable 欄位(PR #443)
|
||||
// Withdrawable 是官方提供的真实可提现余额,比简单计算更可靠
|
||||
availableBalance := 0.0
|
||||
if accountState.Withdrawable != "" {
|
||||
withdrawable, err := strconv.ParseFloat(accountState.Withdrawable, 64)
|
||||
if err == nil && withdrawable > 0 {
|
||||
availableBalance = withdrawable
|
||||
log.Printf("✓ 使用 Withdrawable 作为可用余额: %.2f", availableBalance)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f",
|
||||
// 降级方案:如果没有 Withdrawable,使用简单计算
|
||||
if availableBalance == 0 && accountState.Withdrawable == "" {
|
||||
availableBalance = accountValue - totalMarginUsed
|
||||
if availableBalance < 0 {
|
||||
log.Printf("⚠️ 计算出的可用余额为负数 (%.2f),重置为 0", availableBalance)
|
||||
availableBalance = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Step 5: 正確處理 Spot + Perpetuals 余额
|
||||
// 重要:Spot 只加到總資產,不加到可用餘額
|
||||
// 原因:Spot 和 Perpetuals 是獨立帳戶,需手動 ClassTransfer 才能轉帳
|
||||
totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance
|
||||
|
||||
result["totalWalletBalance"] = totalWalletBalance // 總資產(Perp + Spot)
|
||||
result["availableBalance"] = availableBalance // 可用餘額(僅 Perpetuals,不含 Spot)
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未實現盈虧(僅來自 Perpetuals)
|
||||
result["spotBalance"] = spotUSDCBalance // Spot 現貨餘額(單獨返回)
|
||||
|
||||
log.Printf("✓ Hyperliquid 完整账户:")
|
||||
log.Printf(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance)
|
||||
log.Printf(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)",
|
||||
accountValue,
|
||||
walletBalanceWithoutUnrealized,
|
||||
totalUnrealizedPnl,
|
||||
result["availableBalance"],
|
||||
totalMarginUsed)
|
||||
totalUnrealizedPnl)
|
||||
log.Printf(" • Perpetuals 可用余额: %.2f USDC (可直接用於開倉)", availableBalance)
|
||||
log.Printf(" • 保证金占用: %.2f USDC", totalMarginUsed)
|
||||
log.Printf(" • 總資產 (Perp+Spot): %.2f USDC", totalWalletBalance)
|
||||
log.Printf(" ⭐ 总资产: %.2f USDC | Perp 可用: %.2f USDC | Spot 余额: %.2f USDC",
|
||||
totalWalletBalance, availableBalance, spotUSDCBalance)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -501,6 +573,40 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
|
||||
func (t *HyperliquidTrader) CancelStopOrders(symbol string) error {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取所有挂单
|
||||
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取挂单失败: %w", err)
|
||||
}
|
||||
|
||||
// 注意:Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段
|
||||
// 因此暂时取消该币种的所有挂单(包括止盈止损单)
|
||||
// 这是安全的,因为在设置新的止盈止损之前,应该清理所有旧订单
|
||||
canceledCount := 0
|
||||
for _, order := range openOrders {
|
||||
if order.Coin == coin {
|
||||
_, err := t.exchange.Cancel(t.ctx, coin, order.Oid)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err)
|
||||
continue
|
||||
}
|
||||
canceledCount++
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有挂单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMarketPrice 获取市场价格
|
||||
func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
@@ -39,6 +39,9 @@ type Trader interface {
|
||||
// CancelAllOrders 取消该币种的所有挂单
|
||||
CancelAllOrders(symbol string) error
|
||||
|
||||
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
|
||||
CancelStopOrders(symbol string) error
|
||||
|
||||
// FormatQuantity 格式化数量到正确的精度
|
||||
FormatQuantity(symbol string, quantity float64) (string, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user