mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 10:31:04 +08:00
Merge pull request #416 from zhouyongyou/fix/bug-fixes-collection-v2
fix: 修復 6 個 bug(替代 #271)
This commit is contained in:
@@ -89,7 +89,7 @@ func (s *Server) setupRoutes() {
|
||||
// 系统提示词模板管理(无需认证)
|
||||
api.GET("/prompt-templates", s.handleGetPromptTemplates)
|
||||
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
|
||||
|
||||
|
||||
// 公开的竞赛数据(无需认证)
|
||||
api.GET("/traders", s.handlePublicTraderList)
|
||||
api.GET("/competition", s.handlePublicCompetition)
|
||||
@@ -169,7 +169,7 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
||||
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
||||
altcoinLeverage = val
|
||||
}
|
||||
|
||||
|
||||
// 获取内测模式配置
|
||||
betaModeStr, _ := s.database.GetSystemConfig("beta_mode")
|
||||
betaMode := betaModeStr == "true"
|
||||
@@ -599,14 +599,14 @@ func (s *Server) handleDeleteTrader(c *gin.Context) {
|
||||
func (s *Server) handleStartTrader(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
traderID := c.Param("id")
|
||||
|
||||
|
||||
// 校验交易员是否属于当前用户
|
||||
_, _, _, err := s.database.GetTraderConfig(userID, traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
trader, err := s.traderManager.GetTrader(traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||
@@ -642,14 +642,14 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
||||
func (s *Server) handleStopTrader(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
traderID := c.Param("id")
|
||||
|
||||
|
||||
// 校验交易员是否属于当前用户
|
||||
_, _, _, err := s.database.GetTraderConfig(userID, traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
trader, err := s.traderManager.GetTrader(traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||
@@ -859,19 +859,12 @@ func (s *Server) handleTraderList(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// AIModelID 应该已经是 provider(如 "deepseek"),直接使用
|
||||
// 如果是旧数据格式(如 "admin_deepseek"),提取 provider 部分
|
||||
aiModelID := trader.AIModelID
|
||||
// 兼容旧数据:如果包含下划线,提取最后一部分作为 provider
|
||||
if strings.Contains(aiModelID, "_") {
|
||||
parts := strings.Split(aiModelID, "_")
|
||||
aiModelID = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// 返回完整的 AIModelID(如 "admin_deepseek"),不要截断
|
||||
// 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致)
|
||||
result = append(result, map[string]interface{}{
|
||||
"trader_id": trader.ID,
|
||||
"trader_name": trader.Name,
|
||||
"ai_model": aiModelID,
|
||||
"ai_model": trader.AIModelID, // 使用完整 ID
|
||||
"exchange_id": trader.ExchangeID,
|
||||
"is_running": isRunning,
|
||||
"initial_balance": trader.InitialBalance,
|
||||
@@ -1649,7 +1642,7 @@ func (s *Server) handlePublicCompetition(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, competition)
|
||||
}
|
||||
|
||||
@@ -1662,7 +1655,7 @@ func (s *Server) handleTopTraders(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, topTraders)
|
||||
}
|
||||
|
||||
@@ -1671,7 +1664,7 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) {
|
||||
var requestBody struct {
|
||||
TraderIDs []string `json:"trader_ids"`
|
||||
}
|
||||
|
||||
|
||||
// 尝试解析POST请求的JSON body
|
||||
if err := c.ShouldBindJSON(&requestBody); err != nil {
|
||||
// 如果JSON解析失败,尝试从query参数获取(兼容GET请求)
|
||||
@@ -1685,13 +1678,13 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
traders, ok := topTraders["traders"].([]map[string]interface{})
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 提取trader IDs
|
||||
traderIDs := make([]string, 0, len(traders))
|
||||
for _, trader := range traders {
|
||||
@@ -1699,24 +1692,24 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) {
|
||||
traderIDs = append(traderIDs, traderID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result := s.getEquityHistoryForTraders(traderIDs)
|
||||
c.JSON(http.StatusOK, result)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 解析逗号分隔的trader IDs
|
||||
requestBody.TraderIDs = strings.Split(traderIDsParam, ",")
|
||||
for i := range requestBody.TraderIDs {
|
||||
requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 限制最多20个交易员,防止请求过大
|
||||
if len(requestBody.TraderIDs) > 20 {
|
||||
requestBody.TraderIDs = requestBody.TraderIDs[:20]
|
||||
}
|
||||
|
||||
|
||||
result := s.getEquityHistoryForTraders(requestBody.TraderIDs)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -1726,31 +1719,31 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter
|
||||
result := make(map[string]interface{})
|
||||
histories := make(map[string]interface{})
|
||||
errors := make(map[string]string)
|
||||
|
||||
|
||||
for _, traderID := range traderIDs {
|
||||
if traderID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
trader, err := s.traderManager.GetTrader(traderID)
|
||||
if err != nil {
|
||||
errors[traderID] = "交易员不存在"
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取历史数据(用于对比展示,限制数据量)
|
||||
records, err := trader.GetDecisionLogger().GetLatestRecords(500)
|
||||
if err != nil {
|
||||
errors[traderID] = fmt.Sprintf("获取历史数据失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 构建收益率历史数据
|
||||
history := make([]map[string]interface{}, 0, len(records))
|
||||
for _, record := range records {
|
||||
// 计算总权益(余额+未实现盈亏)
|
||||
totalEquity := record.AccountState.TotalBalance + record.AccountState.TotalUnrealizedProfit
|
||||
|
||||
|
||||
history = append(history, map[string]interface{}{
|
||||
"timestamp": record.Timestamp,
|
||||
"total_equity": totalEquity,
|
||||
@@ -1758,16 +1751,16 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter
|
||||
"balance": record.AccountState.TotalBalance,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
histories[traderID] = history
|
||||
}
|
||||
|
||||
|
||||
result["histories"] = histories
|
||||
result["count"] = len(histories)
|
||||
if len(errors) > 0 {
|
||||
result["errors"] = errors
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1801,4 +1794,3 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
|
||||
@@ -258,17 +258,17 @@ func (d *Database) initDefaultData() error {
|
||||
|
||||
// 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新
|
||||
systemConfigs := map[string]string{
|
||||
"admin_mode": "true", // 默认开启管理员模式,便于首次使用
|
||||
"beta_mode": "false", // 默认关闭内测模式
|
||||
"api_server_port": "8080", // 默认API端口
|
||||
"use_default_coins": "true", // 默认使用内置币种列表
|
||||
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式)
|
||||
"max_daily_loss": "10.0", // 最大日损失百分比
|
||||
"max_drawdown": "20.0", // 最大回撤百分比
|
||||
"stop_trading_minutes": "60", // 停止交易时间(分钟)
|
||||
"btc_eth_leverage": "5", // BTC/ETH杠杆倍数
|
||||
"altcoin_leverage": "5", // 山寨币杠杆倍数
|
||||
"jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成
|
||||
"admin_mode": "true", // 默认开启管理员模式,便于首次使用
|
||||
"beta_mode": "false", // 默认关闭内测模式
|
||||
"api_server_port": "8080", // 默认API端口
|
||||
"use_default_coins": "true", // 默认使用内置币种列表
|
||||
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式)
|
||||
"max_daily_loss": "10.0", // 最大日损失百分比
|
||||
"max_drawdown": "20.0", // 最大回撤百分比
|
||||
"stop_trading_minutes": "60", // 停止交易时间(分钟)
|
||||
"btc_eth_leverage": "5", // BTC/ETH杠杆倍数
|
||||
"altcoin_leverage": "5", // 山寨币杠杆倍数
|
||||
"jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成
|
||||
}
|
||||
|
||||
for key, value := range systemConfigs {
|
||||
@@ -1037,7 +1037,7 @@ func (d *Database) LoadBetaCodesFromFile(filePath string) error {
|
||||
log.Printf("插入内测码 %s 失败: %v", code, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 {
|
||||
insertedCount++
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -10,7 +10,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sonirico/go-hyperliquid v0.17.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -120,8 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
|
||||
@@ -295,6 +295,8 @@ func isRetryableError(err error) bool {
|
||||
"connection refused",
|
||||
"temporary failure",
|
||||
"no such host",
|
||||
"stream error", // HTTP/2 stream 错误
|
||||
"INTERNAL_ERROR", // 服务端内部错误
|
||||
}
|
||||
for _, retryable := range retryableErrors {
|
||||
if strings.Contains(errStr, retryable) {
|
||||
|
||||
10
start.sh
10
start.sh
@@ -165,6 +165,16 @@ start() {
|
||||
# 读取环境变量
|
||||
read_env_vars
|
||||
|
||||
# 确保必要的文件和目录存在(修复 Docker volume 挂载问题)
|
||||
if [ ! -f "config.db" ]; then
|
||||
print_info "创建数据库文件..."
|
||||
touch config.db
|
||||
fi
|
||||
if [ ! -d "decision_logs" ]; then
|
||||
print_info "创建日志目录..."
|
||||
mkdir -p decision_logs
|
||||
fi
|
||||
|
||||
# Auto-build frontend if missing or forced
|
||||
# if [ ! -d "web/dist" ] || [ "$1" == "--build" ]; then
|
||||
# build_frontend
|
||||
|
||||
@@ -196,7 +196,8 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
// 设置默认系统提示词模板
|
||||
systemPromptTemplate := config.SystemPromptTemplate
|
||||
if systemPromptTemplate == "" {
|
||||
systemPromptTemplate = "default" // 默认使用 default 模板
|
||||
// feature/partial-close-dynamic-tpsl 分支默认使用 adaptive(支持动态止盈止损)
|
||||
systemPromptTemplate = "adaptive"
|
||||
}
|
||||
|
||||
return &AutoTrader{
|
||||
@@ -482,6 +483,12 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
if quantity < 0 {
|
||||
quantity = -quantity // 空仓数量为负,转为正数
|
||||
}
|
||||
|
||||
// 跳过已平仓的持仓(quantity = 0),防止"幽灵持仓"传递给AI
|
||||
if quantity == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
unrealizedPnl := pos["unRealizedProfit"].(float64)
|
||||
liquidationPrice := pos["liquidationPrice"].(float64)
|
||||
|
||||
|
||||
@@ -112,9 +112,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
? validHistory.slice(-MAX_DISPLAY_POINTS)
|
||||
: validHistory
|
||||
|
||||
// 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值)
|
||||
const initialBalance =
|
||||
validHistory[0]?.total_equity || account?.total_equity || 100 // 默认值改为100,与常见配置一致
|
||||
// 计算初始余额(优先从 account 获取配置的初始余额,备选从历史数据反推)
|
||||
const initialBalance = account?.initial_balance // 从交易员配置读取真实初始余额
|
||||
|| (validHistory[0] ? validHistory[0].total_equity - validHistory[0].pnl : undefined) // 备选:淨值 - 盈亏
|
||||
|| 1000; // 默认值(与创建交易员时的默认配置一致)
|
||||
|
||||
// 转换数据格式
|
||||
const chartData = displayHistory.map((point) => {
|
||||
|
||||
Reference in New Issue
Block a user