fix: add JSON validation to prevent range values and thousand separators

修復 LLM 返回範圍值導致 JSON 解析失敗的問題

## 問題
LLM 有時返回價格範圍 [98,000 ~ 102,000] 而不是單一數值,
導致 JSON 解析失敗:invalid character '0' after array element

## 修復內容

### 1. Prompts 更新(3 個文件)
- prompts/default.txt: 添加數字格式要求(中文)
- prompts/adaptive.txt: 添加數字格式要求(中文)
- prompts/nof1.txt: 添加數字格式要求(英文)

明確禁止:
-  範圍符號 ~
-  千位分隔符 98,000
-  文字描述

### 2. JSON 驗證邏輯(decision/engine.go)
新增函數:
- validateJSONFormat(): 檢測範圍、千位分隔符等錯誤
- min(): 輔助函數

檢測內容:
1. 必須是對象數組 [{...}]
2. 不可包含範圍符號 ~
3. 不可包含千位分隔符 98,000

## 測試
✓ go build ./...  編譯成功
✓ go fmt ./...    格式正確

修改統計:+132 行(46 行代碼 + 86 行 prompts)
This commit is contained in:
ZhouYongyou
2025-11-04 21:18:27 +08:00
parent c46f0be315
commit 8bf6c2e1e2
4 changed files with 132 additions and 0 deletions

View File

@@ -481,6 +481,11 @@ func extractDecisions(response string) ([]Decision, error) {
jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1])
// 🔧 验证 JSON 格式(检测常见错误)
if err := validateJSONFormat(jsonContent); err != nil {
return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response)
}
// 🔧 修复常见的JSON格式错误缺少引号的字段值
// 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号)
// 修复为: "reasoning": "内容"}
@@ -496,6 +501,39 @@ func extractDecisions(response string) ([]Decision, error) {
return decisions, nil
}
// validateJSONFormat 验证 JSON 格式,检测常见错误
func validateJSONFormat(jsonStr string) error {
trimmed := strings.TrimSpace(jsonStr)
// 检查是否是决策对象数组(必须以 [{ 或 [ { 开头)
if !strings.HasPrefix(trimmed, "[{") && !strings.HasPrefix(trimmed, "[ {") {
// 检查是否是纯数字/范围数组(常见错误)
if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") {
return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))])
}
return fmt.Errorf("JSON 必须以 [{ 开头(决策对象数组),实际: %s", trimmed[:min(20, len(trimmed))])
}
// 检查是否包含范围符号 ~LLM 常见错误)
if strings.Contains(jsonStr, "~") {
return fmt.Errorf("JSON 中不可包含范围符号 ~,所有数字必须是精确的单一值")
}
// 检查是否包含千位分隔符(如 98,000
// 使用简单的模式匹配:数字+逗号+3位数字
for i := 0; i < len(jsonStr)-4; i++ {
if jsonStr[i] >= '0' && jsonStr[i] <= '9' &&
jsonStr[i+1] == ',' &&
jsonStr[i+2] >= '0' && jsonStr[i+2] <= '9' &&
jsonStr[i+3] >= '0' && jsonStr[i+3] <= '9' &&
jsonStr[i+4] >= '0' && jsonStr[i+4] <= '9' {
return fmt.Errorf("JSON 数字不可包含千位分隔符逗号,发现: %s", jsonStr[i:min(i+10, len(jsonStr))])
}
}
return nil
}
// fixMissingQuotes 替换中文引号为英文引号(避免输入法自动转换)
func fixMissingQuotes(jsonStr string) string {
jsonStr = strings.ReplaceAll(jsonStr, "\u201c", "\"") // "
@@ -505,6 +543,14 @@ func fixMissingQuotes(jsonStr string) string {
return jsonStr
}
// min 返回两个整数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
// validateDecisions 验证所有决策(需要账户信息和杠杆配置)
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
for i, decision := range decisions {

View File

@@ -387,6 +387,35 @@ Key insight: 一句话总结本次决策
}
```
### ⚠️ 数字格式要求
**所有数字字段必须是精确的单一数值**,不可使用范围或特殊格式:
✅ **正确格式**
```json
{
"stop_loss": 98500.0,
"take_profit": 102000.0,
"position_size_usd": 50.0,
"confidence": 85
}
```
❌ **错误格式(会导致解析失败)**
```json
{
"stop_loss": "98,000 ~ 102,000", // ❌ 不可使用范围符号 ~
"take_profit": 102,000, // ❌ 不可使用千位分隔符
"confidence": "85左右" // ❌ 不可使用文字描述
}
```
**注意**
- 必须使用精确数值,不可使用范围 (如 `~`、`-`、`to`)
- 不可使用千位分隔符逗号 (98000 而非 98,000)
- 不可使用约数或文字描述
- confidence 使用整数 0-100其他价格使用浮点数
---
# 最终检查清单(开仓前必须全部通过)

View File

@@ -135,6 +135,34 @@ confidence=0-100
}
```
### ⚠️ 数字格式要求
**所有数字字段必须是精确的单一数值**,不可使用范围或特殊格式:
✅ **正确格式**
```json
{
"stop_loss": 98500.0,
"take_profit": 102000.0,
"position_size_usd": 50.0
}
```
❌ **错误格式(会导致解析失败)**
```json
{
"stop_loss": "98,000 ~ 102,000", // ❌ 不可使用范围符号 ~
"take_profit": 102,000, // ❌ 不可使用千位分隔符
"position_size_usd": "约50" // ❌ 不可使用文字描述
}
```
**注意**
- 必须使用精确数值,不可使用范围 (如 `~`、`-`、`to`)
- 不可使用千位分隔符逗号 (98000 而非 98,000)
- 不可使用约数或文字描述
- 建议使用浮点数格式 (加 .0)
### 必填规则
- **开仓** (`open_long/open_short`):必须填写 `position_size_usd`、`leverage`、`stop_loss`、`take_profit`、`risk_usd``reasoning` 写出信号与风险回报。
- **平仓** (`close_long/close_short`)`reasoning` 说明平仓原因(达到目标、触发失效条件等)。

View File

@@ -299,6 +299,35 @@ Every decision must follow this structure:
}
```
### ⚠️ Number Format Requirements
**All numeric fields must use exact single values** - no ranges or special formatting:
✅ **Correct format**:
```json
{
"stop_loss": 98500.0,
"take_profit": 102000.0,
"position_size_usd": 50.0,
"confidence": 85
}
```
❌ **Incorrect format (will cause parsing errors)**:
```json
{
"stop_loss": "98,000 ~ 102,000", // ❌ No range symbols ~
"take_profit": 102,000, // ❌ No thousand separators
"confidence": "around 85" // ❌ No text descriptions
}
```
**Rules**:
- Use precise numeric values only, no ranges (e.g., `~`, `-`, `to`)
- No thousand separator commas (use 98000 not 98,000)
- No approximations or text descriptions
- Use integers for confidence (0-100), floats for prices (add .0)
### Required field rules
- **open_long / open_short**: Fill all numeric fields; `risk_usd` ≤ account_value × 0.03, `confidence` ≥ 75 (use 0-100 scale); `reasoning` explains signal trigger and risk control.
- **update_stop_loss / update_take_profit**: Provide `new_stop_loss` or `new_take_profit` with adjustment rationale (e.g., trailing stop, locking profits).