fix: initial balance calculation and UI improvements

- Fix initial balance using available_balance instead of total_equity
- Fix WSMonitor nil pointer by starting market monitor before loading traders
- Add strategy name display on traders list and dashboard pages
- Various position sync and trading improvements
This commit is contained in:
tinkle-community
2025-12-10 14:40:08 +08:00
parent c19ee51dee
commit 319ccb8ca3
45 changed files with 2951 additions and 3392 deletions

View File

@@ -12,7 +12,6 @@ import (
"time"
"nofx/backtest"
"nofx/decision"
"nofx/store"
"github.com/gin-gonic/gin"
@@ -64,14 +63,6 @@ func (s *Server) handleBacktestStart(c *gin.Context) {
if cfg.RunID == "" {
cfg.RunID = "bt_" + time.Now().UTC().Format("20060102_150405")
}
cfg.PromptTemplate = strings.TrimSpace(cfg.PromptTemplate)
if cfg.PromptTemplate == "" {
cfg.PromptTemplate = "default"
}
if _, err := decision.GetPromptTemplate(cfg.PromptTemplate); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Prompt template does not exist: %s", cfg.PromptTemplate)})
return
}
cfg.CustomPrompt = strings.TrimSpace(cfg.CustomPrompt)
cfg.UserID = normalizeUserID(c.GetString("user_id"))
if err := s.hydrateBacktestAIConfig(&cfg); err != nil {

View File

@@ -10,7 +10,6 @@ import (
"nofx/backtest"
"nofx/config"
"nofx/crypto"
"nofx/decision"
"nofx/logger"
"nofx/manager"
"nofx/store"
@@ -99,10 +98,6 @@ func (s *Server) setupRoutes() {
api.GET("/crypto/public-key", s.cryptoHandler.HandleGetPublicKey)
api.POST("/crypto/decrypt", s.cryptoHandler.HandleDecryptSensitiveData)
// System prompt template management (no authentication required)
api.GET("/prompt-templates", s.handleGetPromptTemplates)
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
// Public competition data (no authentication required)
api.GET("/traders", s.handlePublicTraderList)
api.GET("/competition", s.handlePublicCompetition)
@@ -150,7 +145,6 @@ func (s *Server) setupRoutes() {
protected.GET("/strategies", s.handleGetStrategies)
protected.GET("/strategies/active", s.handleGetActiveStrategy)
protected.GET("/strategies/default-config", s.handleGetDefaultStrategyConfig)
protected.GET("/strategies/templates", s.handleGetPromptTemplates)
protected.POST("/strategies/preview-prompt", s.handlePreviewPrompt)
protected.POST("/strategies/test-run", s.handleStrategyTestRun)
protected.GET("/strategies/:id", s.handleGetStrategy)
@@ -553,25 +547,19 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
if balanceErr != nil {
logger.Infof("⚠️ Failed to query exchange balance, using user input for initial balance: %v", balanceErr)
} else {
// Extract available balance - supports multiple field name formats
if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
// Binance format: availableBalance (camelCase)
actualBalance = availableBalance
logger.Infof("✓ Queried exchange actual balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance)
} else if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
// Other format: available_balance (snake_case)
actualBalance = availableBalance
logger.Infof("✓ Queried exchange actual balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance)
} else if totalBalance, ok := balanceInfo["totalWalletBalance"].(float64); ok && totalBalance > 0 {
// Binance format: totalWalletBalance (camelCase)
actualBalance = totalBalance
logger.Infof("✓ Queried exchange total balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance)
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
// Other format: balance
actualBalance = totalBalance
logger.Infof("✓ Queried exchange actual balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance)
} else {
logger.Infof("⚠️ Unable to extract available balance from balance info, balanceInfo=%v, using user input for initial balance", balanceInfo)
// Extract total equity (account total value = wallet balance + unrealized PnL)
// Priority: total_equity > totalWalletBalance > wallet_balance > totalEq > balance
// Note: Must use total_equity (not availableBalance) for accurate P&L calculation
balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"}
for _, key := range balanceKeys {
if balance, ok := balanceInfo[key].(float64); ok && balance > 0 {
actualBalance = balance
logger.Infof("✓ Queried exchange total equity (%s): %.2f USDT (user input: %.2f USDT)", key, actualBalance, req.InitialBalance)
break
}
}
if actualBalance <= 0 {
logger.Infof("⚠️ Unable to extract total equity from balance info, balanceInfo=%v, using user input for initial balance", balanceInfo)
}
}
}
@@ -1002,16 +990,18 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
return
}
// Extract available balance
// Extract total equity (for P&L calculation, we need total account value, not available balance)
var actualBalance float64
if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
actualBalance = totalBalance
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to get available balance"})
// Priority: total_equity > totalWalletBalance > wallet_balance > totalEq > balance
balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"}
for _, key := range balanceKeys {
if balance, ok := balanceInfo[key].(float64); ok && balance > 0 {
actualBalance = balance
break
}
}
if actualBalance <= 0 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to get total equity"})
return
}
@@ -1438,6 +1428,14 @@ func (s *Server) handleTraderList(c *gin.Context) {
}
}
// Get strategy name if strategy_id is set
var strategyName string
if trader.StrategyID != "" {
if strategy, err := s.store.Strategy().Get(userID, trader.StrategyID); err == nil {
strategyName = strategy.Name
}
}
// Return complete AIModelID (e.g. "admin_deepseek"), don't truncate
// Frontend needs complete ID to verify model exists (consistent with handleGetTraderConfig)
result = append(result, map[string]interface{}{
@@ -1447,6 +1445,8 @@ func (s *Server) handleTraderList(c *gin.Context) {
"exchange_id": trader.ExchangeID,
"is_running": isRunning,
"initial_balance": trader.InitialBalance,
"strategy_id": trader.StrategyID,
"strategy_name": strategyName,
})
}
@@ -2142,40 +2142,6 @@ func (s *Server) Shutdown() error {
return s.httpServer.Shutdown(ctx)
}
// handleGetPromptTemplates Get all system prompt template list
func (s *Server) handleGetPromptTemplates(c *gin.Context) {
// Import decision package
templates := decision.GetAllPromptTemplates()
// Convert to response format
response := make([]map[string]interface{}, 0, len(templates))
for _, tmpl := range templates {
response = append(response, map[string]interface{}{
"name": tmpl.Name,
})
}
c.JSON(http.StatusOK, gin.H{
"templates": response,
})
}
// handleGetPromptTemplate Get prompt template content by specified name
func (s *Server) handleGetPromptTemplate(c *gin.Context) {
templateName := c.Param("name")
template, err := decision.GetPromptTemplate(templateName)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Template does not exist: %s", templateName)})
return
}
c.JSON(http.StatusOK, gin.H{
"name": template.Name,
"content": template.Content,
})
}
// handlePublicTraderList Get public trader list (no authentication required)
func (s *Server) handlePublicTraderList(c *gin.Context) {
// Get trader information from all users

View File

@@ -361,13 +361,9 @@ func (s *Server) handlePreviewPrompt(c *gin.Context) {
req.PromptVariant,
)
// Get list of available prompt templates
templateNames := decision.GetAllPromptTemplateNames()
c.JSON(http.StatusOK, gin.H{
"system_prompt": systemPrompt,
"prompt_variant": req.PromptVariant,
"available_templates": templateNames,
"system_prompt": systemPrompt,
"prompt_variant": req.PromptVariant,
"config_summary": gin.H{
"coin_source": req.Config.CoinSource.SourceType,
"primary_tf": req.Config.Indicators.Klines.PrimaryTimeframe,
@@ -455,7 +451,7 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
// Build real context (for generating User Prompt)
testContext := &decision.Context{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
RuntimeMinutes: 0,
CallCount: 1,
Account: decision.AccountInfo{