mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 07:16:56 +08:00
Merge upstream/main: Integrate Hyperliquid support with leverage config
Resolved conflicts in manager/trader_manager.go by combining: - Upstream: Exchange field, Hyperliquid API keys, Hyperliquid testnet - Local: BTCETHLeverage and AltcoinLeverage fields Both features are now working together.
This commit is contained in:
@@ -20,10 +20,18 @@ type AutoTraderConfig struct {
|
||||
Name string // Trader显示名称
|
||||
AIModel string // AI模型: "qwen" 或 "deepseek"
|
||||
|
||||
// API配置
|
||||
// 交易平台选择
|
||||
Exchange string // "binance" 或 "hyperliquid"
|
||||
|
||||
// 币安API配置
|
||||
BinanceAPIKey string
|
||||
BinanceSecretKey string
|
||||
CoinPoolAPIURL string
|
||||
|
||||
// Hyperliquid配置
|
||||
HyperliquidPrivateKey string
|
||||
HyperliquidTestnet bool
|
||||
|
||||
CoinPoolAPIURL string
|
||||
|
||||
// AI配置
|
||||
UseQwen bool
|
||||
@@ -51,8 +59,9 @@ type AutoTrader struct {
|
||||
id string // Trader唯一标识
|
||||
name string // Trader显示名称
|
||||
aiModel string // AI模型名称
|
||||
exchange string // 交易平台名称
|
||||
config AutoTraderConfig
|
||||
trader *FuturesTrader
|
||||
trader Trader // 使用Trader接口(支持多平台)
|
||||
decisionLogger *logger.DecisionLogger // 决策日志记录器
|
||||
initialBalance float64
|
||||
dailyPnL float64
|
||||
@@ -95,8 +104,28 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
pool.SetCoinPoolAPI(config.CoinPoolAPIURL)
|
||||
}
|
||||
|
||||
// 初始化币安合约交易器
|
||||
trader := NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
|
||||
// 设置默认交易平台
|
||||
if config.Exchange == "" {
|
||||
config.Exchange = "binance"
|
||||
}
|
||||
|
||||
// 根据配置创建对应的交易器
|
||||
var trader Trader
|
||||
var err error
|
||||
|
||||
switch config.Exchange {
|
||||
case "binance":
|
||||
log.Printf("🏦 [%s] 使用币安合约交易", config.Name)
|
||||
trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
|
||||
case "hyperliquid":
|
||||
log.Printf("🏦 [%s] 使用Hyperliquid交易", config.Name)
|
||||
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidTestnet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
|
||||
}
|
||||
|
||||
// 验证初始金额配置
|
||||
if config.InitialBalance <= 0 {
|
||||
@@ -111,6 +140,7 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
id: config.ID,
|
||||
name: config.Name,
|
||||
aiModel: config.AIModel,
|
||||
exchange: config.Exchange,
|
||||
config: config,
|
||||
trader: trader,
|
||||
decisionLogger: decisionLogger,
|
||||
@@ -693,6 +723,7 @@ func (at *AutoTrader) GetStatus() map[string]interface{} {
|
||||
"trader_id": at.id,
|
||||
"trader_name": at.name,
|
||||
"ai_model": at.aiModel,
|
||||
"exchange": at.exchange,
|
||||
"is_running": at.isRunning,
|
||||
"start_time": at.startTime.Format(time.RFC3339),
|
||||
"runtime_minutes": int(time.Since(at.startTime).Minutes()),
|
||||
|
||||
672
trader/hyperliquid_trader.go
Normal file
672
trader/hyperliquid_trader.go
Normal file
@@ -0,0 +1,672 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/sonirico/go-hyperliquid"
|
||||
)
|
||||
|
||||
// HyperliquidTrader Hyperliquid交易器
|
||||
type HyperliquidTrader struct {
|
||||
exchange *hyperliquid.Exchange
|
||||
ctx context.Context
|
||||
walletAddr string
|
||||
meta *hyperliquid.Meta // 缓存meta信息(包含精度等)
|
||||
}
|
||||
|
||||
// NewHyperliquidTrader 创建Hyperliquid交易器
|
||||
func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrader, error) {
|
||||
// 解析私钥
|
||||
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析私钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 选择API URL
|
||||
apiURL := hyperliquid.MainnetAPIURL
|
||||
if testnet {
|
||||
apiURL = hyperliquid.TestnetAPIURL
|
||||
}
|
||||
|
||||
// 从私钥生成钱包地址
|
||||
pubKey := privateKey.Public()
|
||||
publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("无法转换公钥")
|
||||
}
|
||||
walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建Exchange客户端(Exchange包含Info功能)
|
||||
exchange := hyperliquid.NewExchange(
|
||||
ctx,
|
||||
privateKey,
|
||||
apiURL,
|
||||
nil, // Meta will be fetched automatically
|
||||
"", // vault address (empty for personal account)
|
||||
walletAddr, // wallet address
|
||||
nil, // SpotMeta will be fetched automatically
|
||||
)
|
||||
|
||||
log.Printf("✓ Hyperliquid交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr)
|
||||
|
||||
// 获取meta信息(包含精度等配置)
|
||||
meta, err := exchange.Info().Meta(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取meta信息失败: %w", err)
|
||||
}
|
||||
|
||||
return &HyperliquidTrader{
|
||||
exchange: exchange,
|
||||
ctx: ctx,
|
||||
walletAddr: walletAddr,
|
||||
meta: meta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBalance 获取账户余额
|
||||
func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
log.Printf("🔄 正在调用Hyperliquid API获取账户余额...")
|
||||
|
||||
// 获取账户状态
|
||||
accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
log.Printf("❌ Hyperliquid API调用失败: %v", err)
|
||||
return nil, fmt.Errorf("获取账户信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析余额信息(MarginSummary字段都是string)
|
||||
result := make(map[string]interface{})
|
||||
|
||||
accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
||||
|
||||
// ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏
|
||||
totalUnrealizedPnl := 0.0
|
||||
for _, assetPos := range accountState.AssetPositions {
|
||||
unrealizedPnl, _ := strconv.ParseFloat(assetPos.Position.UnrealizedPnl, 64)
|
||||
totalUnrealizedPnl += unrealizedPnl
|
||||
}
|
||||
|
||||
// ✅ 正确理解Hyperliquid字段:
|
||||
// AccountValue = 账户净值(包含未实现盈亏)= 这是真正的总资产
|
||||
// 钱包余额(已实现)= AccountValue - 未实现盈亏
|
||||
walletBalance := accountValue - totalUnrealizedPnl
|
||||
|
||||
result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分)
|
||||
result["availableBalance"] = accountValue - totalMarginUsed // 可用余额
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
|
||||
|
||||
log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f",
|
||||
accountValue,
|
||||
result["totalWalletBalance"],
|
||||
result["availableBalance"],
|
||||
result["totalUnrealizedProfit"])
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetPositions 获取所有持仓
|
||||
func (t *HyperliquidTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
// 获取账户状态
|
||||
accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取持仓失败: %w", err)
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
|
||||
// 遍历所有持仓
|
||||
for _, assetPos := range accountState.AssetPositions {
|
||||
position := assetPos.Position
|
||||
|
||||
// 持仓数量(string类型)
|
||||
posAmt, _ := strconv.ParseFloat(position.Szi, 64)
|
||||
|
||||
if posAmt == 0 {
|
||||
continue // 跳过无持仓的
|
||||
}
|
||||
|
||||
posMap := make(map[string]interface{})
|
||||
|
||||
// 标准化symbol格式(Hyperliquid使用如"BTC",我们转换为"BTCUSDT")
|
||||
symbol := position.Coin + "USDT"
|
||||
posMap["symbol"] = symbol
|
||||
|
||||
// 持仓数量和方向
|
||||
if posAmt > 0 {
|
||||
posMap["side"] = "long"
|
||||
posMap["positionAmt"] = posAmt
|
||||
} else {
|
||||
posMap["side"] = "short"
|
||||
posMap["positionAmt"] = -posAmt // 转为正数
|
||||
}
|
||||
|
||||
// 价格信息(EntryPx和LiquidationPx是指针类型)
|
||||
var entryPrice, liquidationPx float64
|
||||
if position.EntryPx != nil {
|
||||
entryPrice, _ = strconv.ParseFloat(*position.EntryPx, 64)
|
||||
}
|
||||
if position.LiquidationPx != nil {
|
||||
liquidationPx, _ = strconv.ParseFloat(*position.LiquidationPx, 64)
|
||||
}
|
||||
|
||||
positionValue, _ := strconv.ParseFloat(position.PositionValue, 64)
|
||||
unrealizedPnl, _ := strconv.ParseFloat(position.UnrealizedPnl, 64)
|
||||
|
||||
// 计算mark price(positionValue / abs(posAmt))
|
||||
var markPrice float64
|
||||
if posAmt != 0 {
|
||||
markPrice = positionValue / absFloat(posAmt)
|
||||
}
|
||||
|
||||
posMap["entryPrice"] = entryPrice
|
||||
posMap["markPrice"] = markPrice
|
||||
posMap["unRealizedProfit"] = unrealizedPnl
|
||||
posMap["leverage"] = float64(position.Leverage.Value)
|
||||
posMap["liquidationPrice"] = liquidationPx
|
||||
|
||||
result = append(result, posMap)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetLeverage 设置杠杆
|
||||
func (t *HyperliquidTrader) SetLeverage(symbol string, leverage int) error {
|
||||
// Hyperliquid symbol格式(去掉USDT后缀)
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 调用UpdateLeverage (leverage int, name string, isCross bool)
|
||||
_, err := t.exchange.UpdateLeverage(t.ctx, leverage, coin, false) // false = 逐仓模式
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置杠杆失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenLong 开多仓
|
||||
func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先取消该币种的所有委托单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧委托单失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置杠杆
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Hyperliquid symbol格式
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取当前价格(用于市价单)
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 1.01)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice)
|
||||
|
||||
// 创建市价买入订单(使用IOC limit order with aggressive price)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: true,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: aggressivePrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Limit: &hyperliquid.LimitOrderType{
|
||||
Tif: hyperliquid.TifIoc, // Immediate or Cancel (类似市价单)
|
||||
},
|
||||
},
|
||||
ReduceOnly: false,
|
||||
}
|
||||
|
||||
_, err = t.exchange.Order(t.ctx, order, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("开多仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 开多仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = 0 // Hyperliquid没有返回order ID
|
||||
result["symbol"] = symbol
|
||||
result["status"] = "FILLED"
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// OpenShort 开空仓
|
||||
func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先取消该币种的所有委托单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧委托单失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置杠杆
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Hyperliquid symbol格式
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取当前价格
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 0.99)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice)
|
||||
|
||||
// 创建市价卖出订单
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: false,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: aggressivePrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Limit: &hyperliquid.LimitOrderType{
|
||||
Tif: hyperliquid.TifIoc,
|
||||
},
|
||||
},
|
||||
ReduceOnly: false,
|
||||
}
|
||||
|
||||
_, err = t.exchange.Order(t.ctx, order, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("开空仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 开空仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = 0
|
||||
result["symbol"] = symbol
|
||||
result["status"] = "FILLED"
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CloseLong 平多仓
|
||||
func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
// 如果数量为0,获取当前持仓数量
|
||||
if quantity == 0 {
|
||||
positions, err := t.GetPositions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if quantity == 0 {
|
||||
return nil, fmt.Errorf("没有找到 %s 的多仓", symbol)
|
||||
}
|
||||
}
|
||||
|
||||
// Hyperliquid symbol格式
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取当前价格
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 0.99)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice)
|
||||
|
||||
// 创建平仓订单(卖出 + ReduceOnly)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: false,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: aggressivePrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Limit: &hyperliquid.LimitOrderType{
|
||||
Tif: hyperliquid.TifIoc,
|
||||
},
|
||||
},
|
||||
ReduceOnly: true, // 只平仓,不开新仓
|
||||
}
|
||||
|
||||
_, err = t.exchange.Order(t.ctx, order, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("平多仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 平多仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
// 平仓后取消该币种的所有挂单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = 0
|
||||
result["symbol"] = symbol
|
||||
result["status"] = "FILLED"
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CloseShort 平空仓
|
||||
func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
// 如果数量为0,获取当前持仓数量
|
||||
if quantity == 0 {
|
||||
positions, err := t.GetPositions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if quantity == 0 {
|
||||
return nil, fmt.Errorf("没有找到 %s 的空仓", symbol)
|
||||
}
|
||||
}
|
||||
|
||||
// Hyperliquid symbol格式
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取当前价格
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 1.01)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice)
|
||||
|
||||
// 创建平仓订单(买入 + ReduceOnly)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: true,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: aggressivePrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Limit: &hyperliquid.LimitOrderType{
|
||||
Tif: hyperliquid.TifIoc,
|
||||
},
|
||||
},
|
||||
ReduceOnly: true,
|
||||
}
|
||||
|
||||
_, err = t.exchange.Order(t.ctx, order, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("平空仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 平空仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
// 平仓后取消该币种的所有挂单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = 0
|
||||
result["symbol"] = symbol
|
||||
result["status"] = "FILLED"
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CancelAllOrders 取消该币种的所有挂单
|
||||
func (t *HyperliquidTrader) CancelAllOrders(symbol string) error {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取所有挂单
|
||||
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取挂单失败: %w", err)
|
||||
}
|
||||
|
||||
// 取消该币种的所有挂单
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 已取消 %s 的所有挂单", symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMarketPrice 获取市场价格
|
||||
func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 获取所有市场价格
|
||||
allMids, err := t.exchange.Info().AllMids(t.ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("获取价格失败: %w", err)
|
||||
}
|
||||
|
||||
// 查找对应币种的价格(allMids是map[string]string)
|
||||
if priceStr, ok := allMids[coin]; ok {
|
||||
priceFloat, err := strconv.ParseFloat(priceStr, 64)
|
||||
if err == nil {
|
||||
return priceFloat, nil
|
||||
}
|
||||
return 0, fmt.Errorf("价格格式错误: %v", err)
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("未找到 %s 的价格", symbol)
|
||||
}
|
||||
|
||||
// SetStopLoss 设置止损单
|
||||
func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
isBuy := positionSide == "SHORT" // 空仓止损=买入,多仓止损=卖出
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
roundedStopPrice := t.roundPriceToSigfigs(stopPrice)
|
||||
|
||||
// 创建止损单(Trigger Order)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: isBuy,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: roundedStopPrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Trigger: &hyperliquid.TriggerOrderType{
|
||||
TriggerPx: roundedStopPrice,
|
||||
IsMarket: true,
|
||||
Tpsl: "sl", // stop loss
|
||||
},
|
||||
},
|
||||
ReduceOnly: true,
|
||||
}
|
||||
|
||||
_, err := t.exchange.Order(t.ctx, order, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置止损失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" 止损价设置: %.4f", roundedStopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTakeProfit 设置止盈单
|
||||
func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
isBuy := positionSide == "SHORT" // 空仓止盈=买入,多仓止盈=卖出
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
roundedTakeProfitPrice := t.roundPriceToSigfigs(takeProfitPrice)
|
||||
|
||||
// 创建止盈单(Trigger Order)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: isBuy,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: roundedTakeProfitPrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Trigger: &hyperliquid.TriggerOrderType{
|
||||
TriggerPx: roundedTakeProfitPrice,
|
||||
IsMarket: true,
|
||||
Tpsl: "tp", // take profit
|
||||
},
|
||||
},
|
||||
ReduceOnly: true,
|
||||
}
|
||||
|
||||
_, err := t.exchange.Order(t.ctx, order, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置止盈失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" 止盈价设置: %.4f", roundedTakeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatQuantity 格式化数量到正确的精度
|
||||
func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
szDecimals := t.getSzDecimals(coin)
|
||||
|
||||
// 使用szDecimals格式化数量
|
||||
formatStr := fmt.Sprintf("%%.%df", szDecimals)
|
||||
return fmt.Sprintf(formatStr, quantity), nil
|
||||
}
|
||||
|
||||
// getSzDecimals 获取币种的数量精度
|
||||
func (t *HyperliquidTrader) getSzDecimals(coin string) int {
|
||||
if t.meta == nil {
|
||||
log.Printf("⚠️ meta信息为空,使用默认精度4")
|
||||
return 4 // 默认精度
|
||||
}
|
||||
|
||||
// 在meta.Universe中查找对应的币种
|
||||
for _, asset := range t.meta.Universe {
|
||||
if asset.Name == coin {
|
||||
return asset.SzDecimals
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("⚠️ 未找到 %s 的精度信息,使用默认精度4", coin)
|
||||
return 4 // 默认精度
|
||||
}
|
||||
|
||||
// roundToSzDecimals 将数量四舍五入到正确的精度
|
||||
func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 {
|
||||
szDecimals := t.getSzDecimals(coin)
|
||||
|
||||
// 计算倍数(10^szDecimals)
|
||||
multiplier := 1.0
|
||||
for i := 0; i < szDecimals; i++ {
|
||||
multiplier *= 10.0
|
||||
}
|
||||
|
||||
// 四舍五入
|
||||
return float64(int(quantity*multiplier+0.5)) / multiplier
|
||||
}
|
||||
|
||||
// roundPriceToSigfigs 将价格四舍五入到5位有效数字
|
||||
// Hyperliquid要求价格使用5位有效数字(significant figures)
|
||||
func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
|
||||
if price == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
const sigfigs = 5 // Hyperliquid标准:5位有效数字
|
||||
|
||||
// 计算价格的数量级
|
||||
var magnitude float64
|
||||
if price < 0 {
|
||||
magnitude = -price
|
||||
} else {
|
||||
magnitude = price
|
||||
}
|
||||
|
||||
// 计算需要的倍数
|
||||
multiplier := 1.0
|
||||
for magnitude >= 10 {
|
||||
magnitude /= 10
|
||||
multiplier /= 10
|
||||
}
|
||||
for magnitude < 1 {
|
||||
magnitude *= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
|
||||
// 应用有效数字精度
|
||||
for i := 0; i < sigfigs-1; i++ {
|
||||
multiplier *= 10
|
||||
}
|
||||
|
||||
// 四舍五入
|
||||
rounded := float64(int(price*multiplier+0.5)) / multiplier
|
||||
return rounded
|
||||
}
|
||||
|
||||
// convertSymbolToHyperliquid 将标准symbol转换为Hyperliquid格式
|
||||
// 例如: "BTCUSDT" -> "BTC"
|
||||
func convertSymbolToHyperliquid(symbol string) string {
|
||||
// 去掉USDT后缀
|
||||
if len(symbol) > 4 && symbol[len(symbol)-4:] == "USDT" {
|
||||
return symbol[:len(symbol)-4]
|
||||
}
|
||||
return symbol
|
||||
}
|
||||
|
||||
// absFloat 返回浮点数的绝对值
|
||||
func absFloat(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
41
trader/interface.go
Normal file
41
trader/interface.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package trader
|
||||
|
||||
// Trader 交易器统一接口
|
||||
// 支持多个交易平台(币安、Hyperliquid等)
|
||||
type Trader interface {
|
||||
// GetBalance 获取账户余额
|
||||
GetBalance() (map[string]interface{}, error)
|
||||
|
||||
// GetPositions 获取所有持仓
|
||||
GetPositions() ([]map[string]interface{}, error)
|
||||
|
||||
// OpenLong 开多仓
|
||||
OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error)
|
||||
|
||||
// OpenShort 开空仓
|
||||
OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error)
|
||||
|
||||
// CloseLong 平多仓(quantity=0表示全部平仓)
|
||||
CloseLong(symbol string, quantity float64) (map[string]interface{}, error)
|
||||
|
||||
// CloseShort 平空仓(quantity=0表示全部平仓)
|
||||
CloseShort(symbol string, quantity float64) (map[string]interface{}, error)
|
||||
|
||||
// SetLeverage 设置杠杆
|
||||
SetLeverage(symbol string, leverage int) error
|
||||
|
||||
// GetMarketPrice 获取市场价格
|
||||
GetMarketPrice(symbol string) (float64, error)
|
||||
|
||||
// SetStopLoss 设置止损单
|
||||
SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error
|
||||
|
||||
// SetTakeProfit 设置止盈单
|
||||
SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error
|
||||
|
||||
// CancelAllOrders 取消该币种的所有挂单
|
||||
CancelAllOrders(symbol string) error
|
||||
|
||||
// FormatQuantity 格式化数量到正确的精度
|
||||
FormatQuantity(symbol string, quantity float64) (string, error)
|
||||
}
|
||||
Reference in New Issue
Block a user