Skip to content

Commit 5187894

Browse files
authored
Merge pull request NoFxAiOS#415 from zhouyongyou/feat/partial-close-core-v2
feat: 部分平倉和動態止盈止損核心實現 / Partial Close & Dynamic TP/SL Core
2 parents a8a9cdf + 52eaec4 commit 5187894

File tree

7 files changed

+398
-16
lines changed

7 files changed

+398
-16
lines changed

decision/engine.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,20 @@ type Context struct {
7171
// Decision AI的交易决策
7272
type Decision struct {
7373
Symbol string `json:"symbol"`
74-
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait"
74+
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait"
75+
76+
// 开仓参数
7577
Leverage int `json:"leverage,omitempty"`
7678
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
7779
StopLoss float64 `json:"stop_loss,omitempty"`
7880
TakeProfit float64 `json:"take_profit,omitempty"`
81+
82+
// 调整参数(新增)
83+
NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss
84+
NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit
85+
ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100)
86+
87+
// 通用参数
7988
Confidence int `json:"confidence,omitempty"` // 信心度 (0-100)
8089
RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险
8190
Reasoning string `json:"reasoning"`
@@ -504,12 +513,15 @@ func findMatchingBracket(s string, start int) int {
504513
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
505514
// 验证action
506515
validActions := map[string]bool{
507-
"open_long": true,
508-
"open_short": true,
509-
"close_long": true,
510-
"close_short": true,
511-
"hold": true,
512-
"wait": true,
516+
"open_long": true,
517+
"open_short": true,
518+
"close_long": true,
519+
"close_short": true,
520+
"update_stop_loss": true,
521+
"update_take_profit": true,
522+
"partial_close": true,
523+
"hold": true,
524+
"wait": true,
513525
}
514526

515527
if !validActions[d.Action] {
@@ -589,5 +601,26 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
589601
}
590602
}
591603

604+
// 动态调整止损验证
605+
if d.Action == "update_stop_loss" {
606+
if d.NewStopLoss <= 0 {
607+
return fmt.Errorf("新止损价格必须大于0: %.2f", d.NewStopLoss)
608+
}
609+
}
610+
611+
// 动态调整止盈验证
612+
if d.Action == "update_take_profit" {
613+
if d.NewTakeProfit <= 0 {
614+
return fmt.Errorf("新止盈价格必须大于0: %.2f", d.NewTakeProfit)
615+
}
616+
}
617+
618+
// 部分平仓验证
619+
if d.Action == "partial_close" {
620+
if d.ClosePercentage <= 0 || d.ClosePercentage > 100 {
621+
return fmt.Errorf("平仓百分比必须在0-100之间: %.1f", d.ClosePercentage)
622+
}
623+
}
624+
592625
return nil
593626
}

logger/decision_logger.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,18 +409,24 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
409409
quantity := openPos["quantity"].(float64)
410410
leverage := openPos["leverage"].(int)
411411

412+
// 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量
413+
actualQuantity := quantity
414+
if action.Action == "partial_close" {
415+
actualQuantity = action.Quantity
416+
}
417+
412418
// 计算实际盈亏(USDT)
413-
// 合约交易 PnL 计算:quantity × 价格差
419+
// 合约交易 PnL 计算:actualQuantity × 价格差
414420
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
415421
var pnl float64
416422
if side == "long" {
417-
pnl = quantity * (action.Price - openPrice)
423+
pnl = actualQuantity * (action.Price - openPrice)
418424
} else {
419-
pnl = quantity * (openPrice - action.Price)
425+
pnl = actualQuantity * (openPrice - action.Price)
420426
}
421427

422428
// 计算盈亏百分比(相对保证金)
423-
positionValue := quantity * openPrice
429+
positionValue := actualQuantity * openPrice
424430
marginUsed := positionValue / float64(leverage)
425431
pnlPct := 0.0
426432
if marginUsed > 0 {
@@ -431,7 +437,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
431437
outcome := TradeOutcome{
432438
Symbol: symbol,
433439
Side: side,
434-
Quantity: quantity,
440+
Quantity: actualQuantity,
435441
Leverage: leverage,
436442
OpenPrice: openPrice,
437443
ClosePrice: action.Price,

trader/aster_trader.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,61 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error {
10051005
return err
10061006
}
10071007

1008+
// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置)
1009+
func (t *AsterTrader) CancelStopOrders(symbol string) error {
1010+
// 获取该币种的所有未完成订单
1011+
params := map[string]interface{}{
1012+
"symbol": symbol,
1013+
}
1014+
1015+
body, err := t.request("GET", "/fapi/v3/openOrders", params)
1016+
if err != nil {
1017+
return fmt.Errorf("获取未完成订单失败: %w", err)
1018+
}
1019+
1020+
var orders []map[string]interface{}
1021+
if err := json.Unmarshal(body, &orders); err != nil {
1022+
return fmt.Errorf("解析订单数据失败: %w", err)
1023+
}
1024+
1025+
// 过滤出止盈止损单并取消
1026+
canceledCount := 0
1027+
for _, order := range orders {
1028+
orderType, _ := order["type"].(string)
1029+
1030+
// 只取消止损和止盈订单
1031+
if orderType == "STOP_MARKET" ||
1032+
orderType == "TAKE_PROFIT_MARKET" ||
1033+
orderType == "STOP" ||
1034+
orderType == "TAKE_PROFIT" {
1035+
1036+
orderID, _ := order["orderId"].(float64)
1037+
cancelParams := map[string]interface{}{
1038+
"symbol": symbol,
1039+
"orderId": int64(orderID),
1040+
}
1041+
1042+
_, err := t.request("DELETE", "/fapi/v3/order", cancelParams)
1043+
if err != nil {
1044+
log.Printf(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err)
1045+
continue
1046+
}
1047+
1048+
canceledCount++
1049+
log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
1050+
symbol, int64(orderID), orderType)
1051+
}
1052+
}
1053+
1054+
if canceledCount == 0 {
1055+
log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol)
1056+
} else {
1057+
log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
1058+
}
1059+
1060+
return nil
1061+
}
1062+
10081063
// FormatQuantity 格式化数量(实现Trader接口)
10091064
func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
10101065
formatted, err := t.formatQuantity(symbol, quantity)

0 commit comments

Comments
 (0)