mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-01 18:11:20 +08:00
fix: 修復5個關鍵交易bug(保證金計算、部分平倉統計、止損止盈分離、雙向持倉)
## 修復內容 ### 1. 保證金計算錯誤(Critical) - 修正提示詞中的保證金公式(adaptive.txt, nof1.txt, default.txt) - 新增代碼級保證金驗證(auto_trader.go) - 防止開倉時保證金不足錯誤(code=-2019) ### 2. 部分平倉統計錯誤(Medium) - 修改統計邏輯:多次 partial_close 聚合為一筆交易 - 新增追蹤字段:remainingQuantity, accumulatedPnL - 只在完全平倉時計入 TotalTrades++ ### 3. 前端配置覆蓋問題(Medium) - 修正 TraderConfigModal.tsx 條件判斷 - 防止空字符串覆蓋用戶選擇的提示詞 ### 4/5. 動態止損/止盈刪除配對訂單(Critical) - 新增接口:CancelStopLossOrders, CancelTakeProfitOrders - 分離訂單取消邏輯(Binance, Hyperliquid, Aster) - 調整止損時不刪除止盈,反之亦然 ### 7. 雙向持倉模式初始化(Critical) - 新增 setDualSidePosition() 函數 - 在 NewFuturesTrader() 中初始化 Hedge Mode - 防止 code=-4061 錯誤(PositionSide 參數錯誤) ## 影響範圍 - 修改文件:10個 - 新增代碼:+480行 - 刪除代碼:-71行 ## 測試狀態 - ✅ 編譯通過(go build ./...) - ✅ 語法檢查通過 - ⚠️ 需要在測試環境運行驗證實際交易效果
This commit is contained in:
@@ -243,8 +243,10 @@ func (l *DecisionLogger) GetStatistics() (*Statistics, error) {
|
||||
switch action.Action {
|
||||
case "open_long", "open_short":
|
||||
stats.TotalOpenPositions++
|
||||
case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short":
|
||||
case "close_long", "close_short", "auto_close_long", "auto_close_short":
|
||||
stats.TotalClosePositions++
|
||||
// 🔧 BUG FIX:partial_close 不計入 TotalClosePositions,避免重複計數
|
||||
// case "partial_close": // 不計數,因為只有完全平倉才算一次
|
||||
// update_stop_loss 和 update_take_profit 不計入統計
|
||||
}
|
||||
}
|
||||
@@ -418,11 +420,15 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
case "open_long", "open_short":
|
||||
// 更新开仓记录(可能已经在预填充时记录过了)
|
||||
openPositions[posKey] = map[string]interface{}{
|
||||
"side": side,
|
||||
"openPrice": action.Price,
|
||||
"openTime": action.Timestamp,
|
||||
"quantity": action.Quantity,
|
||||
"leverage": action.Leverage,
|
||||
"side": side,
|
||||
"openPrice": action.Price,
|
||||
"openTime": action.Timestamp,
|
||||
"quantity": action.Quantity,
|
||||
"leverage": action.Leverage,
|
||||
"remainingQuantity": action.Quantity, // 🔧 BUG FIX:追蹤剩餘數量
|
||||
"accumulatedPnL": 0.0, // 🔧 BUG FIX:累積部分平倉盈虧
|
||||
"partialCloseCount": 0, // 🔧 BUG FIX:部分平倉次數
|
||||
"partialCloseVolume": 0.0, // 🔧 BUG FIX:部分平倉總量
|
||||
}
|
||||
|
||||
case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short":
|
||||
@@ -434,15 +440,22 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
quantity := openPos["quantity"].(float64)
|
||||
leverage := openPos["leverage"].(int)
|
||||
|
||||
// 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量
|
||||
actualQuantity := quantity
|
||||
// 🔧 BUG FIX:取得追蹤字段(若不存在則初始化)
|
||||
remainingQty, _ := openPos["remainingQuantity"].(float64)
|
||||
if remainingQty == 0 {
|
||||
remainingQty = quantity // 兼容舊數據(沒有 remainingQuantity 字段)
|
||||
}
|
||||
accumulatedPnL, _ := openPos["accumulatedPnL"].(float64)
|
||||
partialCloseCount, _ := openPos["partialCloseCount"].(int)
|
||||
partialCloseVolume, _ := openPos["partialCloseVolume"].(float64)
|
||||
|
||||
// 对于 partial_close,使用实际平仓数量;否则使用剩余仓位数量
|
||||
actualQuantity := remainingQty
|
||||
if action.Action == "partial_close" {
|
||||
actualQuantity = action.Quantity
|
||||
}
|
||||
|
||||
// 计算实际盈亏(USDT)
|
||||
// 合约交易 PnL 计算:actualQuantity × 价格差
|
||||
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
|
||||
// 计算本次平仓的盈亏(USDT)
|
||||
var pnl float64
|
||||
if side == "long" {
|
||||
pnl = actualQuantity * (action.Price - openPrice)
|
||||
@@ -450,61 +463,134 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
pnl = actualQuantity * (openPrice - action.Price)
|
||||
}
|
||||
|
||||
// 计算盈亏百分比(相对保证金)
|
||||
positionValue := actualQuantity * openPrice
|
||||
marginUsed := positionValue / float64(leverage)
|
||||
pnlPct := 0.0
|
||||
if marginUsed > 0 {
|
||||
pnlPct = (pnl / marginUsed) * 100
|
||||
}
|
||||
// 🔧 BUG FIX:處理 partial_close 聚合邏輯
|
||||
if action.Action == "partial_close" {
|
||||
// 累積盈虧和數量
|
||||
accumulatedPnL += pnl
|
||||
remainingQty -= actualQuantity
|
||||
partialCloseCount++
|
||||
partialCloseVolume += actualQuantity
|
||||
|
||||
// 记录交易结果
|
||||
outcome := TradeOutcome{
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Quantity: actualQuantity,
|
||||
Leverage: leverage,
|
||||
OpenPrice: openPrice,
|
||||
ClosePrice: action.Price,
|
||||
PositionValue: positionValue,
|
||||
MarginUsed: marginUsed,
|
||||
PnL: pnl,
|
||||
PnLPct: pnlPct,
|
||||
Duration: action.Timestamp.Sub(openTime).String(),
|
||||
OpenTime: openTime,
|
||||
CloseTime: action.Timestamp,
|
||||
}
|
||||
// 更新 openPositions(保留持倉記錄,但更新追蹤數據)
|
||||
openPos["remainingQuantity"] = remainingQty
|
||||
openPos["accumulatedPnL"] = accumulatedPnL
|
||||
openPos["partialCloseCount"] = partialCloseCount
|
||||
openPos["partialCloseVolume"] = partialCloseVolume
|
||||
|
||||
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
|
||||
analysis.TotalTrades++
|
||||
// 判斷是否已完全平倉
|
||||
if remainingQty <= 0.0001 { // 使用小閾值避免浮點誤差
|
||||
// ✅ 完全平倉:記錄為一筆完整交易
|
||||
positionValue := quantity * openPrice
|
||||
marginUsed := positionValue / float64(leverage)
|
||||
pnlPct := 0.0
|
||||
if marginUsed > 0 {
|
||||
pnlPct = (accumulatedPnL / marginUsed) * 100
|
||||
}
|
||||
|
||||
// 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损)
|
||||
if pnl > 0 {
|
||||
analysis.WinningTrades++
|
||||
analysis.AvgWin += pnl
|
||||
} else if pnl < 0 {
|
||||
analysis.LosingTrades++
|
||||
analysis.AvgLoss += pnl
|
||||
}
|
||||
// pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数
|
||||
outcome := TradeOutcome{
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Quantity: quantity, // 使用原始總量
|
||||
Leverage: leverage,
|
||||
OpenPrice: openPrice,
|
||||
ClosePrice: action.Price, // 最後一次平倉價格
|
||||
PositionValue: positionValue,
|
||||
MarginUsed: marginUsed,
|
||||
PnL: accumulatedPnL, // 🔧 使用累積盈虧
|
||||
PnLPct: pnlPct,
|
||||
Duration: action.Timestamp.Sub(openTime).String(),
|
||||
OpenTime: openTime,
|
||||
CloseTime: action.Timestamp,
|
||||
}
|
||||
|
||||
// 更新币种统计
|
||||
if _, exists := analysis.SymbolStats[symbol]; !exists {
|
||||
analysis.SymbolStats[symbol] = &SymbolPerformance{
|
||||
Symbol: symbol,
|
||||
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
|
||||
analysis.TotalTrades++ // 🔧 只在完全平倉時計數
|
||||
|
||||
// 分类交易
|
||||
if accumulatedPnL > 0 {
|
||||
analysis.WinningTrades++
|
||||
analysis.AvgWin += accumulatedPnL
|
||||
} else if accumulatedPnL < 0 {
|
||||
analysis.LosingTrades++
|
||||
analysis.AvgLoss += accumulatedPnL
|
||||
}
|
||||
|
||||
// 更新币种统计
|
||||
if _, exists := analysis.SymbolStats[symbol]; !exists {
|
||||
analysis.SymbolStats[symbol] = &SymbolPerformance{
|
||||
Symbol: symbol,
|
||||
}
|
||||
}
|
||||
stats := analysis.SymbolStats[symbol]
|
||||
stats.TotalTrades++
|
||||
stats.TotalPnL += accumulatedPnL
|
||||
if accumulatedPnL > 0 {
|
||||
stats.WinningTrades++
|
||||
} else if accumulatedPnL < 0 {
|
||||
stats.LosingTrades++
|
||||
}
|
||||
|
||||
// 刪除持倉記錄
|
||||
delete(openPositions, posKey)
|
||||
}
|
||||
}
|
||||
stats := analysis.SymbolStats[symbol]
|
||||
stats.TotalTrades++
|
||||
stats.TotalPnL += pnl
|
||||
if pnl > 0 {
|
||||
stats.WinningTrades++
|
||||
} else if pnl < 0 {
|
||||
stats.LosingTrades++
|
||||
}
|
||||
// ⚠️ 否則不做任何操作(等待後續 partial_close 或 full close)
|
||||
|
||||
// 移除已平仓记录(partial_close 不刪除,因為還有剩餘倉位)
|
||||
if action.Action != "partial_close" {
|
||||
} else {
|
||||
// 🔧 完全平倉(close_long/close_short/auto_close)
|
||||
// 如果之前有部分平倉,需要加上累積的 PnL
|
||||
totalPnL := accumulatedPnL + pnl
|
||||
|
||||
positionValue := quantity * openPrice
|
||||
marginUsed := positionValue / float64(leverage)
|
||||
pnlPct := 0.0
|
||||
if marginUsed > 0 {
|
||||
pnlPct = (totalPnL / marginUsed) * 100
|
||||
}
|
||||
|
||||
outcome := TradeOutcome{
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Quantity: quantity, // 使用原始總量
|
||||
Leverage: leverage,
|
||||
OpenPrice: openPrice,
|
||||
ClosePrice: action.Price,
|
||||
PositionValue: positionValue,
|
||||
MarginUsed: marginUsed,
|
||||
PnL: totalPnL, // 🔧 包含之前部分平倉的 PnL
|
||||
PnLPct: pnlPct,
|
||||
Duration: action.Timestamp.Sub(openTime).String(),
|
||||
OpenTime: openTime,
|
||||
CloseTime: action.Timestamp,
|
||||
}
|
||||
|
||||
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
|
||||
analysis.TotalTrades++
|
||||
|
||||
// 分类交易
|
||||
if totalPnL > 0 {
|
||||
analysis.WinningTrades++
|
||||
analysis.AvgWin += totalPnL
|
||||
} else if totalPnL < 0 {
|
||||
analysis.LosingTrades++
|
||||
analysis.AvgLoss += totalPnL
|
||||
}
|
||||
|
||||
// 更新币种统计
|
||||
if _, exists := analysis.SymbolStats[symbol]; !exists {
|
||||
analysis.SymbolStats[symbol] = &SymbolPerformance{
|
||||
Symbol: symbol,
|
||||
}
|
||||
}
|
||||
stats := analysis.SymbolStats[symbol]
|
||||
stats.TotalTrades++
|
||||
stats.TotalPnL += totalPnL
|
||||
if totalPnL > 0 {
|
||||
stats.WinningTrades++
|
||||
} else if totalPnL < 0 {
|
||||
stats.LosingTrades++
|
||||
}
|
||||
|
||||
// 刪除持倉記錄
|
||||
delete(openPositions, posKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,18 @@
|
||||
|
||||
## 仓位计算公式
|
||||
|
||||
**Position Size (USD) = Available Cash × Leverage × Allocation %**
|
||||
**Position Size (Coins) = Position Size (USD) / Current Price**
|
||||
**重要**:position_size_usd 是**名义价值**(包含杠杆),非保证金需求。
|
||||
|
||||
**计算步骤**:
|
||||
1. **可用保证金** = Available Cash × 0.95 × Allocation %(预留5%给手续费)
|
||||
2. **名义价值** = 可用保证金 × Leverage
|
||||
3. **position_size_usd** = 名义价值(这是 JSON 中应填写的值)
|
||||
4. **Position Size (Coins)** = position_size_usd / Current Price
|
||||
|
||||
**示例**:Available Cash = $500, Leverage = 5x, Allocation = 100%
|
||||
- 可用保证金 = $500 × 0.95 × 100% = $475
|
||||
- position_size_usd = $475 × 5 = **$2,375** ← JSON 中填写此值
|
||||
- 实际占用保证金 = $475,剩余 $25 用于手续费
|
||||
|
||||
## 杠杆选择指引
|
||||
|
||||
|
||||
@@ -142,6 +142,20 @@ confidence=0-100
|
||||
- **部分平仓** (`partial_close`):需填写 `close_percentage`(1-100),说明目的(如锁定利润)。
|
||||
- **观望或持有** (`wait/hold`):`reasoning` 必须说明观望或继续持有的原因(例如信号不足、冷却中、趋势未变)。
|
||||
|
||||
### 仓位大小计算
|
||||
**重要**:`position_size_usd` 是**名义价值**(包含杠杆),非保证金需求。
|
||||
|
||||
**计算步骤**:
|
||||
1. **可用保证金** = Available Cash × 0.95 × 配置比例(预留5%手续费)
|
||||
2. **名义价值** = 可用保证金 × Leverage
|
||||
3. **position_size_usd** = 名义价值(JSON中填写此值)
|
||||
4. **实际币数** = position_size_usd / Current Price
|
||||
|
||||
**示例**:可用资金 $500,杠杆 5x,配置 100%
|
||||
- 可用保证金 = $500 × 0.95 = $475
|
||||
- position_size_usd = $475 × 5 = **$2,375** ← JSON填此值
|
||||
- 实际占用保证金 = $475,剩余 $25 用于手续费
|
||||
|
||||
---
|
||||
|
||||
记住:
|
||||
|
||||
@@ -114,10 +114,19 @@ stop tightened to $2,950 (break-even). Exit fully if 4h MACD crosses down."
|
||||
|
||||
# POSITION SIZING FRAMEWORK
|
||||
|
||||
Calculate position size using this formula:
|
||||
**IMPORTANT**: `position_size_usd` is the **notional value** (includes leverage), NOT margin requirement.
|
||||
|
||||
Position Size (USD) = Available Cash × Leverage × Allocation %
|
||||
Position Size (Coins) = Position Size (USD) / Current Price
|
||||
## Calculation Steps:
|
||||
|
||||
1. **Available Margin** = Available Cash × 0.95 × Allocation % (reserve 5% for fees)
|
||||
2. **Notional Value** = Available Margin × Leverage
|
||||
3. **position_size_usd** = Notional Value (this is the value for JSON)
|
||||
4. **Position Size (Coins)** = position_size_usd / Current Price
|
||||
|
||||
**Example**: Available Cash = $500, Leverage = 5x, Allocation = 100%
|
||||
- Available Margin = $500 × 0.95 × 100% = $475
|
||||
- position_size_usd = $475 × 5 = **$2,375** ← Fill this value in JSON
|
||||
- Actual margin used = $475, remaining $25 for fees
|
||||
|
||||
## Sizing Considerations
|
||||
|
||||
|
||||
@@ -1036,6 +1036,106 @@ func (t *AsterTrader) CancelStopOrders(symbol string) error {
|
||||
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
|
||||
}
|
||||
|
||||
// FormatQuantity 格式化数量(实现Trader接口)
|
||||
func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
||||
formatted, err := t.formatQuantity(symbol, quantity)
|
||||
|
||||
@@ -758,6 +758,32 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, act
|
||||
return err
|
||||
}
|
||||
|
||||
// ⚠️ 保证金验证:防止保证金不足错误(code=-2019)
|
||||
// position_size_usd 是名义价值(包含杠杆),实际需要的保证金 = position_size_usd / leverage
|
||||
requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage)
|
||||
|
||||
// 获取当前可用余额
|
||||
balance, err := at.trader.GetBalance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取账户余额失败: %w", err)
|
||||
}
|
||||
availableBalance := 0.0
|
||||
if avail, ok := balance["availableBalance"].(float64); ok {
|
||||
availableBalance = avail
|
||||
}
|
||||
|
||||
// 手续费估算(Taker费率 0.04%)
|
||||
estimatedFee := decision.PositionSizeUSD * 0.0004
|
||||
totalRequired := requiredMargin + estimatedFee
|
||||
|
||||
// 验证保证金充足(需要保证金 + 手续费 <= 可用余额)
|
||||
if totalRequired > availableBalance {
|
||||
return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT。建议降低仓位或杠杆",
|
||||
totalRequired, requiredMargin, estimatedFee, availableBalance)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 保证金检查通过: 需要 %.2f USDT,可用 %.2f USDT", totalRequired, availableBalance)
|
||||
|
||||
// 计算数量
|
||||
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
|
||||
actionRecord.Quantity = quantity
|
||||
@@ -817,6 +843,32 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, ac
|
||||
return err
|
||||
}
|
||||
|
||||
// ⚠️ 保证金验证:防止保证金不足错误(code=-2019)
|
||||
// position_size_usd 是名义价值(包含杠杆),实际需要的保证金 = position_size_usd / leverage
|
||||
requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage)
|
||||
|
||||
// 获取当前可用余额
|
||||
balance, err := at.trader.GetBalance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取账户余额失败: %w", err)
|
||||
}
|
||||
availableBalance := 0.0
|
||||
if avail, ok := balance["availableBalance"].(float64); ok {
|
||||
availableBalance = avail
|
||||
}
|
||||
|
||||
// 手续费估算(Taker费率 0.04%)
|
||||
estimatedFee := decision.PositionSizeUSD * 0.0004
|
||||
totalRequired := requiredMargin + estimatedFee
|
||||
|
||||
// 验证保证金充足(需要保证金 + 手续费 <= 可用余额)
|
||||
if totalRequired > availableBalance {
|
||||
return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT。建议降低仓位或杠杆",
|
||||
totalRequired, requiredMargin, estimatedFee, availableBalance)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 保证金检查通过: 需要 %.2f USDT,可用 %.2f USDT", totalRequired, availableBalance)
|
||||
|
||||
// 计算数量
|
||||
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
|
||||
actionRecord.Quantity = quantity
|
||||
@@ -953,8 +1005,8 @@ func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decisio
|
||||
return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
|
||||
}
|
||||
|
||||
// 取消旧的止损单(避免多个止损单共存)
|
||||
if err := at.trader.CancelStopOrders(decision.Symbol); err != nil {
|
||||
// 取消旧的止损单(只删除止损单,不影响止盈单)
|
||||
if err := at.trader.CancelStopLossOrders(decision.Symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧止损单失败: %v", err)
|
||||
// 不中断执行,继续设置新止损
|
||||
}
|
||||
@@ -1015,8 +1067,8 @@ func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decis
|
||||
return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
|
||||
}
|
||||
|
||||
// 取消旧的止盈单(避免多个止盈单共存)
|
||||
if err := at.trader.CancelStopOrders(decision.Symbol); err != nil {
|
||||
// 取消旧的止盈单(只删除止盈单,不影响止损单)
|
||||
if err := at.trader.CancelTakeProfitOrders(decision.Symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧止盈单失败: %v", err)
|
||||
// 不中断执行,继续设置新止盈
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@ func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader {
|
||||
log.Printf("⚠️ 初始化同步币安服务器时间失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置双向持仓模式(Hedge Mode)
|
||||
// 这是必需的,因为代码中使用了 PositionSide (LONG/SHORT)
|
||||
if err := trader.setDualSidePosition(); err != nil {
|
||||
log.Printf("⚠️ 设置双向持仓模式失败: %v (如果已是双向模式则忽略此警告)", err)
|
||||
}
|
||||
|
||||
return trader
|
||||
}
|
||||
|
||||
@@ -246,6 +252,30 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDualSidePosition 设置双向持仓模式(初始化时调用)
|
||||
func (t *FuturesTrader) setDualSidePosition() error {
|
||||
// 尝试设置双向持仓模式
|
||||
err := t.callWithTimeSync("设置双向持仓模式", func() error {
|
||||
return t.client.NewChangePositionModeService().
|
||||
DualSide(true). // true = 双向持仓(Hedge Mode)
|
||||
Do(context.Background())
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// 如果错误信息包含"No need to change",说明已经是双向持仓模式
|
||||
if contains(err.Error(), "No need to change position side") {
|
||||
log.Printf(" ✓ 账户已是双向持仓模式(Hedge Mode)")
|
||||
return nil
|
||||
}
|
||||
// 其他错误则返回(但在调用方不会中断初始化)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 账户已切换为双向持仓模式(Hedge Mode)")
|
||||
log.Printf(" ℹ️ 双向持仓模式允许同时持有多单和空单")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLeverage 设置杠杆(智能判断+冷却期)
|
||||
func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
|
||||
// 先尝试获取当前杠杆(从持仓信息)
|
||||
@@ -572,6 +602,90 @@ func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopLossOrders 仅取消止损单(不影响止盈单)
|
||||
func (t *FuturesTrader) CancelStopLossOrders(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.OrderTypeStop {
|
||||
_, 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(" ✓ 已取消止损单 (订单ID: %d, 类型: %s)", order.OrderID, orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止损单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelTakeProfitOrders 仅取消止盈单(不影响止损单)
|
||||
func (t *FuturesTrader) CancelTakeProfitOrders(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.OrderTypeTakeProfitMarket || 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(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s)", 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())
|
||||
|
||||
@@ -535,6 +535,22 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopLossOrders 仅取消止损单(Hyperliquid 暂无法区分止损和止盈,取消所有)
|
||||
func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error {
|
||||
// Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段
|
||||
// 无法区分止损和止盈单,因此取消该币种的所有挂单
|
||||
log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
// CancelTakeProfitOrders 仅取消止盈单(Hyperliquid 暂无法区分止损和止盈,取消所有)
|
||||
func (t *HyperliquidTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
// Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段
|
||||
// 无法区分止损和止盈单,因此取消该币种的所有挂单
|
||||
log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
// GetMarketPrice 获取市场价格
|
||||
func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
@@ -39,9 +39,16 @@ type Trader interface {
|
||||
// CancelAllOrders 取消该币种的所有挂单
|
||||
CancelAllOrders(symbol string) error
|
||||
|
||||
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
|
||||
// CancelStopOrders 取消该币种的止盈/止损单(已废弃:会同时删除止损和止盈)
|
||||
// 请使用 CancelStopLossOrders 或 CancelTakeProfitOrders
|
||||
CancelStopOrders(symbol string) error
|
||||
|
||||
// CancelStopLossOrders 仅取消止损单(修复 BUG:调整止损时不删除止盈)
|
||||
CancelStopLossOrders(symbol string) error
|
||||
|
||||
// CancelTakeProfitOrders 仅取消止盈单(修复 BUG:调整止盈时不删除止损)
|
||||
CancelTakeProfitOrders(symbol string) error
|
||||
|
||||
// FormatQuantity 格式化数量到正确的精度
|
||||
FormatQuantity(symbol string, quantity float64) (string, error)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,8 @@ export function TraderConfigModal({
|
||||
});
|
||||
}
|
||||
// 确保旧数据也有默认的 system_prompt_template
|
||||
if (traderData && !traderData.system_prompt_template) {
|
||||
// 修复BUG:只处理 undefined,不覆盖已有值(包括空字符串)
|
||||
if (traderData && traderData.system_prompt_template === undefined) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
system_prompt_template: 'default'
|
||||
|
||||
Reference in New Issue
Block a user