mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +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的交易决策
|
||||
type Decision struct {
|
||||
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"`
|
||||
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
|
||||
StopLoss float64 `json:"stop_loss,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)
|
||||
RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险
|
||||
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 {
|
||||
// 验证action
|
||||
validActions := map[string]bool{
|
||||
"open_long": true,
|
||||
"open_short": true,
|
||||
"close_long": true,
|
||||
"close_short": true,
|
||||
"hold": true,
|
||||
"wait": true,
|
||||
"open_long": true,
|
||||
"open_short": true,
|
||||
"close_long": true,
|
||||
"close_short": true,
|
||||
"update_stop_loss": true,
|
||||
"update_take_profit": true,
|
||||
"partial_close": true,
|
||||
"hold": true,
|
||||
"wait": true,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -409,18 +409,24 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
quantity := openPos["quantity"].(float64)
|
||||
leverage := openPos["leverage"].(int)
|
||||
|
||||
// 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量
|
||||
actualQuantity := quantity
|
||||
if action.Action == "partial_close" {
|
||||
actualQuantity = action.Quantity
|
||||
}
|
||||
|
||||
// 计算实际盈亏(USDT)
|
||||
// 合约交易 PnL 计算:quantity × 价格差
|
||||
// 合约交易 PnL 计算:actualQuantity × 价格差
|
||||
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
|
||||
var pnl float64
|
||||
if side == "long" {
|
||||
pnl = quantity * (action.Price - openPrice)
|
||||
pnl = actualQuantity * (action.Price - openPrice)
|
||||
} else {
|
||||
pnl = quantity * (openPrice - action.Price)
|
||||
pnl = actualQuantity * (openPrice - action.Price)
|
||||
}
|
||||
|
||||
// 计算盈亏百分比(相对保证金)
|
||||
positionValue := quantity * openPrice
|
||||
positionValue := actualQuantity * openPrice
|
||||
marginUsed := positionValue / float64(leverage)
|
||||
pnlPct := 0.0
|
||||
if marginUsed > 0 {
|
||||
@@ -431,7 +437,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
outcome := TradeOutcome{
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Quantity: quantity,
|
||||
Quantity: actualQuantity,
|
||||
Leverage: leverage,
|
||||
OpenPrice: openPrice,
|
||||
ClosePrice: action.Price,
|
||||
|
||||
@@ -1005,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"
|
||||
@@ -593,6 +594,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
|
||||
@@ -771,6 +778,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
|
||||
@@ -984,12 +1186,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())
|
||||
|
||||
@@ -511,6 +511,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