mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 03:21:04 +08:00
fix: prevent initial_balance modification and fix equity history calculation
## 核心問題修復 1. **API 層保護** (api/server.go - handleUpdateTrader) - 檢測並阻止任何修改 initial_balance 的嘗試 - 強制使用原始 initial_balance 值 - 記錄詳細的警告日誌和嘗試修改的細節 - 返回友善的錯誤訊息給用戶 2. **資料庫層保護** (config/database.go - UpdateTrader) - 從 UPDATE SQL 語句中移除 initial_balance 欄位 - 雙重防護:即使 API 層被繞過,DB 也不會更新 3. **修復盈虧計算錯誤** (api/server.go - handleEquityHistory) - ✅ 修復:從資料庫讀取 initial_balance 作為唯一真實來源 - ❌ 移除:錯誤的後備邏輯(使用 records[0].TotalBalance) - ✅ 重新計算:基於正確的 initial_balance 重算所有盈虧百分比 ## 影響範圍 - 用戶無法再通過 UpdateTrader API 修改 initial_balance - 解決「初始餘額異常變動」的根本原因 - 確保盈虧計算始終基於正確的基準值 ## 技術細節 - 浮點數比較容差:0.01 USDT (避免浮點數精度問題) - 錯誤碼:INITIAL_BALANCE_IMMUTABLE - 日誌格式:包含 user_id, trader_id, 原值, 請求值, 差異 Related-Issue: 用戶報告「初始餘額變少」問題
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"nofx/auth"
|
"nofx/auth"
|
||||||
"nofx/config"
|
"nofx/config"
|
||||||
@@ -527,6 +528,40 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
systemPromptTemplate = existingTrader.SystemPromptTemplate // 保持原值
|
systemPromptTemplate = existingTrader.SystemPromptTemplate // 保持原值
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔒 保護 initial_balance 不被修改
|
||||||
|
initialBalance := existingTrader.InitialBalance
|
||||||
|
if req.InitialBalance > 0 {
|
||||||
|
// 檢查是否嘗試修改初始餘額(允許 0.01 USDT 的誤差)
|
||||||
|
diff := math.Abs(req.InitialBalance - existingTrader.InitialBalance)
|
||||||
|
if diff > 0.01 {
|
||||||
|
// 記錄警告日誌
|
||||||
|
log.Printf("⚠️ BLOCKED: User %s attempted to modify initial_balance | Trader=%s | Original=%.2f | Requested=%.2f | Diff=%.2f",
|
||||||
|
userID, traderID, existingTrader.InitialBalance, req.InitialBalance, diff)
|
||||||
|
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "不允許修改初始餘額",
|
||||||
|
"code": "INITIAL_BALANCE_IMMUTABLE",
|
||||||
|
"details": gin.H{
|
||||||
|
"current_value": existingTrader.InitialBalance,
|
||||||
|
"requested_value": req.InitialBalance,
|
||||||
|
"difference": diff,
|
||||||
|
"trader_id": traderID,
|
||||||
|
"trader_name": existingTrader.Name,
|
||||||
|
"created_at": existingTrader.CreatedAt,
|
||||||
|
},
|
||||||
|
"message": fmt.Sprintf(
|
||||||
|
"初始餘額是固定的基準值,創建後不可修改。\n\n"+
|
||||||
|
"當前初始餘額: %.2f USDT\n"+
|
||||||
|
"嘗試修改為: %.2f USDT\n\n"+
|
||||||
|
"如確實需要修改,請聯繫管理員。",
|
||||||
|
existingTrader.InitialBalance,
|
||||||
|
req.InitialBalance,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新交易员配置
|
// 更新交易员配置
|
||||||
trader := &config.TraderRecord{
|
trader := &config.TraderRecord{
|
||||||
ID: traderID,
|
ID: traderID,
|
||||||
@@ -534,7 +569,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
AIModelID: req.AIModelID,
|
AIModelID: req.AIModelID,
|
||||||
ExchangeID: req.ExchangeID,
|
ExchangeID: req.ExchangeID,
|
||||||
InitialBalance: req.InitialBalance,
|
InitialBalance: initialBalance,
|
||||||
BTCETHLeverage: btcEthLeverage,
|
BTCETHLeverage: btcEthLeverage,
|
||||||
AltcoinLeverage: altcoinLeverage,
|
AltcoinLeverage: altcoinLeverage,
|
||||||
TradingSymbols: req.TradingSymbols,
|
TradingSymbols: req.TradingSymbols,
|
||||||
@@ -1195,6 +1230,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
|
|||||||
|
|
||||||
// handleEquityHistory 收益率历史数据
|
// handleEquityHistory 收益率历史数据
|
||||||
func (s *Server) handleEquityHistory(c *gin.Context) {
|
func (s *Server) handleEquityHistory(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
_, traderID, err := s.getTraderFromQuery(c)
|
_, traderID, err := s.getTraderFromQuery(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -1229,19 +1265,13 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
|
|||||||
CycleNumber int `json:"cycle_number"`
|
CycleNumber int `json:"cycle_number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从AutoTrader获取初始余额(用于计算盈亏百分比)
|
traderRecord, _, _, err := s.database.GetTraderConfig(userID, traderID)
|
||||||
initialBalance := 0.0
|
if err != nil {
|
||||||
if status := trader.GetStatus(); status != nil {
|
c.JSON(http.StatusNotFound, gin.H{"error": "无法获取交易员配置"})
|
||||||
if ib, ok := status["initial_balance"].(float64); ok && ib > 0 {
|
return
|
||||||
initialBalance = ib
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果无法从status获取,且有历史记录,则从第一条记录获取
|
initialBalance := traderRecord.InitialBalance
|
||||||
if initialBalance == 0 && len(records) > 0 {
|
|
||||||
// 第一条记录的equity作为初始余额
|
|
||||||
initialBalance = records[0].AccountState.TotalBalance
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果还是无法获取,返回错误
|
// 如果还是无法获取,返回错误
|
||||||
if initialBalance == 0 {
|
if initialBalance == 0 {
|
||||||
@@ -1253,16 +1283,9 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
|
|||||||
|
|
||||||
var history []EquityPoint
|
var history []EquityPoint
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
// TotalBalance字段实际存储的是TotalEquity
|
|
||||||
totalEquity := record.AccountState.TotalBalance
|
totalEquity := record.AccountState.TotalBalance
|
||||||
// TotalUnrealizedProfit字段实际存储的是TotalPnL(相对初始余额)
|
totalPnL := totalEquity - initialBalance
|
||||||
totalPnL := record.AccountState.TotalUnrealizedProfit
|
totalPnLPct := (totalPnL / initialBalance) * 100
|
||||||
|
|
||||||
// 计算盈亏百分比
|
|
||||||
totalPnLPct := 0.0
|
|
||||||
if initialBalance > 0 {
|
|
||||||
totalPnLPct = (totalPnL / initialBalance) * 100
|
|
||||||
}
|
|
||||||
|
|
||||||
history = append(history, EquityPoint{
|
history = append(history, EquityPoint{
|
||||||
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
|
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
|
||||||
|
|||||||
@@ -845,12 +845,12 @@ func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error {
|
|||||||
func (d *Database) UpdateTrader(trader *TraderRecord) error {
|
func (d *Database) UpdateTrader(trader *TraderRecord) error {
|
||||||
_, err := d.db.Exec(`
|
_, err := d.db.Exec(`
|
||||||
UPDATE traders SET
|
UPDATE traders SET
|
||||||
name = ?, ai_model_id = ?, exchange_id = ?, initial_balance = ?,
|
name = ?, ai_model_id = ?, exchange_id = ?,
|
||||||
scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?,
|
scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?,
|
||||||
trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?,
|
trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?,
|
||||||
system_prompt_template = ?, is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
|
system_prompt_template = ?, is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ? AND user_id = ?
|
WHERE id = ? AND user_id = ?
|
||||||
`, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance,
|
`, trader.Name, trader.AIModelID, trader.ExchangeID,
|
||||||
trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage,
|
trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage,
|
||||||
trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt,
|
trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt,
|
||||||
trader.SystemPromptTemplate, trader.IsCrossMargin, trader.ID, trader.UserID)
|
trader.SystemPromptTemplate, trader.IsCrossMargin, trader.ID, trader.UserID)
|
||||||
|
|||||||
Reference in New Issue
Block a user