From a41f2f5a72620a4a5689f2f8684e208cdca93689 Mon Sep 17 00:00:00 2001 From: Lawrence Liu Date: Sun, 16 Nov 2025 08:15:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(decision):=20clarify=20field=20names=20for?= =?UTF-8?q?=20update=5Fstop=5Floss=20and=20update=5Ftake=5Fprofit=20action?= =?UTF-8?q?s=20(#993)=20*=20fix(decision):=20clarify=20field=20names=20for?= =?UTF-8?q?=20update=5Fstop=5Floss=20and=20update=5Ftake=5Fprofit=20action?= =?UTF-8?q?s=20=E4=BF=AE=E5=A4=8D=20AI=20=E5=86=B3=E7=AD=96=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=AD=97=E6=AE=B5=E5=90=8D=E6=B7=B7=E6=B7=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9A=20**=E9=97=AE=E9=A2=98**=EF=BC=9A=20AI=20?= =?UTF-8?q?=E5=9C=A8=E4=BD=BF=E7=94=A8=20update=5Fstop=5Floss=20=E6=97=B6?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=9C=B0=E4=BD=BF=E7=94=A8=E4=BA=86=20`stop?= =?UTF-8?q?=5Floss`=20=E5=AD=97=E6=AE=B5=EF=BC=8C=20=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=EF=BC=88backend=20?= =?UTF-8?q?=E6=9C=9F=E6=9C=9B=20`new=5Fstop=5Floss`=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=89=20**=E6=A0=B9=E5=9B=A0**=EF=BC=9A=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=20prompt=20=E7=9A=84=E5=AD=97=E6=AE=B5=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E4=B8=8D=E5=A4=9F=E6=98=8E=E7=A1=AE=EF=BC=8CAI=20?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E7=9F=A5=E9=81=93=20update=5Fstop=5Floss=20?= =?UTF-8?q?=E5=BA=94=E8=AF=A5=E4=BD=BF=E7=94=A8=20new=5Fstop=5Floss=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E8=80=8C=E9=9D=9E=20stop=5Floss=20**?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D**=EF=BC=9A=201.=20=E5=9C=A8=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E8=AF=B4=E6=98=8E=E4=B8=AD=E6=98=8E=E7=A1=AE=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=EF=BC=9A=20=20=20=20-=20update=5Fstop=5Floss=20?= =?UTF-8?q?=E6=97=B6=E5=BF=85=E5=A1=AB:=20new=5Fstop=5Floss=20(=E4=B8=8D?= =?UTF-8?q?=E6=98=AF=20stop=5Floss)=20=20=20=20-=20update=5Ftake=5Fprofit?= =?UTF-8?q?=20=E6=97=B6=E5=BF=85=E5=A1=AB:=20new=5Ftake=5Fprofit=20(?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=20take=5Fprofit)=202.=20=E5=9C=A8=20JSON=20?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E4=B8=AD=E5=A2=9E=E5=8A=A0=20update=5Fstop?= =?UTF-8?q?=5Floss=20=E7=9A=84=E5=85=B7=E4=BD=93=E7=94=A8=E6=B3=95?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=20**=E9=AA=8C=E8=AF=81**=EF=BC=9A=20decision?= =?UTF-8?q?=5Flogs=20=E4=B8=AD=E7=9A=84=E9=94=99=E8=AF=AF=20"=E6=96=B0?= =?UTF-8?q?=E6=AD=A2=E6=8D=9F=E4=BB=B7=E6=A0=BC=E5=BF=85=E9=A1=BB=E5=A4=A7?= =?UTF-8?q?=E4=BA=8E0:=200.00"=20=E5=BA=94=E8=AF=A5=E6=B6=88=E5=A4=B1=20*?= =?UTF-8?q?=20test(decision):=20add=20validation=20tests=20for=20update=20?= =?UTF-8?q?actions=20=E6=B7=BB=E5=8A=A0=E9=92=88=E5=AF=B9=20update=5Fstop?= =?UTF-8?q?=5Floss=E3=80=81update=5Ftake=5Fprofit=20=E5=92=8C=20partial=5F?= =?UTF-8?q?close=20=E5=8A=A8=E4=BD=9C=E7=9A=84=E5=AD=97=E6=AE=B5=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=9A=20**?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96**=EF=BC=9A=201.=20TestUpda?= =?UTF-8?q?teStopLossValidation=20-=20=E9=AA=8C=E8=AF=81=20new=5Fstop=5Flo?= =?UTF-8?q?ss=20=E5=AD=97=E6=AE=B5=20=20=20=20-=20=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20new=5Fstop=5Floss=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=88=E5=BA=94=E9=80=9A=E8=BF=87=EF=BC=89=20=20=20=20-=20ne?= =?UTF-8?q?w=5Fstop=5Floss=20=E4=B8=BA=200=EF=BC=88=E5=BA=94=E6=8A=A5?= =?UTF-8?q?=E9=94=99=EF=BC=89=20=20=20=20-=20new=5Fstop=5Floss=20=E4=B8=BA?= =?UTF-8?q?=E8=B4=9F=E6=95=B0=EF=BC=88=E5=BA=94=E6=8A=A5=E9=94=99=EF=BC=89?= =?UTF-8?q?=202.=20TestUpdateTakeProfitValidation=20-=20=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=20new=5Ftake=5Fprofit=20=E5=AD=97=E6=AE=B5=20=20=20=20-=20?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E4=BD=BF=E7=94=A8=20new=5Ftake=5Fprofit=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=EF=BC=88=E5=BA=94=E9=80=9A=E8=BF=87=EF=BC=89?= =?UTF-8?q?=20=20=20=20-=20new=5Ftake=5Fprofit=20=E4=B8=BA=200=EF=BC=88?= =?UTF-8?q?=E5=BA=94=E6=8A=A5=E9=94=99=EF=BC=89=20=20=20=20-=20new=5Ftake?= =?UTF-8?q?=5Fprofit=20=E4=B8=BA=E8=B4=9F=E6=95=B0=EF=BC=88=E5=BA=94?= =?UTF-8?q?=E6=8A=A5=E9=94=99=EF=BC=89=203.=20TestPartialCloseValidation?= =?UTF-8?q?=20-=20=E9=AA=8C=E8=AF=81=20close=5Fpercentage=20=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=20=20=20=20-=20=E6=AD=A3=E7=A1=AE=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=20close=5Fpercentage=20=E5=AD=97=E6=AE=B5=EF=BC=88=E5=BA=94?= =?UTF-8?q?=E9=80=9A=E8=BF=87=EF=BC=89=20=20=20=20-=20close=5Fpercentage?= =?UTF-8?q?=20=E4=B8=BA=200=EF=BC=88=E5=BA=94=E6=8A=A5=E9=94=99=EF=BC=89?= =?UTF-8?q?=20=20=20=20-=20close=5Fpercentage=20=E8=B6=85=E8=BF=87=20100?= =?UTF-8?q?=EF=BC=88=E5=BA=94=E6=8A=A5=E9=94=99=EF=BC=89=20**=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=BB=93=E6=9E=9C**=EF=BC=9A=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E9=80=9A=E8=BF=87=20?= =?UTF-8?q?=E2=9C=93=20---------=20Co-authored-by:=20Shui=20<88711385+hzb1?= =?UTF-8?q?115@users.noreply.github.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 6 +- decision/validate_test.go | 195 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) diff --git a/decision/engine.go b/decision/engine.go index a6b0113e..ea0f4fbc 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -346,13 +346,17 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in sb.WriteString("\n") sb.WriteString("```json\n[\n") sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", btcEthLeverage, accountEquity*5)) + sb.WriteString(" {\"symbol\": \"SOLUSDT\", \"action\": \"update_stop_loss\", \"new_stop_loss\": 155, \"reasoning\": \"移动止损至保本位\"},\n") sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈离场\"}\n") sb.WriteString("]\n```\n") sb.WriteString("\n\n") sb.WriteString("## 字段说明\n\n") sb.WriteString("- `action`: open_long | open_short | close_long | close_short | update_stop_loss | update_take_profit | partial_close | hold | wait\n") sb.WriteString("- `confidence`: 0-100(开仓建议≥75)\n") - sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd, reasoning\n\n") + sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd, reasoning\n") + sb.WriteString("- update_stop_loss 时必填: new_stop_loss (注意是 new_stop_loss,不是 stop_loss)\n") + sb.WriteString("- update_take_profit 时必填: new_take_profit (注意是 new_take_profit,不是 take_profit)\n") + sb.WriteString("- partial_close 时必填: close_percentage (0-100)\n\n") return sb.String() } diff --git a/decision/validate_test.go b/decision/validate_test.go index faac4fe5..d7e89229 100644 --- a/decision/validate_test.go +++ b/decision/validate_test.go @@ -98,3 +98,198 @@ func TestLeverageFallback(t *testing.T) { }) } } + +// TestUpdateStopLossValidation 测试 update_stop_loss 动作的字段验证 +func TestUpdateStopLossValidation(t *testing.T) { + tests := []struct { + name string + decision Decision + wantError bool + errorMsg string + }{ + { + name: "正确使用new_stop_loss字段", + decision: Decision{ + Symbol: "SOLUSDT", + Action: "update_stop_loss", + NewStopLoss: 155.5, + Reasoning: "移动止损至保本位", + }, + wantError: false, + }, + { + name: "new_stop_loss为0应该报错", + decision: Decision{ + Symbol: "SOLUSDT", + Action: "update_stop_loss", + NewStopLoss: 0, + Reasoning: "测试错误情况", + }, + wantError: true, + errorMsg: "新止损价格必须大于0", + }, + { + name: "new_stop_loss为负数应该报错", + decision: Decision{ + Symbol: "SOLUSDT", + Action: "update_stop_loss", + NewStopLoss: -100, + Reasoning: "测试错误情况", + }, + wantError: true, + errorMsg: "新止损价格必须大于0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDecision(&tt.decision, 1000.0, 10, 5) + + if (err != nil) != tt.wantError { + t.Errorf("validateDecision() error = %v, wantError %v", err, tt.wantError) + return + } + + if tt.wantError && err != nil { + if tt.errorMsg != "" && !contains(err.Error(), tt.errorMsg) { + t.Errorf("错误信息不匹配: got %q, want to contain %q", err.Error(), tt.errorMsg) + } + } + }) + } +} + +// TestUpdateTakeProfitValidation 测试 update_take_profit 动作的字段验证 +func TestUpdateTakeProfitValidation(t *testing.T) { + tests := []struct { + name string + decision Decision + wantError bool + errorMsg string + }{ + { + name: "正确使用new_take_profit字段", + decision: Decision{ + Symbol: "BTCUSDT", + Action: "update_take_profit", + NewTakeProfit: 98000, + Reasoning: "调整止盈至关键阻力位", + }, + wantError: false, + }, + { + name: "new_take_profit为0应该报错", + decision: Decision{ + Symbol: "BTCUSDT", + Action: "update_take_profit", + NewTakeProfit: 0, + Reasoning: "测试错误情况", + }, + wantError: true, + errorMsg: "新止盈价格必须大于0", + }, + { + name: "new_take_profit为负数应该报错", + decision: Decision{ + Symbol: "BTCUSDT", + Action: "update_take_profit", + NewTakeProfit: -1000, + Reasoning: "测试错误情况", + }, + wantError: true, + errorMsg: "新止盈价格必须大于0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDecision(&tt.decision, 1000.0, 10, 5) + + if (err != nil) != tt.wantError { + t.Errorf("validateDecision() error = %v, wantError %v", err, tt.wantError) + return + } + + if tt.wantError && err != nil { + if tt.errorMsg != "" && !contains(err.Error(), tt.errorMsg) { + t.Errorf("错误信息不匹配: got %q, want to contain %q", err.Error(), tt.errorMsg) + } + } + }) + } +} + +// TestPartialCloseValidation 测试 partial_close 动作的字段验证 +func TestPartialCloseValidation(t *testing.T) { + tests := []struct { + name string + decision Decision + wantError bool + errorMsg string + }{ + { + name: "正确使用close_percentage字段", + decision: Decision{ + Symbol: "ETHUSDT", + Action: "partial_close", + ClosePercentage: 50.0, + Reasoning: "锁定一半利润", + }, + wantError: false, + }, + { + name: "close_percentage为0应该报错", + decision: Decision{ + Symbol: "ETHUSDT", + Action: "partial_close", + ClosePercentage: 0, + Reasoning: "测试错误情况", + }, + wantError: true, + errorMsg: "平仓百分比必须在0-100之间", + }, + { + name: "close_percentage超过100应该报错", + decision: Decision{ + Symbol: "ETHUSDT", + Action: "partial_close", + ClosePercentage: 150, + Reasoning: "测试错误情况", + }, + wantError: true, + errorMsg: "平仓百分比必须在0-100之间", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDecision(&tt.decision, 1000.0, 10, 5) + + if (err != nil) != tt.wantError { + t.Errorf("validateDecision() error = %v, wantError %v", err, tt.wantError) + return + } + + if tt.wantError && err != nil { + if tt.errorMsg != "" && !contains(err.Error(), tt.errorMsg) { + t.Errorf("错误信息不匹配: got %q, want to contain %q", err.Error(), tt.errorMsg) + } + } + }) + } +} + +// contains 检查字符串是否包含子串(辅助函数) +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > 0 && len(substr) > 0 && stringContains(s, substr))) +} + +func stringContains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +}