mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 07:16:56 +08:00
Merge pull request #415 from zhouyongyou/feat/partial-close-core-v2
feat: 部分平倉和動態止盈止損核心實現 / Partial Close & Dynamic TP/SL Core
This commit is contained in:
@@ -71,11 +71,20 @@ type Context struct {
|
|||||||
// Decision AI的交易决策
|
// Decision AI的交易决策
|
||||||
type Decision struct {
|
type Decision struct {
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait"
|
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait"
|
||||||
|
|
||||||
|
// 开仓参数
|
||||||
Leverage int `json:"leverage,omitempty"`
|
Leverage int `json:"leverage,omitempty"`
|
||||||
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
|
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
|
||||||
StopLoss float64 `json:"stop_loss,omitempty"`
|
StopLoss float64 `json:"stop_loss,omitempty"`
|
||||||
TakeProfit float64 `json:"take_profit,omitempty"`
|
TakeProfit float64 `json:"take_profit,omitempty"`
|
||||||
|
|
||||||
|
// 调整参数(新增)
|
||||||
|
NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss
|
||||||
|
NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit
|
||||||
|
ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100)
|
||||||
|
|
||||||
|
// 通用参数
|
||||||
Confidence int `json:"confidence,omitempty"` // 信心度 (0-100)
|
Confidence int `json:"confidence,omitempty"` // 信心度 (0-100)
|
||||||
RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险
|
RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险
|
||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
@@ -504,12 +513,15 @@ func findMatchingBracket(s string, start int) int {
|
|||||||
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||||
// 验证action
|
// 验证action
|
||||||
validActions := map[string]bool{
|
validActions := map[string]bool{
|
||||||
"open_long": true,
|
"open_long": true,
|
||||||
"open_short": true,
|
"open_short": true,
|
||||||
"close_long": true,
|
"close_long": true,
|
||||||
"close_short": true,
|
"close_short": true,
|
||||||
"hold": true,
|
"update_stop_loss": true,
|
||||||
"wait": true,
|
"update_take_profit": true,
|
||||||
|
"partial_close": true,
|
||||||
|
"hold": true,
|
||||||
|
"wait": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !validActions[d.Action] {
|
if !validActions[d.Action] {
|
||||||
@@ -589,5 +601,26 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 动态调整止损验证
|
||||||
|
if d.Action == "update_stop_loss" {
|
||||||
|
if d.NewStopLoss <= 0 {
|
||||||
|
return fmt.Errorf("新止损价格必须大于0: %.2f", d.NewStopLoss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态调整止盈验证
|
||||||
|
if d.Action == "update_take_profit" {
|
||||||
|
if d.NewTakeProfit <= 0 {
|
||||||
|
return fmt.Errorf("新止盈价格必须大于0: %.2f", d.NewTakeProfit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部分平仓验证
|
||||||
|
if d.Action == "partial_close" {
|
||||||
|
if d.ClosePercentage <= 0 || d.ClosePercentage > 100 {
|
||||||
|
return fmt.Errorf("平仓百分比必须在0-100之间: %.1f", d.ClosePercentage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -409,18 +409,24 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
|||||||
quantity := openPos["quantity"].(float64)
|
quantity := openPos["quantity"].(float64)
|
||||||
leverage := openPos["leverage"].(int)
|
leverage := openPos["leverage"].(int)
|
||||||
|
|
||||||
|
// 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量
|
||||||
|
actualQuantity := quantity
|
||||||
|
if action.Action == "partial_close" {
|
||||||
|
actualQuantity = action.Quantity
|
||||||
|
}
|
||||||
|
|
||||||
// 计算实际盈亏(USDT)
|
// 计算实际盈亏(USDT)
|
||||||
// 合约交易 PnL 计算:quantity × 价格差
|
// 合约交易 PnL 计算:actualQuantity × 价格差
|
||||||
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
|
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
|
||||||
var pnl float64
|
var pnl float64
|
||||||
if side == "long" {
|
if side == "long" {
|
||||||
pnl = quantity * (action.Price - openPrice)
|
pnl = actualQuantity * (action.Price - openPrice)
|
||||||
} else {
|
} else {
|
||||||
pnl = quantity * (openPrice - action.Price)
|
pnl = actualQuantity * (openPrice - action.Price)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算盈亏百分比(相对保证金)
|
// 计算盈亏百分比(相对保证金)
|
||||||
positionValue := quantity * openPrice
|
positionValue := actualQuantity * openPrice
|
||||||
marginUsed := positionValue / float64(leverage)
|
marginUsed := positionValue / float64(leverage)
|
||||||
pnlPct := 0.0
|
pnlPct := 0.0
|
||||||
if marginUsed > 0 {
|
if marginUsed > 0 {
|
||||||
@@ -431,7 +437,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
|||||||
outcome := TradeOutcome{
|
outcome := TradeOutcome{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Side: side,
|
Side: side,
|
||||||
Quantity: quantity,
|
Quantity: actualQuantity,
|
||||||
Leverage: leverage,
|
Leverage: leverage,
|
||||||
OpenPrice: openPrice,
|
OpenPrice: openPrice,
|
||||||
ClosePrice: action.Price,
|
ClosePrice: action.Price,
|
||||||
|
|||||||
@@ -1005,6 +1005,61 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error {
|
|||||||
return err
|
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接口)
|
// FormatQuantity 格式化数量(实现Trader接口)
|
||||||
func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
||||||
formatted, err := t.formatQuantity(symbol, quantity)
|
formatted, err := t.formatQuantity(symbol, quantity)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"nofx/decision"
|
"nofx/decision"
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
@@ -593,6 +594,12 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, act
|
|||||||
return at.executeCloseLongWithRecord(decision, actionRecord)
|
return at.executeCloseLongWithRecord(decision, actionRecord)
|
||||||
case "close_short":
|
case "close_short":
|
||||||
return at.executeCloseShortWithRecord(decision, actionRecord)
|
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":
|
case "hold", "wait":
|
||||||
// 无需执行,仅记录
|
// 无需执行,仅记录
|
||||||
return nil
|
return nil
|
||||||
@@ -771,6 +778,201 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, a
|
|||||||
return nil
|
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
|
// GetID 获取trader ID
|
||||||
func (at *AutoTrader) GetID() string {
|
func (at *AutoTrader) GetID() string {
|
||||||
return at.id
|
return at.id
|
||||||
@@ -984,12 +1186,14 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision
|
|||||||
// 定义优先级
|
// 定义优先级
|
||||||
getActionPriority := func(action string) int {
|
getActionPriority := func(action string) int {
|
||||||
switch action {
|
switch action {
|
||||||
case "close_long", "close_short":
|
case "close_long", "close_short", "partial_close":
|
||||||
return 1 // 最高优先级:先平仓
|
return 1 // 最高优先级:先平仓(包括部分平仓)
|
||||||
|
case "update_stop_loss", "update_take_profit":
|
||||||
|
return 2 // 调整持仓止盈止损
|
||||||
case "open_long", "open_short":
|
case "open_long", "open_short":
|
||||||
return 2 // 次优先级:后开仓
|
return 3 // 次优先级:后开仓
|
||||||
case "hold", "wait":
|
case "hold", "wait":
|
||||||
return 3 // 最低优先级:观望
|
return 4 // 最低优先级:观望
|
||||||
default:
|
default:
|
||||||
return 999 // 未知动作放最后
|
return 999 // 未知动作放最后
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,6 +425,53 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error {
|
|||||||
return nil
|
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 获取市场价格
|
// GetMarketPrice 获取市场价格
|
||||||
func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) {
|
func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||||
prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background())
|
prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background())
|
||||||
|
|||||||
@@ -511,6 +511,40 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error {
|
|||||||
return nil
|
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 获取市场价格
|
// GetMarketPrice 获取市场价格
|
||||||
func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) {
|
func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||||
coin := convertSymbolToHyperliquid(symbol)
|
coin := convertSymbolToHyperliquid(symbol)
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ type Trader interface {
|
|||||||
// CancelAllOrders 取消该币种的所有挂单
|
// CancelAllOrders 取消该币种的所有挂单
|
||||||
CancelAllOrders(symbol string) error
|
CancelAllOrders(symbol string) error
|
||||||
|
|
||||||
|
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
|
||||||
|
CancelStopOrders(symbol string) error
|
||||||
|
|
||||||
// FormatQuantity 格式化数量到正确的精度
|
// FormatQuantity 格式化数量到正确的精度
|
||||||
FormatQuantity(symbol string, quantity float64) (string, error)
|
FormatQuantity(symbol string, quantity float64) (string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user