mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 08:16:56 +08:00
fix(trader): separate stop-loss and take-profit order cancellation to prevent accidental deletions
## Problem When adjusting stop-loss or take-profit levels, `CancelStopOrders()` deleted BOTH stop-loss AND take-profit orders simultaneously, causing: - **Adjusting stop-loss** → Take-profit order deleted → Position has no exit plan ❌ - **Adjusting take-profit** → Stop-loss order deleted → Position unprotected ❌ **Root cause:** ```go CancelStopOrders(symbol) { // Cancelled ALL orders with type STOP_MARKET or TAKE_PROFIT_MARKET // No distinction between stop-loss and take-profit } ``` ## Solution ### 1. Added new interface methods (trader/interface.go) ```go CancelStopLossOrders(symbol string) error // Only cancel stop-loss orders CancelTakeProfitOrders(symbol string) error // Only cancel take-profit orders CancelStopOrders(symbol string) error // Deprecated (cancels both) ``` ### 2. Implemented for all 3 exchanges **Binance (trader/binance_futures.go)**: - `CancelStopLossOrders`: Filters `OrderTypeStopMarket | OrderTypeStop` - `CancelTakeProfitOrders`: Filters `OrderTypeTakeProfitMarket | OrderTypeTakeProfit` - Full order type differentiation ✅ **Hyperliquid (trader/hyperliquid_trader.go)**: - ⚠️ Limitation: SDK's OpenOrder struct doesn't expose trigger field - Both methods call `CancelStopOrders` (cancels all pending orders) - Trade-off: Safe but less precise **Aster (trader/aster_trader.go)**: - `CancelStopLossOrders`: Filters `STOP_MARKET | STOP` - `CancelTakeProfitOrders`: Filters `TAKE_PROFIT_MARKET | TAKE_PROFIT` - Full order type differentiation ✅ ### 3. Usage in auto_trader.go When `update_stop_loss` or `update_take_profit` actions are implemented, they will use: ```go // update_stop_loss: at.trader.CancelStopLossOrders(symbol) // Only cancel SL, keep TP at.trader.SetStopLoss(...) // update_take_profit: at.trader.CancelTakeProfitOrders(symbol) // Only cancel TP, keep SL at.trader.SetTakeProfit(...) ``` ## Impact - ✅ Adjusting stop-loss no longer deletes take-profit - ✅ Adjusting take-profit no longer deletes stop-loss - ✅ Backward compatible: `CancelStopOrders` still exists (deprecated) - ⚠️ Hyperliquid limitation: still cancels all orders (SDK constraint) ## Testing - ✅ Compiles successfully across all 3 exchanges - ⚠️ Requires live testing: - [ ] Binance: Adjust SL → verify TP remains - [ ] Binance: Adjust TP → verify SL remains - [ ] Hyperliquid: Verify behavior with limitation - [ ] Aster: Verify order filtering works correctly ## Code Changes ``` trader/interface.go: +9 lines (new interface methods) trader/binance_futures.go: +133 lines (3 new functions) trader/hyperliquid_trader.go: +56 lines (3 new functions) trader/aster_trader.go: +157 lines (3 new functions) Total: +355 lines ```
This commit is contained in:
@@ -971,6 +971,161 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity
|
||||
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
|
||||
}
|
||||
|
||||
// CancelStopLossOrders 仅取消止损单(不影响止盈单)
|
||||
func (t *AsterTrader) CancelStopLossOrders(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 == "STOP" {
|
||||
orderID, _ := order["orderId"].(float64)
|
||||
cancelParams := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
"orderId": int64(orderID),
|
||||
}
|
||||
|
||||
_, err := t.request("DELETE", "/fapi/v1/order", cancelParams)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消止损单 %d 失败: %v", int64(orderID), err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s)", int64(orderID), orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止损单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelTakeProfitOrders 仅取消止盈单(不影响止损单)
|
||||
func (t *AsterTrader) CancelTakeProfitOrders(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 == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" {
|
||||
orderID, _ := order["orderId"].(float64)
|
||||
cancelParams := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
"orderId": int64(orderID),
|
||||
}
|
||||
|
||||
_, err := t.request("DELETE", "/fapi/v1/order", cancelParams)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消止盈单 %d 失败: %v", int64(orderID), err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s)", int64(orderID), orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelAllOrders 取消所有订单
|
||||
func (t *AsterTrader) CancelAllOrders(symbol string) error {
|
||||
params := map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user