From 146d2ad9a7141046e07a74de97dfb88e0c735e61 Mon Sep 17 00:00:00 2001 From: Lawrence Liu Date: Sun, 9 Nov 2025 16:21:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20AI=20=E5=86=B3?= =?UTF-8?q?=E7=AD=96=E6=97=B6=E6=94=B6=E5=88=B0=E7=9A=84=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E7=9B=88=E4=BA=8F=E7=99=BE=E5=88=86=E6=AF=94=E6=9C=AA=E8=80=83?= =?UTF-8?q?=E8=99=91=E6=9D=A0=E6=9D=86=20(#819)=20Fixes=20#818=20##=20?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20=E4=BC=A0=E9=80=92=E7=BB=99=20AI=20?= =?UTF-8?q?=E5=86=B3=E7=AD=96=E7=9A=84=E6=8C=81=E4=BB=93=E7=9B=88=E4=BA=8F?= =?UTF-8?q?=E7=99=BE=E5=88=86=E6=AF=94=E5=8F=AA=E8=AE=A1=E7=AE=97=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E5=8F=98=E5=8A=A8=EF=BC=8C=E6=9C=AA=E8=80=83=E8=99=91?= =?UTF-8?q?=E6=9D=A0=E6=9D=86=E5=80=8D=E6=95=B0=E3=80=82=20=E4=BE=8B?= =?UTF-8?q?=E5=A6=82=EF=BC=9A10=E5=80=8D=E6=9D=A0=E6=9D=86=EF=BC=8C?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E4=B8=8A=E6=B6=A81%=EF=BC=8CAI=E7=9C=8B?= =?UTF-8?q?=E5=88=B0=E7=9A=84=E6=98=AF1%=E8=80=8C=E9=9D=9E=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E7=9A=8410%=E6=94=B6=E7=9B=8A=E7=8E=87=E3=80=82=20##?= =?UTF-8?q?=20=E6=94=B9=E5=8A=A8=201.=20=E4=BF=AE=E5=A4=8D=20buildTradingC?= =?UTF-8?q?ontext=20=E4=B8=AD=E7=9A=84=E7=9B=88=E4=BA=8F=E7=99=BE=E5=88=86?= =?UTF-8?q?=E6=AF=94=E8=AE=A1=E7=AE=97=20=20=20=20-=20=E4=BB=8E=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E4=BB=B7=E6=A0=BC=E5=8F=98=E5=8A=A8=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=E4=BF=9D=E8=AF=81=E9=87=91=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=20=20=20=20-=20=E6=94=B6=E7=9B=8A=E7=8E=87=20=3D=20=E6=9C=AA?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9B=88=E4=BA=8F=20/=20=E4=BF=9D=E8=AF=81?= =?UTF-8?q?=E9=87=91=20=C3=97=20100%=202.=20=E6=8A=BD=E5=8F=96=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E5=87=BD=E6=95=B0=20calculatePnLPercentage=20=20=20?= =?UTF-8?q?=20-=20=E6=B6=88=E9=99=A4=20buildTradingContext=20=E5=92=8C=20G?= =?UTF-8?q?etPositions=20=E7=9A=84=E9=87=8D=E5=A4=8D=E4=BB=A3=E7=A0=81=20?= =?UTF-8?q?=20=20=20-=20=E7=A1=AE=E4=BF=9D=E4=B8=A4=E5=A4=84=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9B=B8=E5=90=8C=E7=9A=84=E8=AE=A1=E7=AE=97=E9=80=BB?= =?UTF-8?q?=E8=BE=91=203.=20=E6=96=B0=E5=A2=9E=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=20(trader/auto=5Ftrader=5Ftest.go)=20=20=20=20-=209?= =?UTF-8?q?=E4=B8=AA=E5=9F=BA=E7=A1=80=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=EF=BC=88=E6=AD=A3=E5=B8=B8=E3=80=81=E8=BE=B9=E7=95=8C=E3=80=81?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=EF=BC=89=20=20=20=20-=203=E4=B8=AA=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=9C=BA=E6=99=AF=E6=B5=8B=E8=AF=95=EF=BC=88BTC/ETH/S?= =?UTF-8?q?OL=E4=B8=8D=E5=90=8C=E6=9D=A0=E6=9D=86=EF=BC=89=20=20=20=20-=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87=EF=BC=9A100%=204.?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0=20.gitignore=20=20=20=20-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20SQLite=20WAL=20=E7=9B=B8=E5=85=B3=E6=96=87=E4=BB=B6?= =?UTF-8?q?=20(config.db-shm,=20config.db-wal,=20nofx.db)=20##=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=BB=93=E6=9E=9C=20=E2=9C=85=20=E6=89=80=E6=9C=89=20?= =?UTF-8?q?12=20=E4=B8=AA=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E9=80=9A?= =?UTF-8?q?=E8=BF=87=20=E2=9C=85=20=E4=BB=A3=E7=A0=81=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=80=9A=E8=BF=87=20=E2=9C=85=20=E4=B8=8E=20GetPositions=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4=20##=20?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=20-=20AI=20=E7=8E=B0=E5=9C=A8=E8=83=BD?= =?UTF-8?q?=E5=A4=9F=E5=87=86=E7=A1=AE=E8=AF=84=E4=BC=B0=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E6=94=B6=E7=9B=8A=E7=8E=87=20-=20=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E5=9B=A0=E9=94=99=E8=AF=AF=E6=95=B0=E6=8D=AE=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E8=BF=87=E6=97=A9=E6=AD=A2=E7=9B=88=E6=88=96?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E6=AD=A2=E6=8D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/auto_trader.go | 26 ++++---- trader/auto_trader_test.go | 118 +++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 trader/auto_trader_test.go diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 8eec4a42..0df685b1 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -622,14 +622,6 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { unrealizedPnl := pos["unRealizedProfit"].(float64) liquidationPrice := pos["liquidationPrice"].(float64) - // 计算盈亏百分比 - pnlPct := 0.0 - if side == "long" { - pnlPct = ((markPrice - entryPrice) / entryPrice) * 100 - } else { - pnlPct = ((entryPrice - markPrice) / entryPrice) * 100 - } - // 计算占用保证金(估算) leverage := 10 // 默认值,实际应该从持仓信息获取 if lev, ok := pos["leverage"].(float64); ok { @@ -638,6 +630,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { marginUsed := (quantity * markPrice) / float64(leverage) totalMarginUsed += marginUsed + // 计算盈亏百分比(基于保证金,考虑杠杆) + pnlPct := calculatePnLPercentage(unrealizedPnl, marginUsed) + // 跟踪持仓首次出现时间 posKey := symbol + "_" + side currentPositionKeys[posKey] = true @@ -1382,11 +1377,7 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { marginUsed := (quantity * markPrice) / float64(leverage) // 计算盈亏百分比(基于保证金) - // 收益率 = 未实现盈亏 / 保证金 × 100% - pnlPct := 0.0 - if marginUsed > 0 { - pnlPct = (unrealizedPnl / marginUsed) * 100 - } + pnlPct := calculatePnLPercentage(unrealizedPnl, marginUsed) result = append(result, map[string]interface{}{ "symbol": symbol, @@ -1405,6 +1396,15 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } +// calculatePnLPercentage 计算盈亏百分比(基于保证金,自动考虑杠杆) +// 收益率 = 未实现盈亏 / 保证金 × 100% +func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 { + if marginUsed > 0 { + return (unrealizedPnl / marginUsed) * 100 + } + return 0.0 +} + // sortDecisionsByPriority 对决策排序:先平仓,再开仓,最后hold/wait // 这样可以避免换仓时仓位叠加超限 func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision { diff --git a/trader/auto_trader_test.go b/trader/auto_trader_test.go new file mode 100644 index 00000000..40d2e562 --- /dev/null +++ b/trader/auto_trader_test.go @@ -0,0 +1,118 @@ +package trader + +import ( + "math" + "testing" +) + +func TestCalculatePnLPercentage(t *testing.T) { + tests := []struct { + name string + unrealizedPnl float64 + marginUsed float64 + expected float64 + }{ + { + name: "正常盈利 - 10倍杠杆", + unrealizedPnl: 100.0, // 盈利 100 USDT + marginUsed: 1000.0, // 保证金 1000 USDT + expected: 10.0, // 10% 收益率 + }, + { + name: "正常亏损 - 10倍杠杆", + unrealizedPnl: -50.0, // 亏损 50 USDT + marginUsed: 1000.0, // 保证金 1000 USDT + expected: -5.0, // -5% 收益率 + }, + { + name: "高杠杆盈利 - 价格上涨1%,20倍杠杆", + unrealizedPnl: 200.0, // 盈利 200 USDT + marginUsed: 1000.0, // 保证金 1000 USDT + expected: 20.0, // 20% 收益率 + }, + { + name: "保证金为0 - 边界情况", + unrealizedPnl: 100.0, + marginUsed: 0.0, + expected: 0.0, // 应该返回 0 而不是除以零错误 + }, + { + name: "负保证金 - 边界情况", + unrealizedPnl: 100.0, + marginUsed: -1000.0, + expected: 0.0, // 应该返回 0(异常情况) + }, + { + name: "盈亏为0", + unrealizedPnl: 0.0, + marginUsed: 1000.0, + expected: 0.0, + }, + { + name: "小额交易", + unrealizedPnl: 0.5, + marginUsed: 10.0, + expected: 5.0, + }, + { + name: "大额盈利", + unrealizedPnl: 5000.0, + marginUsed: 10000.0, + expected: 50.0, + }, + { + name: "极小保证金", + unrealizedPnl: 1.0, + marginUsed: 0.01, + expected: 10000.0, // 100倍收益率 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculatePnLPercentage(tt.unrealizedPnl, tt.marginUsed) + + // 使用精度比较,避免浮点数误差 + if math.Abs(result-tt.expected) > 0.0001 { + t.Errorf("calculatePnLPercentage(%v, %v) = %v, want %v", + tt.unrealizedPnl, tt.marginUsed, result, tt.expected) + } + }) + } +} + +// TestCalculatePnLPercentage_RealWorldScenarios 真实场景测试 +func TestCalculatePnLPercentage_RealWorldScenarios(t *testing.T) { + t.Run("BTC 10倍杠杆,价格上涨2%", func(t *testing.T) { + // 开仓:1000 USDT 保证金,10倍杠杆 = 10000 USDT 仓位 + // 价格上涨 2% = 200 USDT 盈利 + // 收益率 = 200 / 1000 = 20% + result := calculatePnLPercentage(200.0, 1000.0) + expected := 20.0 + if math.Abs(result-expected) > 0.0001 { + t.Errorf("BTC场景: got %v, want %v", result, expected) + } + }) + + t.Run("ETH 5倍杠杆,价格下跌3%", func(t *testing.T) { + // 开仓:2000 USDT 保证金,5倍杠杆 = 10000 USDT 仓位 + // 价格下跌 3% = -300 USDT 亏损 + // 收益率 = -300 / 2000 = -15% + result := calculatePnLPercentage(-300.0, 2000.0) + expected := -15.0 + if math.Abs(result-expected) > 0.0001 { + t.Errorf("ETH场景: got %v, want %v", result, expected) + } + }) + + t.Run("SOL 20倍杠杆,价格上涨0.5%", func(t *testing.T) { + // 开仓:500 USDT 保证金,20倍杠杆 = 10000 USDT 仓位 + // 价格上涨 0.5% = 50 USDT 盈利 + // 收益率 = 50 / 500 = 10% + result := calculatePnLPercentage(50.0, 500.0) + expected := 10.0 + if math.Abs(result-expected) > 0.0001 { + t.Errorf("SOL场景: got %v, want %v", result, expected) + } + }) +}