feat: implement hybrid database architecture and frontend encryption

- Add PostgreSQL + SQLite hybrid database support with automatic switching
- Implement frontend AES-GCM + RSA-OAEP encryption for sensitive data
- Add comprehensive DatabaseInterface with all required methods
- Fix compilation issues with interface consistency
- Update all database method signatures to use DatabaseInterface
- Add missing UpdateTraderInitialBalance method to PostgreSQL implementation
- Integrate RSA public key distribution via /api/config endpoint
- Add frontend crypto service with proper error handling
- Support graceful degradation between encrypted and plaintext transmission
- Add directory creation for RSA keys and PEM parsing fixes
- Test both SQLite and PostgreSQL modes successfully

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
icy
2025-11-06 01:50:06 +08:00
parent aecc5e58a1
commit 65053518d6
104 changed files with 16864 additions and 4152 deletions

View File

@@ -7,10 +7,22 @@ import (
"nofx/market"
"nofx/mcp"
"nofx/pool"
"regexp"
"strings"
"time"
)
// 预编译正则表达式(性能优化:避免每次调用时重新编译)
var (
// ✅ 安全的正則:精確匹配 ```json 代碼塊
// 使用反引號 + 拼接避免轉義問題
reJSONFence = regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```")
reJSONArray = regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`)
reArrayHead = regexp.MustCompile(`^\[\s*\{`)
reArrayOpenSpace = regexp.MustCompile(`^\[\s+\{`)
reInvisibleRunes = regexp.MustCompile("[\u200B\u200C\u200D\uFEFF]")
)
// PositionInfo 持仓信息
type PositionInfo struct {
Symbol string `json:"symbol"`
@@ -71,11 +83,20 @@ type Context struct {
// Decision AI的交易决策
type Decision struct {
Symbol string `json:"symbol"`
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait"
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait"
// 开仓参数
Leverage int `json:"leverage,omitempty"`
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
StopLoss float64 `json:"stop_loss,omitempty"`
TakeProfit float64 `json:"take_profit,omitempty"`
// 调整参数(新增)
NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss
NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit
ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100)
// 通用参数
Confidence int `json:"confidence,omitempty"` // 信心度 (0-100)
RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险
Reasoning string `json:"reasoning"`
@@ -160,17 +181,20 @@ func fetchMarketDataForContext(ctx *Context) error {
continue
}
// ⚠️ 流动性过滤:持仓价值低于15M USD的币种不做(多空都不做)
// ⚠️ 流动性过滤:持仓价值低于阈值的币种不做(多空都不做)
// 持仓价值 = 持仓量 × 当前价格
// 但现有持仓必须保留(需要决策是否平仓)
// 💡 OI 門檻配置:用戶可根據風險偏好調整
const minOIThresholdMillions = 15.0 // 可調整15M(保守) / 10M(平衡) / 8M(寬鬆) / 5M(激進)
isExistingPosition := positionSymbols[symbol]
if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 {
// 计算持仓价值USD= 持仓量 × 当前价格
oiValue := data.OpenInterest.Latest * data.CurrentPrice
oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位
if oiValueInMillions < 15 {
log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < 15M),跳过此币种 [持仓量:%.0f × 价格:%.4f]",
symbol, oiValueInMillions, data.OpenInterest.Latest, data.CurrentPrice)
if oiValueInMillions < minOIThresholdMillions {
log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < %.1fM),跳过此币种 [持仓量:%.0f × 价格:%.4f]",
symbol, oiValueInMillions, minOIThresholdMillions, data.OpenInterest.Latest, data.CurrentPrice)
continue
}
}
@@ -200,10 +224,31 @@ func fetchMarketDataForContext(ctx *Context) error {
// calculateMaxCandidates 根据账户状态计算需要分析的候选币种数量
func calculateMaxCandidates(ctx *Context) int {
// 直接返回候选池的全部币种数量
// 因为候选池已经在 auto_trader.go 中筛选过了
// 固定分析前20个评分最高的币种来自AI500
return len(ctx.CandidateCoins)
// ⚠️ 重要:限制候选币种数量,避免 Prompt 过大
// 根据持仓数量动态调整:持仓越少,可以分析更多候选币
const (
maxCandidatesWhenEmpty = 30 // 无持仓时最多分析30个候选币
maxCandidatesWhenHolding1 = 25 // 持仓1个时最多分析25个候选币
maxCandidatesWhenHolding2 = 20 // 持仓2个时最多分析20个候选币
maxCandidatesWhenHolding3 = 15 // 持仓3个时最多分析15个候选币避免 Prompt 过大)
)
positionCount := len(ctx.Positions)
var maxCandidates int
switch positionCount {
case 0:
maxCandidates = maxCandidatesWhenEmpty
case 1:
maxCandidates = maxCandidatesWhenHolding1
case 2:
maxCandidates = maxCandidatesWhenHolding2
default: // 3+ 持仓
maxCandidates = maxCandidatesWhenHolding3
}
// 返回实际候选币数量和上限中的较小值
return min(len(ctx.CandidateCoins), maxCandidates)
}
// buildSystemPromptWithCustom 构建包含自定义内容的 System Prompt
@@ -264,9 +309,11 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in
sb.WriteString("# 硬约束(风险控制)\n\n")
sb.WriteString("1. 风险回报比: 必须 ≥ 1:3冒1%风险赚3%+收益)\n")
sb.WriteString("2. 最多持仓: 3个币种质量>数量)\n")
sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n",
accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage))
sb.WriteString("4. 保证金: 总使用率 ≤ 90%\n\n")
sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n",
accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10))
sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆** (⚠️ 严格执行,不可超过)\n", altcoinLeverage, btcEthLeverage))
sb.WriteString("5. 保证金: 总使用率 ≤ 90%\n")
sb.WriteString("6. 开仓金额: 建议 **≥12 USDT** (交易所最小名义价值 10 USDT + 安全边际)\n\n")
// 3. 输出格式 - 动态生成
sb.WriteString("#输出格式\n\n")
@@ -430,25 +477,44 @@ func extractCoTTrace(response string) string {
// extractDecisions 提取JSON决策列表
func extractDecisions(response string) ([]Decision, error) {
// 直接查找JSON数组 - 找第一个完整的JSON数组
arrayStart := strings.Index(response, "[")
if arrayStart == -1 {
return nil, fmt.Errorf("无法找到JSON数组起始")
// 预清洗:去零宽/BOM
s := removeInvisibleRunes(response)
s = strings.TrimSpace(s)
// 🔧 關鍵修復:在正則匹配之前就先修復全角字符!
// 否則正則表達式 \[ 無法匹配全角的
s = fixMissingQuotes(s)
// 1) 优先从 ```json 代码块中提取
if m := reJSONFence.FindStringSubmatch(s); m != nil && len(m) > 1 {
jsonContent := strings.TrimSpace(m[1])
jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{"
jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有全角)
if err := validateJSONFormat(jsonContent); err != nil {
return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response)
}
var decisions []Decision
if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil {
return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent)
}
return decisions, nil
}
// 从 [ 开始,匹配括号找到对应的 ]
arrayEnd := findMatchingBracket(response, arrayStart)
if arrayEnd == -1 {
return nil, fmt.Errorf("无法找到JSON数组结束")
// 2) 退而求其次:全文寻找首个对象数组
// 注意:此時 s 已經過 fixMissingQuotes(),全角字符已轉換為半角
jsonContent := strings.TrimSpace(reJSONArray.FindString(s))
if jsonContent == "" {
return nil, fmt.Errorf("无法找到JSON数组起始已嘗試修復全角字符\n原始響應前200字符: %s", s[:min(200, len(s))])
}
jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1])
// 🔧 規整格式(此時全角字符已在前面修復過)
jsonContent = compactArrayOpen(jsonContent)
jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有殘留全角)
// 🔧 修复常见的JSON格式错误:缺少引号的字段值
// 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号)
// 修复为: "reasoning": "内容"}
// 使用简单的字符串扫描而不是正则表达式
jsonContent = fixMissingQuotes(jsonContent)
// 🔧 验证 JSON 格式(检测常见错误)
if err := validateJSONFormat(jsonContent); err != nil {
return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response)
}
// 解析JSON
var decisions []Decision
@@ -459,15 +525,86 @@ func extractDecisions(response string) ([]Decision, error) {
return decisions, nil
}
// fixMissingQuotes 替换中文引号为英文引号(避免输入法自动转换
// fixMissingQuotes 替换中文引号和全角字符为英文引号和半角字符避免AI输出全角JSON字符导致解析失败
func fixMissingQuotes(jsonStr string) string {
// 替换中文引号
jsonStr = strings.ReplaceAll(jsonStr, "\u201c", "\"") // "
jsonStr = strings.ReplaceAll(jsonStr, "\u201d", "\"") // "
jsonStr = strings.ReplaceAll(jsonStr, "\u2018", "'") // '
jsonStr = strings.ReplaceAll(jsonStr, "\u2019", "'") // '
// ⚠️ 替换全角括号、冒号、逗号防止AI输出全角JSON字符
jsonStr = strings.ReplaceAll(jsonStr, "", "[") // U+FF3B 全角左方括号
jsonStr = strings.ReplaceAll(jsonStr, "", "]") // U+FF3D 全角右方括号
jsonStr = strings.ReplaceAll(jsonStr, "", "{") // U+FF5B 全角左花括号
jsonStr = strings.ReplaceAll(jsonStr, "", "}") // U+FF5D 全角右花括号
jsonStr = strings.ReplaceAll(jsonStr, "", ":") // U+FF1A 全角冒号
jsonStr = strings.ReplaceAll(jsonStr, "", ",") // U+FF0C 全角逗号
// ⚠️ 替换CJK标点符号AI在中文上下文中也可能输出这些
jsonStr = strings.ReplaceAll(jsonStr, "【", "[") // CJK左方头括号 U+3010
jsonStr = strings.ReplaceAll(jsonStr, "】", "]") // CJK右方头括号 U+3011
jsonStr = strings.ReplaceAll(jsonStr, "", "[") // CJK左龟壳括号 U+3014
jsonStr = strings.ReplaceAll(jsonStr, "", "]") // CJK右龟壳括号 U+3015
jsonStr = strings.ReplaceAll(jsonStr, "、", ",") // CJK顿号 U+3001
// ⚠️ 替换全角空格为半角空格JSON中不应该有全角空格
jsonStr = strings.ReplaceAll(jsonStr, " ", " ") // U+3000 全角空格
return jsonStr
}
// validateJSONFormat 验证 JSON 格式,检测常见错误
func validateJSONFormat(jsonStr string) error {
trimmed := strings.TrimSpace(jsonStr)
// 允许 [ 和 { 之间存在任意空白(含零宽)
if !reArrayHead.MatchString(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
}
// min 返回两个整数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
// removeInvisibleRunes 去除零宽字符和 BOM避免肉眼看不见的前缀破坏校验
func removeInvisibleRunes(s string) string {
return reInvisibleRunes.ReplaceAllString(s, "")
}
// compactArrayOpen 规整开头的 "[ {" → "[{"
func compactArrayOpen(s string) string {
return reArrayOpenSpace.ReplaceAllString(strings.TrimSpace(s), "[{")
}
// validateDecisions 验证所有决策(需要账户信息和杠杆配置)
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
for i, decision := range decisions {
@@ -504,12 +641,15 @@ func findMatchingBracket(s string, start int) int {
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
// 验证action
validActions := map[string]bool{
"open_long": true,
"open_short": true,
"close_long": true,
"close_short": true,
"hold": true,
"wait": true,
"open_long": true,
"open_short": true,
"close_long": true,
"close_short": true,
"update_stop_loss": true,
"update_take_profit": true,
"partial_close": true,
"hold": true,
"wait": true,
}
if !validActions[d.Action] {
@@ -532,6 +672,22 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
if d.PositionSizeUSD <= 0 {
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
}
// ✅ 验证最小开仓金额(防止数量格式化为 0 的错误)
// Binance 最小名义价值 10 USDT + 安全边际
const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际
const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活)
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
if d.PositionSizeUSD < minPositionSizeBTCETH {
return fmt.Errorf("%s 开仓金额过小(%.2f USDT),必须≥%.2f USDT因价格高且精度限制避免数量四舍五入为0", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH)
}
} else {
if d.PositionSizeUSD < minPositionSizeGeneral {
return fmt.Errorf("开仓金额过小(%.2f USDT),必须≥%.2f USDTBinance 最小名义价值要求)", d.PositionSizeUSD, minPositionSizeGeneral)
}
}
// 验证仓位价值上限加1%容差以避免浮点数精度问题)
tolerance := maxPositionValue * 0.01 // 1%容差
if d.PositionSizeUSD > maxPositionValue+tolerance {
@@ -589,5 +745,26 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
}
}
// 动态调整止损验证
if d.Action == "update_stop_loss" {
if d.NewStopLoss <= 0 {
return fmt.Errorf("新止损价格必须大于0: %.2f", d.NewStopLoss)
}
}
// 动态调整止盈验证
if d.Action == "update_take_profit" {
if d.NewTakeProfit <= 0 {
return fmt.Errorf("新止盈价格必须大于0: %.2f", d.NewTakeProfit)
}
}
// 部分平仓验证
if d.Action == "partial_close" {
if d.ClosePercentage <= 0 || d.ClosePercentage > 100 {
return fmt.Errorf("平仓百分比必须在0-100之间: %.1f", d.ClosePercentage)
}
}
return nil
}