mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
313
api/server.go
313
api/server.go
@@ -1,12 +1,14 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"nofx/auth"
|
"nofx/auth"
|
||||||
"nofx/config"
|
"nofx/config"
|
||||||
"nofx/manager"
|
"nofx/manager"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -87,7 +89,9 @@ func (s *Server) setupRoutes() {
|
|||||||
{
|
{
|
||||||
// AI交易员管理
|
// AI交易员管理
|
||||||
protected.GET("/traders", s.handleTraderList)
|
protected.GET("/traders", s.handleTraderList)
|
||||||
|
protected.GET("/traders/:id/config", s.handleGetTraderConfig)
|
||||||
protected.POST("/traders", s.handleCreateTrader)
|
protected.POST("/traders", s.handleCreateTrader)
|
||||||
|
protected.PUT("/traders/:id", s.handleUpdateTrader)
|
||||||
protected.DELETE("/traders/:id", s.handleDeleteTrader)
|
protected.DELETE("/traders/:id", s.handleDeleteTrader)
|
||||||
protected.POST("/traders/:id/start", s.handleStartTrader)
|
protected.POST("/traders/:id/start", s.handleStartTrader)
|
||||||
protected.POST("/traders/:id/stop", s.handleStopTrader)
|
protected.POST("/traders/:id/stop", s.handleStopTrader)
|
||||||
@@ -101,6 +105,10 @@ func (s *Server) setupRoutes() {
|
|||||||
protected.GET("/exchanges", s.handleGetExchangeConfigs)
|
protected.GET("/exchanges", s.handleGetExchangeConfigs)
|
||||||
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
|
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
|
||||||
|
|
||||||
|
// 用户信号源配置
|
||||||
|
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
|
||||||
|
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
|
||||||
|
|
||||||
// 竞赛总览
|
// 竞赛总览
|
||||||
protected.GET("/competition", s.handleCompetition)
|
protected.GET("/competition", s.handleCompetition)
|
||||||
|
|
||||||
@@ -127,8 +135,36 @@ func (s *Server) handleHealth(c *gin.Context) {
|
|||||||
|
|
||||||
// handleGetSystemConfig 获取系统配置(客户端需要知道的配置)
|
// handleGetSystemConfig 获取系统配置(客户端需要知道的配置)
|
||||||
func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
||||||
|
// 获取默认币种
|
||||||
|
defaultCoinsStr, _ := s.database.GetSystemConfig("default_coins")
|
||||||
|
var defaultCoins []string
|
||||||
|
if defaultCoinsStr != "" {
|
||||||
|
json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins)
|
||||||
|
}
|
||||||
|
if len(defaultCoins) == 0 {
|
||||||
|
// 使用硬编码的默认币种
|
||||||
|
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取杠杆配置
|
||||||
|
btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage")
|
||||||
|
altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage")
|
||||||
|
|
||||||
|
btcEthLeverage := 5
|
||||||
|
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
|
||||||
|
btcEthLeverage = val
|
||||||
|
}
|
||||||
|
|
||||||
|
altcoinLeverage := 5
|
||||||
|
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
||||||
|
altcoinLeverage = val
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"admin_mode": auth.IsAdminMode(),
|
"admin_mode": auth.IsAdminMode(),
|
||||||
|
"default_coins": defaultCoins,
|
||||||
|
"btc_eth_leverage": btcEthLeverage,
|
||||||
|
"altcoin_leverage": altcoinLeverage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,21 +200,27 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str
|
|||||||
|
|
||||||
// AI交易员管理相关结构体
|
// AI交易员管理相关结构体
|
||||||
type CreateTraderRequest struct {
|
type CreateTraderRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
AIModelID string `json:"ai_model_id" binding:"required"`
|
AIModelID string `json:"ai_model_id" binding:"required"`
|
||||||
ExchangeID string `json:"exchange_id" binding:"required"`
|
ExchangeID string `json:"exchange_id" binding:"required"`
|
||||||
InitialBalance float64 `json:"initial_balance"`
|
InitialBalance float64 `json:"initial_balance"`
|
||||||
CustomPrompt string `json:"custom_prompt"`
|
BTCETHLeverage int `json:"btc_eth_leverage"`
|
||||||
|
AltcoinLeverage int `json:"altcoin_leverage"`
|
||||||
|
TradingSymbols string `json:"trading_symbols"`
|
||||||
|
CustomPrompt string `json:"custom_prompt"`
|
||||||
OverrideBasePrompt bool `json:"override_base_prompt"`
|
OverrideBasePrompt bool `json:"override_base_prompt"`
|
||||||
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型,nil表示使用默认值true
|
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型,nil表示使用默认值true
|
||||||
|
UseCoinPool bool `json:"use_coin_pool"`
|
||||||
|
UseOITop bool `json:"use_oi_top"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelConfig struct {
|
type ModelConfig struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
APIKey string `json:"apiKey,omitempty"`
|
APIKey string `json:"apiKey,omitempty"`
|
||||||
|
CustomAPIURL string `json:"customApiUrl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExchangeConfig struct {
|
type ExchangeConfig struct {
|
||||||
@@ -193,8 +235,9 @@ type ExchangeConfig struct {
|
|||||||
|
|
||||||
type UpdateModelConfigRequest struct {
|
type UpdateModelConfigRequest struct {
|
||||||
Models map[string]struct {
|
Models map[string]struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
|
CustomAPIURL string `json:"custom_api_url"`
|
||||||
} `json:"models"`
|
} `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +263,28 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验杠杆值
|
||||||
|
if req.BTCETHLeverage < 0 || req.BTCETHLeverage > 50 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "BTC/ETH杠杆必须在1-50倍之间"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.AltcoinLeverage < 0 || req.AltcoinLeverage > 20 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "山寨币杠杆必须在1-20倍之间"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验交易币种格式
|
||||||
|
if req.TradingSymbols != "" {
|
||||||
|
symbols := strings.Split(req.TradingSymbols, ",")
|
||||||
|
for _, symbol := range symbols {
|
||||||
|
symbol = strings.TrimSpace(symbol)
|
||||||
|
if symbol != "" && !strings.HasSuffix(strings.ToUpper(symbol), "USDT") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("无效的币种格式: %s,必须以USDT结尾", symbol)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成交易员ID
|
// 生成交易员ID
|
||||||
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
|
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
|
||||||
|
|
||||||
@@ -229,6 +294,30 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
isCrossMargin = *req.IsCrossMargin
|
isCrossMargin = *req.IsCrossMargin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置杠杆默认值(从系统配置获取)
|
||||||
|
btcEthLeverage := 5
|
||||||
|
altcoinLeverage := 5
|
||||||
|
if req.BTCETHLeverage > 0 {
|
||||||
|
btcEthLeverage = req.BTCETHLeverage
|
||||||
|
} else {
|
||||||
|
// 从系统配置获取默认值
|
||||||
|
if btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage"); btcEthLeverageStr != "" {
|
||||||
|
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
|
||||||
|
btcEthLeverage = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.AltcoinLeverage > 0 {
|
||||||
|
altcoinLeverage = req.AltcoinLeverage
|
||||||
|
} else {
|
||||||
|
// 从系统配置获取默认值
|
||||||
|
if altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage"); altcoinLeverageStr != "" {
|
||||||
|
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
||||||
|
altcoinLeverage = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建交易员配置(数据库实体)
|
// 创建交易员配置(数据库实体)
|
||||||
trader := &config.TraderRecord{
|
trader := &config.TraderRecord{
|
||||||
ID: traderID,
|
ID: traderID,
|
||||||
@@ -237,6 +326,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
AIModelID: req.AIModelID,
|
AIModelID: req.AIModelID,
|
||||||
ExchangeID: req.ExchangeID,
|
ExchangeID: req.ExchangeID,
|
||||||
InitialBalance: req.InitialBalance,
|
InitialBalance: req.InitialBalance,
|
||||||
|
BTCETHLeverage: btcEthLeverage,
|
||||||
|
AltcoinLeverage: altcoinLeverage,
|
||||||
|
TradingSymbols: req.TradingSymbols,
|
||||||
|
UseCoinPool: req.UseCoinPool,
|
||||||
|
UseOITop: req.UseOITop,
|
||||||
CustomPrompt: req.CustomPrompt,
|
CustomPrompt: req.CustomPrompt,
|
||||||
OverrideBasePrompt: req.OverrideBasePrompt,
|
OverrideBasePrompt: req.OverrideBasePrompt,
|
||||||
IsCrossMargin: isCrossMargin,
|
IsCrossMargin: isCrossMargin,
|
||||||
@@ -268,6 +362,108 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTraderRequest 更新交易员请求
|
||||||
|
type UpdateTraderRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
AIModelID string `json:"ai_model_id" binding:"required"`
|
||||||
|
ExchangeID string `json:"exchange_id" binding:"required"`
|
||||||
|
InitialBalance float64 `json:"initial_balance"`
|
||||||
|
BTCETHLeverage int `json:"btc_eth_leverage"`
|
||||||
|
AltcoinLeverage int `json:"altcoin_leverage"`
|
||||||
|
TradingSymbols string `json:"trading_symbols"`
|
||||||
|
CustomPrompt string `json:"custom_prompt"`
|
||||||
|
OverrideBasePrompt bool `json:"override_base_prompt"`
|
||||||
|
IsCrossMargin *bool `json:"is_cross_margin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdateTrader 更新交易员配置
|
||||||
|
func (s *Server) handleUpdateTrader(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
traderID := c.Param("id")
|
||||||
|
|
||||||
|
var req UpdateTraderRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查交易员是否存在且属于当前用户
|
||||||
|
traders, err := s.database.GetTraders(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingTrader *config.TraderRecord
|
||||||
|
for _, trader := range traders {
|
||||||
|
if trader.ID == traderID {
|
||||||
|
existingTrader = trader
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingTrader == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
isCrossMargin := existingTrader.IsCrossMargin // 保持原值
|
||||||
|
if req.IsCrossMargin != nil {
|
||||||
|
isCrossMargin = *req.IsCrossMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置杠杆默认值
|
||||||
|
btcEthLeverage := req.BTCETHLeverage
|
||||||
|
altcoinLeverage := req.AltcoinLeverage
|
||||||
|
if btcEthLeverage <= 0 {
|
||||||
|
btcEthLeverage = existingTrader.BTCETHLeverage // 保持原值
|
||||||
|
}
|
||||||
|
if altcoinLeverage <= 0 {
|
||||||
|
altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新交易员配置
|
||||||
|
trader := &config.TraderRecord{
|
||||||
|
ID: traderID,
|
||||||
|
UserID: userID,
|
||||||
|
Name: req.Name,
|
||||||
|
AIModelID: req.AIModelID,
|
||||||
|
ExchangeID: req.ExchangeID,
|
||||||
|
InitialBalance: req.InitialBalance,
|
||||||
|
BTCETHLeverage: btcEthLeverage,
|
||||||
|
AltcoinLeverage: altcoinLeverage,
|
||||||
|
TradingSymbols: req.TradingSymbols,
|
||||||
|
CustomPrompt: req.CustomPrompt,
|
||||||
|
OverrideBasePrompt: req.OverrideBasePrompt,
|
||||||
|
IsCrossMargin: isCrossMargin,
|
||||||
|
ScanIntervalMinutes: existingTrader.ScanIntervalMinutes, // 保持原值
|
||||||
|
IsRunning: existingTrader.IsRunning, // 保持原值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据库
|
||||||
|
err = s.database.UpdateTrader(trader)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易员失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载交易员到内存
|
||||||
|
err = s.traderManager.LoadUserTraders(s.database, userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✓ 更新交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"trader_id": traderID,
|
||||||
|
"trader_name": req.Name,
|
||||||
|
"ai_model": req.AIModelID,
|
||||||
|
"message": "交易员更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// handleDeleteTrader 删除交易员
|
// handleDeleteTrader 删除交易员
|
||||||
func (s *Server) handleDeleteTrader(c *gin.Context) {
|
func (s *Server) handleDeleteTrader(c *gin.Context) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
@@ -419,7 +615,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
|||||||
|
|
||||||
// 更新每个模型的配置
|
// 更新每个模型的配置
|
||||||
for modelID, modelData := range req.Models {
|
for modelID, modelData := range req.Models {
|
||||||
err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey)
|
err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
|
||||||
return
|
return
|
||||||
@@ -467,6 +663,48 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
|
c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetUserSignalSource 获取用户信号源配置
|
||||||
|
func (s *Server) handleGetUserSignalSource(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
source, err := s.database.GetUserSignalSource(userID)
|
||||||
|
if err != nil {
|
||||||
|
// 如果配置不存在,返回空配置而不是404错误
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"coin_pool_url": "",
|
||||||
|
"oi_top_url": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"coin_pool_url": source.CoinPoolURL,
|
||||||
|
"oi_top_url": source.OITopURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSaveUserSignalSource 保存用户信号源配置
|
||||||
|
func (s *Server) handleSaveUserSignalSource(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
var req struct {
|
||||||
|
CoinPoolURL string `json:"coin_pool_url"`
|
||||||
|
OITopURL string `json:"oi_top_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"})
|
||||||
|
}
|
||||||
|
|
||||||
// handleTraderList trader列表
|
// handleTraderList trader列表
|
||||||
func (s *Server) handleTraderList(c *gin.Context) {
|
func (s *Server) handleTraderList(c *gin.Context) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
@@ -500,6 +738,51 @@ func (s *Server) handleTraderList(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetTraderConfig 获取交易员详细配置
|
||||||
|
func (s *Server) handleGetTraderConfig(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
traderID := c.Param("id")
|
||||||
|
|
||||||
|
if traderID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traderConfig, _, _, err := s.database.GetTraderConfig(userID, traderID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("获取交易员配置失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实时运行状态
|
||||||
|
isRunning := traderConfig.IsRunning
|
||||||
|
if at, err := s.traderManager.GetTrader(traderID); err == nil {
|
||||||
|
status := at.GetStatus()
|
||||||
|
if running, ok := status["is_running"].(bool); ok {
|
||||||
|
isRunning = running
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"trader_id": traderConfig.ID,
|
||||||
|
"trader_name": traderConfig.Name,
|
||||||
|
"ai_model": traderConfig.AIModelID,
|
||||||
|
"exchange_id": traderConfig.ExchangeID,
|
||||||
|
"initial_balance": traderConfig.InitialBalance,
|
||||||
|
"btc_eth_leverage": traderConfig.BTCETHLeverage,
|
||||||
|
"altcoin_leverage": traderConfig.AltcoinLeverage,
|
||||||
|
"trading_symbols": traderConfig.TradingSymbols,
|
||||||
|
"custom_prompt": traderConfig.CustomPrompt,
|
||||||
|
"override_base_prompt": traderConfig.OverrideBasePrompt,
|
||||||
|
"is_cross_margin": traderConfig.IsCrossMargin,
|
||||||
|
"use_coin_pool": traderConfig.UseCoinPool,
|
||||||
|
"use_oi_top": traderConfig.UseOITop,
|
||||||
|
"is_running": isRunning,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
// handleStatus 系统状态
|
// handleStatus 系统状态
|
||||||
func (s *Server) handleStatus(c *gin.Context) {
|
func (s *Server) handleStatus(c *gin.Context) {
|
||||||
_, traderID, err := s.getTraderFromQuery(c)
|
_, traderID, err := s.getTraderFromQuery(c)
|
||||||
@@ -668,7 +951,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
|
|||||||
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
competition, err := s.traderManager.GetCompetitionData(userID)
|
competition, err := s.traderManager.GetCompetitionData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": fmt.Sprintf("获取竞赛数据失败: %v", err),
|
"error": fmt.Sprintf("获取竞赛数据失败: %v", err),
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
"ADAUSDT",
|
"ADAUSDT",
|
||||||
"HYPEUSDT"
|
"HYPEUSDT"
|
||||||
],
|
],
|
||||||
"coin_pool_api_url": "",
|
|
||||||
"oi_top_api_url": "",
|
|
||||||
"api_server_port": 8080,
|
"api_server_port": 8080,
|
||||||
"max_daily_loss": 10.0,
|
"max_daily_loss": 10.0,
|
||||||
"max_drawdown": 20.0,
|
"max_drawdown": 20.0,
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ type Config struct {
|
|||||||
Traders []TraderConfig `json:"traders"`
|
Traders []TraderConfig `json:"traders"`
|
||||||
UseDefaultCoins bool `json:"use_default_coins"` // 是否使用默认主流币种列表
|
UseDefaultCoins bool `json:"use_default_coins"` // 是否使用默认主流币种列表
|
||||||
DefaultCoins []string `json:"default_coins"` // 默认主流币种池
|
DefaultCoins []string `json:"default_coins"` // 默认主流币种池
|
||||||
CoinPoolAPIURL string `json:"coin_pool_api_url"`
|
|
||||||
OITopAPIURL string `json:"oi_top_api_url"`
|
|
||||||
APIServerPort int `json:"api_server_port"`
|
APIServerPort int `json:"api_server_port"`
|
||||||
MaxDailyLoss float64 `json:"max_daily_loss"`
|
MaxDailyLoss float64 `json:"max_daily_loss"`
|
||||||
MaxDrawdown float64 `json:"max_drawdown"`
|
MaxDrawdown float64 `json:"max_drawdown"`
|
||||||
@@ -76,8 +74,8 @@ func LoadConfig(filename string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置默认值:如果use_default_coins未设置(为false)且没有配置coin_pool_api_url,则默认使用默认币种列表
|
// 设置默认值:确保使用默认币种列表
|
||||||
if !config.UseDefaultCoins && config.CoinPoolAPIURL == "" {
|
if !config.UseDefaultCoins {
|
||||||
config.UseDefaultCoins = true
|
config.UseDefaultCoins = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ func (d *Database) createTables() error {
|
|||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
)`,
|
)`,
|
||||||
|
|
||||||
|
// 用户信号源配置表
|
||||||
|
`CREATE TABLE IF NOT EXISTS user_signal_sources (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
coin_pool_url TEXT DEFAULT '',
|
||||||
|
oi_top_url TEXT DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(user_id)
|
||||||
|
)`,
|
||||||
|
|
||||||
// 交易员配置表
|
// 交易员配置表
|
||||||
`CREATE TABLE IF NOT EXISTS traders (
|
`CREATE TABLE IF NOT EXISTS traders (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -82,6 +94,11 @@ func (d *Database) createTables() error {
|
|||||||
initial_balance REAL NOT NULL,
|
initial_balance REAL NOT NULL,
|
||||||
scan_interval_minutes INTEGER DEFAULT 3,
|
scan_interval_minutes INTEGER DEFAULT 3,
|
||||||
is_running BOOLEAN DEFAULT 0,
|
is_running BOOLEAN DEFAULT 0,
|
||||||
|
btc_eth_leverage INTEGER DEFAULT 5,
|
||||||
|
altcoin_leverage INTEGER DEFAULT 5,
|
||||||
|
trading_symbols TEXT DEFAULT '',
|
||||||
|
use_coin_pool BOOLEAN DEFAULT 0,
|
||||||
|
use_oi_top BOOLEAN DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -132,6 +149,12 @@ func (d *Database) createTables() error {
|
|||||||
UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||||
END`,
|
END`,
|
||||||
|
|
||||||
|
`CREATE TRIGGER IF NOT EXISTS update_user_signal_sources_updated_at
|
||||||
|
AFTER UPDATE ON user_signal_sources
|
||||||
|
BEGIN
|
||||||
|
UPDATE user_signal_sources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||||
|
END`,
|
||||||
|
|
||||||
`CREATE TRIGGER IF NOT EXISTS update_system_config_updated_at
|
`CREATE TRIGGER IF NOT EXISTS update_system_config_updated_at
|
||||||
AFTER UPDATE ON system_config
|
AFTER UPDATE ON system_config
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -154,6 +177,14 @@ func (d *Database) createTables() error {
|
|||||||
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
|
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
|
||||||
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
|
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
|
||||||
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式
|
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式
|
||||||
|
`ALTER TABLE traders ADD COLUMN use_default_coins BOOLEAN DEFAULT 1`, // 默认使用默认币种
|
||||||
|
`ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT ''`, // 自定义币种列表(JSON格式)
|
||||||
|
`ALTER TABLE traders ADD COLUMN btc_eth_leverage INTEGER DEFAULT 5`, // BTC/ETH杠杆倍数
|
||||||
|
`ALTER TABLE traders ADD COLUMN altcoin_leverage INTEGER DEFAULT 5`, // 山寨币杠杆倍数
|
||||||
|
`ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔
|
||||||
|
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源
|
||||||
|
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源
|
||||||
|
`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range alterQueries {
|
for _, query := range alterQueries {
|
||||||
@@ -215,8 +246,6 @@ func (d *Database) initDefaultData() error {
|
|||||||
"api_server_port": "8080", // 默认API端口
|
"api_server_port": "8080", // 默认API端口
|
||||||
"use_default_coins": "true", // 默认使用内置币种列表
|
"use_default_coins": "true", // 默认使用内置币种列表
|
||||||
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式)
|
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式)
|
||||||
"coin_pool_api_url": "", // 币种池API URL,默认为空
|
|
||||||
"oi_top_api_url": "", // 持仓量API URL,默认为空
|
|
||||||
"max_daily_loss": "10.0", // 最大日损失百分比
|
"max_daily_loss": "10.0", // 最大日损失百分比
|
||||||
"max_drawdown": "20.0", // 最大回撤百分比
|
"max_drawdown": "20.0", // 最大回撤百分比
|
||||||
"stop_trading_minutes": "60", // 停止交易时间(分钟)
|
"stop_trading_minutes": "60", // 停止交易时间(分钟)
|
||||||
@@ -333,14 +362,15 @@ type User struct {
|
|||||||
|
|
||||||
// AIModelConfig AI模型配置
|
// AIModelConfig AI模型配置
|
||||||
type AIModelConfig struct {
|
type AIModelConfig struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CustomAPIURL string `json:"customApiUrl"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExchangeConfig 交易所配置
|
// ExchangeConfig 交易所配置
|
||||||
@@ -373,13 +403,28 @@ type TraderRecord struct {
|
|||||||
InitialBalance float64 `json:"initial_balance"`
|
InitialBalance float64 `json:"initial_balance"`
|
||||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||||
IsRunning bool `json:"is_running"`
|
IsRunning bool `json:"is_running"`
|
||||||
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数
|
||||||
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数
|
||||||
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓)
|
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
|
||||||
|
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
|
||||||
|
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
|
||||||
|
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
||||||
|
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
||||||
|
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓)
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserSignalSource 用户信号源配置
|
||||||
|
type UserSignalSource struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
CoinPoolURL string `json:"coin_pool_url"`
|
||||||
|
OITopURL string `json:"oi_top_url"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateOTPSecret 生成OTP密钥
|
// GenerateOTPSecret 生成OTP密钥
|
||||||
func GenerateOTPSecret() (string, error) {
|
func GenerateOTPSecret() (string, error) {
|
||||||
secret := make([]byte, 20)
|
secret := make([]byte, 20)
|
||||||
@@ -457,6 +502,25 @@ func (d *Database) GetUserByID(userID string) (*User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllUsers 获取所有用户ID列表
|
||||||
|
func (d *Database) GetAllUsers() ([]string, error) {
|
||||||
|
rows, err := d.db.Query(`SELECT id FROM users ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var userIDs []string
|
||||||
|
for rows.Next() {
|
||||||
|
var userID string
|
||||||
|
if err := rows.Scan(&userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userIDs = append(userIDs, userID)
|
||||||
|
}
|
||||||
|
return userIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUserOTPVerified 更新用户OTP验证状态
|
// UpdateUserOTPVerified 更新用户OTP验证状态
|
||||||
func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
|
func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
|
||||||
_, err := d.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID)
|
_, err := d.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID)
|
||||||
@@ -466,7 +530,7 @@ func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
|
|||||||
// GetAIModels 获取用户的AI模型配置
|
// GetAIModels 获取用户的AI模型配置
|
||||||
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||||||
rows, err := d.db.Query(`
|
rows, err := d.db.Query(`
|
||||||
SELECT id, user_id, name, provider, enabled, api_key, created_at, updated_at
|
SELECT id, user_id, name, provider, enabled, api_key, COALESCE(custom_api_url, '') as custom_api_url, created_at, updated_at
|
||||||
FROM ai_models WHERE user_id = ? ORDER BY id
|
FROM ai_models WHERE user_id = ? ORDER BY id
|
||||||
`, userID)
|
`, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -480,7 +544,7 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
|||||||
var model AIModelConfig
|
var model AIModelConfig
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
||||||
&model.Enabled, &model.APIKey,
|
&model.Enabled, &model.APIKey, &model.CustomAPIURL,
|
||||||
&model.CreatedAt, &model.UpdatedAt,
|
&model.CreatedAt, &model.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -493,11 +557,11 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置
|
// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置
|
||||||
func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey string) error {
|
func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL string) error {
|
||||||
// 首先尝试更新现有的用户配置
|
// 首先尝试更新现有的用户配置
|
||||||
result, err := d.db.Exec(`
|
result, err := d.db.Exec(`
|
||||||
UPDATE ai_models SET enabled = ?, api_key = ? WHERE id = ? AND user_id = ?
|
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ? WHERE id = ? AND user_id = ?
|
||||||
`, enabled, apiKey, id, userID)
|
`, enabled, apiKey, customAPIURL, id, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -532,9 +596,9 @@ func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey string)
|
|||||||
// 创建用户特定的配置
|
// 创建用户特定的配置
|
||||||
userModelID := fmt.Sprintf("%s_%s", userID, id)
|
userModelID := fmt.Sprintf("%s_%s", userID, id)
|
||||||
_, err = d.db.Exec(`
|
_, err = d.db.Exec(`
|
||||||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, created_at, updated_at)
|
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||||
`, userModelID, userID, name, provider, enabled, apiKey)
|
`, userModelID, userID, name, provider, enabled, apiKey, customAPIURL)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,11 +707,11 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateAIModel 创建AI模型配置
|
// CreateAIModel 创建AI模型配置
|
||||||
func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey string) error {
|
func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
|
||||||
_, err := d.db.Exec(`
|
_, err := d.db.Exec(`
|
||||||
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key)
|
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`, id, userID, name, provider, enabled, apiKey)
|
`, id, userID, name, provider, enabled, apiKey, customAPIURL)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,9 +727,9 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap
|
|||||||
// CreateTrader 创建交易员
|
// CreateTrader 创建交易员
|
||||||
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
||||||
_, err := d.db.Exec(`
|
_, err := d.db.Exec(`
|
||||||
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, custom_prompt, override_base_prompt, is_cross_margin)
|
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, is_cross_margin)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.CustomPrompt, trader.OverrideBasePrompt, trader.IsCrossMargin)
|
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.IsCrossMargin)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,6 +737,9 @@ func (d *Database) CreateTrader(trader *TraderRecord) error {
|
|||||||
func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
||||||
rows, err := d.db.Query(`
|
rows, err := d.db.Query(`
|
||||||
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
|
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
|
||||||
|
COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
|
||||||
|
COALESCE(trading_symbols, '') as trading_symbols,
|
||||||
|
COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,
|
||||||
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
|
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
|
||||||
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
||||||
FROM traders WHERE user_id = ? ORDER BY created_at DESC
|
FROM traders WHERE user_id = ? ORDER BY created_at DESC
|
||||||
@@ -688,6 +755,8 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
|||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||||||
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
||||||
|
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
|
||||||
|
&trader.UseCoinPool, &trader.UseOITop,
|
||||||
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin,
|
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin,
|
||||||
&trader.CreatedAt, &trader.UpdatedAt,
|
&trader.CreatedAt, &trader.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -706,6 +775,22 @@ func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTrader 更新交易员配置
|
||||||
|
func (d *Database) UpdateTrader(trader *TraderRecord) error {
|
||||||
|
_, err := d.db.Exec(`
|
||||||
|
UPDATE traders SET
|
||||||
|
name = ?, ai_model_id = ?, exchange_id = ?, initial_balance = ?,
|
||||||
|
scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?,
|
||||||
|
trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?,
|
||||||
|
is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
`, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance,
|
||||||
|
trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage,
|
||||||
|
trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt,
|
||||||
|
trader.IsCrossMargin, trader.ID, trader.UserID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateTraderCustomPrompt 更新交易员自定义Prompt
|
// UpdateTraderCustomPrompt 更新交易员自定义Prompt
|
||||||
func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error {
|
func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error {
|
||||||
_, err := d.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID)
|
_, err := d.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID)
|
||||||
@@ -772,6 +857,40 @@ func (d *Database) SetSystemConfig(key, value string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUserSignalSource 创建用户信号源配置
|
||||||
|
func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
|
||||||
|
_, err := d.db.Exec(`
|
||||||
|
INSERT OR REPLACE INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at)
|
||||||
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`, userID, coinPoolURL, oiTopURL)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserSignalSource 获取用户信号源配置
|
||||||
|
func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) {
|
||||||
|
var source UserSignalSource
|
||||||
|
err := d.db.QueryRow(`
|
||||||
|
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
|
||||||
|
FROM user_signal_sources WHERE user_id = ?
|
||||||
|
`, userID).Scan(
|
||||||
|
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
|
||||||
|
&source.CreatedAt, &source.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserSignalSource 更新用户信号源配置
|
||||||
|
func (d *Database) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
|
||||||
|
_, err := d.db.Exec(`
|
||||||
|
UPDATE user_signal_sources SET coin_pool_url = ?, oi_top_url = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, coinPoolURL, oiTopURL, userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Close 关闭数据库连接
|
// Close 关闭数据库连接
|
||||||
func (d *Database) Close() error {
|
func (d *Database) Close() error {
|
||||||
return d.db.Close()
|
return d.db.Close()
|
||||||
|
|||||||
25
main.go
25
main.go
@@ -208,26 +208,37 @@ func main() {
|
|||||||
log.Fatalf("❌ 加载交易员失败: %v", err)
|
log.Fatalf("❌ 加载交易员失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库中的所有交易员配置(用于显示,使用default用户)
|
// 获取所有用户的交易员配置(用于显示)
|
||||||
traders, err := database.GetTraders("default")
|
userIDs, err := database.GetAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("❌ 获取交易员列表失败: %v", err)
|
log.Printf("⚠️ 获取用户列表失败: %v", err)
|
||||||
|
userIDs = []string{"default"} // 回退到default用户
|
||||||
|
}
|
||||||
|
|
||||||
|
var allTraders []*config.TraderRecord
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
traders, err := database.GetTraders(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ 获取用户 %s 的交易员失败: %v", userID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allTraders = append(allTraders, traders...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载的交易员信息
|
// 显示加载的交易员信息
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("🤖 数据库中的AI交易员配置:")
|
fmt.Println("🤖 数据库中的AI交易员配置:")
|
||||||
if len(traders) == 0 {
|
if len(allTraders) == 0 {
|
||||||
fmt.Println(" • 暂无配置的交易员,请通过Web界面创建")
|
fmt.Println(" • 暂无配置的交易员,请通过Web界面创建")
|
||||||
} else {
|
} else {
|
||||||
for _, trader := range traders {
|
for _, trader := range allTraders {
|
||||||
status := "停止"
|
status := "停止"
|
||||||
if trader.IsRunning {
|
if trader.IsRunning {
|
||||||
status = "运行中"
|
status = "运行中"
|
||||||
}
|
}
|
||||||
fmt.Printf(" • %s (%s + %s) - 初始资金: %.0f USDT [%s]\n",
|
fmt.Printf(" • %s (%s + %s) - 用户: %s - 初始资金: %.0f USDT [%s]\n",
|
||||||
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
|
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
|
||||||
trader.InitialBalance, status)
|
trader.UserID, trader.InitialBalance, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"nofx/config"
|
"nofx/config"
|
||||||
"nofx/trader"
|
"nofx/trader"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -28,28 +30,33 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
// 根据admin_mode确定用户ID
|
// 获取所有用户
|
||||||
adminModeStr, _ := database.GetSystemConfig("admin_mode")
|
userIDs, err := database.GetAllUsers()
|
||||||
userID := "default"
|
|
||||||
if adminModeStr != "false" { // 默认为true
|
|
||||||
userID = "admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据库中的所有交易员
|
|
||||||
traders, err := database.GetTraders(userID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("获取交易员列表失败: %w", err)
|
return fmt.Errorf("获取用户列表失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("📋 加载数据库中的交易员配置: %d 个 (用户: %s)", len(traders), userID)
|
log.Printf("📋 发现 %d 个用户,开始加载所有交易员配置...", len(userIDs))
|
||||||
|
|
||||||
// 获取系统配置
|
var allTraders []*config.TraderRecord
|
||||||
coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url")
|
for _, userID := range userIDs {
|
||||||
|
// 获取每个用户的交易员
|
||||||
|
traders, err := database.GetTraders(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ 获取用户 %s 的交易员失败: %v", userID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("📋 用户 %s: %d 个交易员", userID, len(traders))
|
||||||
|
allTraders = append(allTraders, traders...)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("📋 总共加载 %d 个交易员配置", len(allTraders))
|
||||||
|
|
||||||
|
// 获取系统配置(不包含信号源,信号源现在为用户级别)
|
||||||
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
|
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
|
||||||
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
|
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
|
||||||
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
|
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
|
||||||
btcEthLeverageStr, _ := database.GetSystemConfig("btc_eth_leverage")
|
defaultCoinsStr, _ := database.GetSystemConfig("default_coins")
|
||||||
altcoinLeverageStr, _ := database.GetSystemConfig("altcoin_leverage")
|
|
||||||
|
|
||||||
// 解析配置
|
// 解析配置
|
||||||
maxDailyLoss := 10.0 // 默认值
|
maxDailyLoss := 10.0 // 默认值
|
||||||
@@ -67,20 +74,19 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
stopTradingMinutes = val
|
stopTradingMinutes = val
|
||||||
}
|
}
|
||||||
|
|
||||||
btcEthLeverage := 5 // 默认值
|
// 解析默认币种列表
|
||||||
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
|
var defaultCoins []string
|
||||||
btcEthLeverage = val
|
if defaultCoinsStr != "" {
|
||||||
}
|
if err := json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins); err != nil {
|
||||||
|
log.Printf("⚠️ 解析默认币种配置失败: %v,使用空列表", err)
|
||||||
altcoinLeverage := 5 // 默认值
|
defaultCoins = []string{}
|
||||||
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
}
|
||||||
altcoinLeverage = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每个交易员获取AI模型和交易所配置
|
// 为每个交易员获取AI模型和交易所配置
|
||||||
for _, traderCfg := range traders {
|
for _, traderCfg := range allTraders {
|
||||||
// 获取AI模型配置
|
// 获取AI模型配置(使用交易员所属的用户ID)
|
||||||
aiModels, err := database.GetAIModels(userID)
|
aiModels, err := database.GetAIModels(traderCfg.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 获取AI模型配置失败: %v", err)
|
log.Printf("⚠️ 获取AI模型配置失败: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -104,8 +110,8 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取交易所配置
|
// 获取交易所配置(使用交易员所属的用户ID)
|
||||||
exchanges, err := database.GetExchanges(userID)
|
exchanges, err := database.GetExchanges(traderCfg.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 获取交易所配置失败: %v", err)
|
log.Printf("⚠️ 获取交易所配置失败: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -129,8 +135,18 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取用户信号源配置
|
||||||
|
var coinPoolURL, oiTopURL string
|
||||||
|
if userSignalSource, err := database.GetUserSignalSource(traderCfg.UserID); err == nil {
|
||||||
|
coinPoolURL = userSignalSource.CoinPoolURL
|
||||||
|
oiTopURL = userSignalSource.OITopURL
|
||||||
|
} else {
|
||||||
|
// 如果用户没有配置信号源,使用空字符串
|
||||||
|
log.Printf("🔍 用户 %s 暂未配置信号源", traderCfg.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
// 添加到TraderManager
|
// 添加到TraderManager
|
||||||
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, btcEthLeverage, altcoinLeverage)
|
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
|
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
|
||||||
continue
|
continue
|
||||||
@@ -142,11 +158,36 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁)
|
// addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁)
|
||||||
func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes, btcEthLeverage, altcoinLeverage int) error {
|
func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error {
|
||||||
if _, exists := tm.traders[traderCfg.ID]; exists {
|
if _, exists := tm.traders[traderCfg.ID]; exists {
|
||||||
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
|
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理交易币种列表
|
||||||
|
var tradingCoins []string
|
||||||
|
if traderCfg.TradingSymbols != "" {
|
||||||
|
// 解析逗号分隔的交易币种列表
|
||||||
|
symbols := strings.Split(traderCfg.TradingSymbols, ",")
|
||||||
|
for _, symbol := range symbols {
|
||||||
|
symbol = strings.TrimSpace(symbol)
|
||||||
|
if symbol != "" {
|
||||||
|
tradingCoins = append(tradingCoins, symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定交易币种,使用默认币种
|
||||||
|
if len(tradingCoins) == 0 {
|
||||||
|
tradingCoins = defaultCoins
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据交易员配置决定是否使用信号源
|
||||||
|
var effectiveCoinPoolURL string
|
||||||
|
if traderCfg.UseCoinPool && coinPoolURL != "" {
|
||||||
|
effectiveCoinPoolURL = coinPoolURL
|
||||||
|
log.Printf("✓ 交易员 %s 启用 COIN POOL 信号源: %s", traderCfg.Name, coinPoolURL)
|
||||||
|
}
|
||||||
|
|
||||||
// 构建AutoTraderConfig
|
// 构建AutoTraderConfig
|
||||||
traderConfig := trader.AutoTraderConfig{
|
traderConfig := trader.AutoTraderConfig{
|
||||||
ID: traderCfg.ID,
|
ID: traderCfg.ID,
|
||||||
@@ -157,18 +198,20 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
BinanceSecretKey: "",
|
BinanceSecretKey: "",
|
||||||
HyperliquidPrivateKey: "",
|
HyperliquidPrivateKey: "",
|
||||||
HyperliquidTestnet: exchangeCfg.Testnet,
|
HyperliquidTestnet: exchangeCfg.Testnet,
|
||||||
CoinPoolAPIURL: coinPoolURL,
|
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||||
UseQwen: aiModelCfg.Provider == "qwen",
|
UseQwen: aiModelCfg.Provider == "qwen",
|
||||||
DeepSeekKey: "",
|
DeepSeekKey: "",
|
||||||
QwenKey: "",
|
QwenKey: "",
|
||||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||||
InitialBalance: traderCfg.InitialBalance,
|
InitialBalance: traderCfg.InitialBalance,
|
||||||
BTCETHLeverage: btcEthLeverage,
|
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||||
AltcoinLeverage: altcoinLeverage,
|
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||||
MaxDailyLoss: maxDailyLoss,
|
MaxDailyLoss: maxDailyLoss,
|
||||||
MaxDrawdown: maxDrawdown,
|
MaxDrawdown: maxDrawdown,
|
||||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||||
|
DefaultCoins: defaultCoins,
|
||||||
|
TradingCoins: tradingCoins,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据交易所类型设置API密钥
|
// 根据交易所类型设置API密钥
|
||||||
@@ -216,7 +259,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
// AddTrader 从数据库配置添加trader (移除旧版兼容性)
|
// AddTrader 从数据库配置添加trader (移除旧版兼容性)
|
||||||
|
|
||||||
// AddTraderFromDB 从数据库配置添加trader
|
// AddTraderFromDB 从数据库配置添加trader
|
||||||
func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes, btcEthLeverage, altcoinLeverage int) error {
|
func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error {
|
||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
@@ -224,6 +267,31 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
|
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理交易币种列表
|
||||||
|
var tradingCoins []string
|
||||||
|
if traderCfg.TradingSymbols != "" {
|
||||||
|
// 解析逗号分隔的交易币种列表
|
||||||
|
symbols := strings.Split(traderCfg.TradingSymbols, ",")
|
||||||
|
for _, symbol := range symbols {
|
||||||
|
symbol = strings.TrimSpace(symbol)
|
||||||
|
if symbol != "" {
|
||||||
|
tradingCoins = append(tradingCoins, symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定交易币种,使用默认币种
|
||||||
|
if len(tradingCoins) == 0 {
|
||||||
|
tradingCoins = defaultCoins
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据交易员配置决定是否使用信号源
|
||||||
|
var effectiveCoinPoolURL string
|
||||||
|
if traderCfg.UseCoinPool && coinPoolURL != "" {
|
||||||
|
effectiveCoinPoolURL = coinPoolURL
|
||||||
|
log.Printf("✓ 交易员 %s 启用 COIN POOL 信号源: %s", traderCfg.Name, coinPoolURL)
|
||||||
|
}
|
||||||
|
|
||||||
// 构建AutoTraderConfig
|
// 构建AutoTraderConfig
|
||||||
traderConfig := trader.AutoTraderConfig{
|
traderConfig := trader.AutoTraderConfig{
|
||||||
ID: traderCfg.ID,
|
ID: traderCfg.ID,
|
||||||
@@ -234,18 +302,20 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
BinanceSecretKey: "",
|
BinanceSecretKey: "",
|
||||||
HyperliquidPrivateKey: "",
|
HyperliquidPrivateKey: "",
|
||||||
HyperliquidTestnet: exchangeCfg.Testnet,
|
HyperliquidTestnet: exchangeCfg.Testnet,
|
||||||
CoinPoolAPIURL: coinPoolURL,
|
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||||
UseQwen: aiModelCfg.Provider == "qwen",
|
UseQwen: aiModelCfg.Provider == "qwen",
|
||||||
DeepSeekKey: "",
|
DeepSeekKey: "",
|
||||||
QwenKey: "",
|
QwenKey: "",
|
||||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||||
InitialBalance: traderCfg.InitialBalance,
|
InitialBalance: traderCfg.InitialBalance,
|
||||||
BTCETHLeverage: btcEthLeverage,
|
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||||
AltcoinLeverage: altcoinLeverage,
|
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||||
MaxDailyLoss: maxDailyLoss,
|
MaxDailyLoss: maxDailyLoss,
|
||||||
MaxDrawdown: maxDrawdown,
|
MaxDrawdown: maxDrawdown,
|
||||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||||
|
DefaultCoins: defaultCoins,
|
||||||
|
TradingCoins: tradingCoins,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据交易所类型设置API密钥
|
// 根据交易所类型设置API密钥
|
||||||
@@ -373,6 +443,7 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
|
|||||||
"trader_id": t.GetID(),
|
"trader_id": t.GetID(),
|
||||||
"trader_name": t.GetName(),
|
"trader_name": t.GetName(),
|
||||||
"ai_model": t.GetAIModel(),
|
"ai_model": t.GetAIModel(),
|
||||||
|
"exchange": t.GetExchange(),
|
||||||
"total_equity": account["total_equity"],
|
"total_equity": account["total_equity"],
|
||||||
"total_pnl": account["total_pnl"],
|
"total_pnl": account["total_pnl"],
|
||||||
"total_pnl_pct": account["total_pnl_pct"],
|
"total_pnl_pct": account["total_pnl_pct"],
|
||||||
@@ -389,42 +460,55 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
|
|||||||
return comparison, nil
|
return comparison, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompetitionData 获取竞赛数据(特定用户的所有交易员)
|
// GetCompetitionData 获取竞赛数据(全平台所有交易员)
|
||||||
func (tm *TraderManager) GetCompetitionData(userID string) (map[string]interface{}, error) {
|
func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) {
|
||||||
tm.mu.RLock()
|
tm.mu.RLock()
|
||||||
defer tm.mu.RUnlock()
|
defer tm.mu.RUnlock()
|
||||||
|
|
||||||
comparison := make(map[string]interface{})
|
comparison := make(map[string]interface{})
|
||||||
traders := make([]map[string]interface{}, 0)
|
traders := make([]map[string]interface{}, 0)
|
||||||
|
|
||||||
// 只获取该用户的交易员
|
// 获取全平台所有交易员
|
||||||
for traderID, t := range tm.traders {
|
for _, t := range tm.traders {
|
||||||
// 检查trader是否属于该用户(通过ID前缀判断)
|
|
||||||
// 格式:userID_traderName
|
|
||||||
if !isUserTrader(traderID, userID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := t.GetAccountInfo()
|
account, err := t.GetAccountInfo()
|
||||||
if err != nil {
|
|
||||||
log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", traderID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
status := t.GetStatus()
|
status := t.GetStatus()
|
||||||
traders = append(traders, map[string]interface{}{
|
|
||||||
"trader_id": t.GetID(),
|
var traderData map[string]interface{}
|
||||||
"trader_name": t.GetName(),
|
|
||||||
"ai_model": t.GetAIModel(),
|
if err != nil {
|
||||||
"total_equity": account["total_equity"],
|
// 如果获取账户信息失败,使用默认值但仍然显示交易员
|
||||||
"total_pnl": account["total_pnl"],
|
log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", t.GetID(), err)
|
||||||
"total_pnl_pct": account["total_pnl_pct"],
|
traderData = map[string]interface{}{
|
||||||
"position_count": account["position_count"],
|
"trader_id": t.GetID(),
|
||||||
"margin_used_pct": account["margin_used_pct"],
|
"trader_name": t.GetName(),
|
||||||
"is_running": status["is_running"],
|
"ai_model": t.GetAIModel(),
|
||||||
})
|
"exchange": t.GetExchange(),
|
||||||
|
"total_equity": 0.0,
|
||||||
|
"total_pnl": 0.0,
|
||||||
|
"total_pnl_pct": 0.0,
|
||||||
|
"position_count": 0,
|
||||||
|
"margin_used_pct": 0.0,
|
||||||
|
"is_running": status["is_running"],
|
||||||
|
"error": "账户数据获取失败",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 正常情况下使用真实账户数据
|
||||||
|
traderData = map[string]interface{}{
|
||||||
|
"trader_id": t.GetID(),
|
||||||
|
"trader_name": t.GetName(),
|
||||||
|
"ai_model": t.GetAIModel(),
|
||||||
|
"exchange": t.GetExchange(),
|
||||||
|
"total_equity": account["total_equity"],
|
||||||
|
"total_pnl": account["total_pnl"],
|
||||||
|
"total_pnl_pct": account["total_pnl_pct"],
|
||||||
|
"position_count": account["position_count"],
|
||||||
|
"margin_used_pct": account["margin_used_pct"],
|
||||||
|
"is_running": status["is_running"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traders = append(traders, traderData)
|
||||||
}
|
}
|
||||||
|
|
||||||
comparison["traders"] = traders
|
comparison["traders"] = traders
|
||||||
comparison["count"] = len(traders)
|
comparison["count"] = len(traders)
|
||||||
|
|
||||||
@@ -474,13 +558,21 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
|||||||
|
|
||||||
log.Printf("📋 为用户 %s 加载交易员配置: %d 个", userID, len(traders))
|
log.Printf("📋 为用户 %s 加载交易员配置: %d 个", userID, len(traders))
|
||||||
|
|
||||||
// 获取系统配置
|
// 获取系统配置(不包含信号源,信号源现在为用户级别)
|
||||||
coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url")
|
|
||||||
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
|
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
|
||||||
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
|
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
|
||||||
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
|
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
|
||||||
btcEthLeverageStr, _ := database.GetSystemConfig("btc_eth_leverage")
|
defaultCoinsStr, _ := database.GetSystemConfig("default_coins")
|
||||||
altcoinLeverageStr, _ := database.GetSystemConfig("altcoin_leverage")
|
|
||||||
|
// 获取用户信号源配置
|
||||||
|
var coinPoolURL, oiTopURL string
|
||||||
|
if userSignalSource, err := database.GetUserSignalSource(userID); err == nil {
|
||||||
|
coinPoolURL = userSignalSource.CoinPoolURL
|
||||||
|
oiTopURL = userSignalSource.OITopURL
|
||||||
|
log.Printf("📡 加载用户 %s 的信号源配置: COIN POOL=%s, OI TOP=%s", userID, coinPoolURL, oiTopURL)
|
||||||
|
} else {
|
||||||
|
log.Printf("🔍 用户 %s 暂未配置信号源", userID)
|
||||||
|
}
|
||||||
|
|
||||||
// 解析配置
|
// 解析配置
|
||||||
maxDailyLoss := 10.0 // 默认值
|
maxDailyLoss := 10.0 // 默认值
|
||||||
@@ -498,14 +590,13 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
|||||||
stopTradingMinutes = val
|
stopTradingMinutes = val
|
||||||
}
|
}
|
||||||
|
|
||||||
btcEthLeverage := 5 // 默认值
|
// 解析默认币种列表
|
||||||
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
|
var defaultCoins []string
|
||||||
btcEthLeverage = val
|
if defaultCoinsStr != "" {
|
||||||
}
|
if err := json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins); err != nil {
|
||||||
|
log.Printf("⚠️ 解析默认币种配置失败: %v,使用空列表", err)
|
||||||
altcoinLeverage := 5 // 默认值
|
defaultCoins = []string{}
|
||||||
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
}
|
||||||
altcoinLeverage = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每个交易员获取AI模型和交易所配置
|
// 为每个交易员获取AI模型和交易所配置
|
||||||
@@ -567,7 +658,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用现有的方法加载交易员
|
// 使用现有的方法加载交易员
|
||||||
err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, btcEthLeverage, altcoinLeverage)
|
err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err)
|
log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err)
|
||||||
}
|
}
|
||||||
@@ -577,7 +668,32 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑)
|
// loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑)
|
||||||
func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes, btcEthLeverage, altcoinLeverage int) error {
|
func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error {
|
||||||
|
// 处理交易币种列表
|
||||||
|
var tradingCoins []string
|
||||||
|
if traderCfg.TradingSymbols != "" {
|
||||||
|
// 解析逗号分隔的交易币种列表
|
||||||
|
symbols := strings.Split(traderCfg.TradingSymbols, ",")
|
||||||
|
for _, symbol := range symbols {
|
||||||
|
symbol = strings.TrimSpace(symbol)
|
||||||
|
if symbol != "" {
|
||||||
|
tradingCoins = append(tradingCoins, symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定交易币种,使用默认币种
|
||||||
|
if len(tradingCoins) == 0 {
|
||||||
|
tradingCoins = defaultCoins
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据交易员配置决定是否使用信号源
|
||||||
|
var effectiveCoinPoolURL string
|
||||||
|
if traderCfg.UseCoinPool && coinPoolURL != "" {
|
||||||
|
effectiveCoinPoolURL = coinPoolURL
|
||||||
|
log.Printf("✓ 交易员 %s 启用 COIN POOL 信号源: %s", traderCfg.Name, coinPoolURL)
|
||||||
|
}
|
||||||
|
|
||||||
// 构建AutoTraderConfig
|
// 构建AutoTraderConfig
|
||||||
traderConfig := trader.AutoTraderConfig{
|
traderConfig := trader.AutoTraderConfig{
|
||||||
ID: traderCfg.ID,
|
ID: traderCfg.ID,
|
||||||
@@ -585,14 +701,16 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
|
|||||||
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
||||||
Exchange: exchangeCfg.ID, // 使用exchange ID
|
Exchange: exchangeCfg.ID, // 使用exchange ID
|
||||||
InitialBalance: traderCfg.InitialBalance,
|
InitialBalance: traderCfg.InitialBalance,
|
||||||
|
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||||
|
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||||
CoinPoolAPIURL: coinPoolURL,
|
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||||
BTCETHLeverage: btcEthLeverage,
|
|
||||||
AltcoinLeverage: altcoinLeverage,
|
|
||||||
MaxDailyLoss: maxDailyLoss,
|
MaxDailyLoss: maxDailyLoss,
|
||||||
MaxDrawdown: maxDrawdown,
|
MaxDrawdown: maxDrawdown,
|
||||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||||
|
DefaultCoins: defaultCoins,
|
||||||
|
TradingCoins: tradingCoins,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据交易所类型设置API密钥
|
// 根据交易所类型设置API密钥
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ type AutoTraderConfig struct {
|
|||||||
|
|
||||||
// 仓位模式
|
// 仓位模式
|
||||||
IsCrossMargin bool // true=全仓模式, false=逐仓模式
|
IsCrossMargin bool // true=全仓模式, false=逐仓模式
|
||||||
|
|
||||||
|
// 币种配置
|
||||||
|
DefaultCoins []string // 默认币种列表(从数据库获取)
|
||||||
|
TradingCoins []string // 实际交易币种列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoTrader 自动交易器
|
// AutoTrader 自动交易器
|
||||||
@@ -82,6 +86,8 @@ type AutoTrader struct {
|
|||||||
dailyPnL float64
|
dailyPnL float64
|
||||||
customPrompt string // 自定义交易策略prompt
|
customPrompt string // 自定义交易策略prompt
|
||||||
overrideBasePrompt bool // 是否覆盖基础prompt
|
overrideBasePrompt bool // 是否覆盖基础prompt
|
||||||
|
defaultCoins []string // 默认币种列表(从数据库获取)
|
||||||
|
tradingCoins []string // 实际交易币种列表
|
||||||
lastResetTime time.Time
|
lastResetTime time.Time
|
||||||
stopUntil time.Time
|
stopUntil time.Time
|
||||||
isRunning bool
|
isRunning bool
|
||||||
@@ -184,6 +190,8 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
|||||||
mcpClient: mcpClient,
|
mcpClient: mcpClient,
|
||||||
decisionLogger: decisionLogger,
|
decisionLogger: decisionLogger,
|
||||||
initialBalance: config.InitialBalance,
|
initialBalance: config.InitialBalance,
|
||||||
|
defaultCoins: config.DefaultCoins,
|
||||||
|
tradingCoins: config.TradingCoins,
|
||||||
lastResetTime: time.Now(),
|
lastResetTime: time.Now(),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
callCount: 0,
|
callCount: 0,
|
||||||
@@ -486,30 +494,12 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 获取合并的候选币种池(AI500 + OI Top,去重)
|
// 3. 获取交易员的候选币种池
|
||||||
// 无论有没有持仓,都分析相同数量的币种(让AI看到所有好机会)
|
candidateCoins, err := at.getCandidateCoins()
|
||||||
// AI会根据保证金使用率和现有持仓情况,自己决定是否要换仓
|
|
||||||
const ai500Limit = 20 // AI500取前20个评分最高的币种
|
|
||||||
|
|
||||||
// 获取合并后的币种池(AI500 + OI Top)
|
|
||||||
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
|
return nil, fmt.Errorf("获取候选币种失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建候选币种列表(包含来源信息)
|
|
||||||
var candidateCoins []decision.CandidateCoin
|
|
||||||
for _, symbol := range mergedPool.AllSymbols {
|
|
||||||
sources := mergedPool.SymbolSources[symbol]
|
|
||||||
candidateCoins = append(candidateCoins, decision.CandidateCoin{
|
|
||||||
Symbol: symbol,
|
|
||||||
Sources: sources, // "ai500" 和/或 "oi_top"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("📋 合并币种池: AI500前%d + OI_Top20 = 总计%d个候选币种",
|
|
||||||
ai500Limit, len(candidateCoins))
|
|
||||||
|
|
||||||
// 4. 计算总盈亏
|
// 4. 计算总盈亏
|
||||||
totalPnL := totalEquity - at.initialBalance
|
totalPnL := totalEquity - at.initialBalance
|
||||||
totalPnLPct := 0.0
|
totalPnLPct := 0.0
|
||||||
@@ -759,6 +749,11 @@ func (at *AutoTrader) GetAIModel() string {
|
|||||||
return at.aiModel
|
return at.aiModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExchange 获取交易所
|
||||||
|
func (at *AutoTrader) GetExchange() string {
|
||||||
|
return at.exchange
|
||||||
|
}
|
||||||
|
|
||||||
// SetCustomPrompt 设置自定义交易策略prompt
|
// SetCustomPrompt 设置自定义交易策略prompt
|
||||||
func (at *AutoTrader) SetCustomPrompt(prompt string) {
|
func (at *AutoTrader) SetCustomPrompt(prompt string) {
|
||||||
at.customPrompt = prompt
|
at.customPrompt = prompt
|
||||||
@@ -968,3 +963,74 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision
|
|||||||
|
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCandidateCoins 获取交易员的候选币种列表
|
||||||
|
func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
|
||||||
|
if len(at.tradingCoins) == 0 {
|
||||||
|
// 使用数据库配置的默认币种列表
|
||||||
|
var candidateCoins []decision.CandidateCoin
|
||||||
|
|
||||||
|
if len(at.defaultCoins) > 0 {
|
||||||
|
// 使用数据库中配置的默认币种
|
||||||
|
for _, coin := range at.defaultCoins {
|
||||||
|
symbol := normalizeSymbol(coin)
|
||||||
|
candidateCoins = append(candidateCoins, decision.CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: []string{"default"}, // 标记为数据库默认币种
|
||||||
|
})
|
||||||
|
}
|
||||||
|
log.Printf("📋 [%s] 使用数据库默认币种: %d个币种 %v",
|
||||||
|
at.name, len(candidateCoins), at.defaultCoins)
|
||||||
|
return candidateCoins, nil
|
||||||
|
} else {
|
||||||
|
// 如果数据库中没有配置默认币种,则使用AI500+OI Top作为fallback
|
||||||
|
const ai500Limit = 20 // AI500取前20个评分最高的币种
|
||||||
|
|
||||||
|
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建候选币种列表(包含来源信息)
|
||||||
|
for _, symbol := range mergedPool.AllSymbols {
|
||||||
|
sources := mergedPool.SymbolSources[symbol]
|
||||||
|
candidateCoins = append(candidateCoins, decision.CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: sources, // "ai500" 和/或 "oi_top"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("📋 [%s] 数据库无默认币种配置,使用AI500+OI Top: AI500前%d + OI_Top20 = 总计%d个候选币种",
|
||||||
|
at.name, ai500Limit, len(candidateCoins))
|
||||||
|
return candidateCoins, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用自定义币种列表
|
||||||
|
var candidateCoins []decision.CandidateCoin
|
||||||
|
for _, coin := range at.tradingCoins {
|
||||||
|
// 确保币种格式正确(转为大写USDT交易对)
|
||||||
|
symbol := normalizeSymbol(coin)
|
||||||
|
candidateCoins = append(candidateCoins, decision.CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: []string{"custom"}, // 标记为自定义来源
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("📋 [%s] 使用自定义币种: %d个币种 %v",
|
||||||
|
at.name, len(candidateCoins), at.tradingCoins)
|
||||||
|
return candidateCoins, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeSymbol 标准化币种符号(确保以USDT结尾)
|
||||||
|
func normalizeSymbol(symbol string) string {
|
||||||
|
// 转为大写
|
||||||
|
symbol = strings.ToUpper(strings.TrimSpace(symbol))
|
||||||
|
|
||||||
|
// 确保以USDT结尾
|
||||||
|
if !strings.HasSuffix(symbol, "USDT") {
|
||||||
|
symbol = symbol + "USDT"
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbol
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icons/nofx.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NOFX - AI Auto Trading Dashboard</title>
|
<title>NOFX - AI Auto Trading Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
296
web/public/icons/nofx.svg
Normal file
296
web/public/icons/nofx.svg
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="100%" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve">
|
||||||
|
<path fill="#FDFDFD" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M500.000000,1025.000000
|
||||||
|
C333.333344,1025.000000 167.166672,1025.000000 1.000000,1025.000000
|
||||||
|
C1.000000,683.666687 1.000000,342.333344 1.000000,1.000000
|
||||||
|
C342.333344,1.000000 683.666687,1.000000 1025.000000,1.000000
|
||||||
|
C1025.000000,342.333344 1025.000000,683.666687 1025.000000,1025.000000
|
||||||
|
C850.166687,1025.000000 675.333313,1025.000000 500.000000,1025.000000
|
||||||
|
M534.031067,584.483826
|
||||||
|
C534.031067,588.398560 534.031067,592.313354 534.031067,596.807495
|
||||||
|
C545.055725,596.807495 555.191895,596.895569 565.324890,596.757568
|
||||||
|
C568.675903,596.711914 570.108704,597.732422 570.067505,601.303345
|
||||||
|
C569.908142,615.130981 569.858154,628.962708 570.028137,642.789673
|
||||||
|
C570.077393,646.796997 568.523926,648.072266 564.742004,647.996033
|
||||||
|
C556.247864,647.824707 547.744873,648.036621 539.253845,647.796204
|
||||||
|
C535.514526,647.690369 533.817627,648.561218 533.996155,652.742493
|
||||||
|
C534.308533,660.059753 533.817566,667.412292 534.157166,674.727051
|
||||||
|
C534.375000,679.418884 533.066345,681.195984 528.085999,681.018494
|
||||||
|
C517.601807,680.644897 507.094879,680.960999 496.599945,680.810852
|
||||||
|
C493.243347,680.762817 491.914062,681.787109 491.941986,685.357300
|
||||||
|
C492.094421,704.850281 492.122192,724.345581 491.971497,743.838440
|
||||||
|
C491.941101,747.773010 493.228638,749.004578 497.124115,748.978394
|
||||||
|
C518.949890,748.831421 540.777771,748.821838 562.603271,748.980225
|
||||||
|
C566.173035,749.006165 567.215881,747.891907 567.153503,744.444214
|
||||||
|
C566.966858,734.117310 567.222534,723.781128 566.953308,713.457703
|
||||||
|
C566.847107,709.385132 568.577148,708.202698 572.203125,708.286438
|
||||||
|
C578.188965,708.424683 584.192078,708.207458 590.160583,708.579163
|
||||||
|
C594.476196,708.847839 596.032410,707.578918 595.856140,703.035217
|
||||||
|
C595.503296,693.939453 595.751465,684.820312 595.751465,675.649231
|
||||||
|
C596.925232,675.398560 597.552307,675.149048 598.180054,675.147583
|
||||||
|
C615.339722,675.106445 632.499634,675.126099 649.658997,675.034668
|
||||||
|
C652.399414,675.020081 653.028992,676.300964 653.009949,678.716370
|
||||||
|
C652.946899,686.712646 653.113281,694.711670 652.973572,702.705811
|
||||||
|
C652.917664,705.902710 653.994019,707.188904 657.276245,707.107605
|
||||||
|
C663.269775,706.959106 669.278931,707.315979 675.266113,707.076050
|
||||||
|
C679.086365,706.922974 680.126648,708.399414 680.068604,712.025635
|
||||||
|
C679.897949,722.685486 680.140930,733.351624 679.989075,744.012024
|
||||||
|
C679.937683,747.618469 681.041504,748.997803 684.821899,748.976501
|
||||||
|
C708.314026,748.844055 731.807556,748.852051 755.299805,748.975403
|
||||||
|
C758.820862,748.993835 759.868530,747.806763 759.844788,744.357910
|
||||||
|
C759.709351,724.698547 759.709839,705.037598 759.818420,685.377991
|
||||||
|
C759.837585,681.910950 758.672913,680.734314 755.189087,680.794067
|
||||||
|
C744.195374,680.982727 733.194397,680.763184 722.201355,680.970520
|
||||||
|
C718.237671,681.045227 716.403687,679.967041 716.487549,675.664490
|
||||||
|
C716.636658,668.012939 716.185547,660.350403 716.298767,652.697144
|
||||||
|
C716.354187,648.952393 714.940552,647.784058 711.308655,647.875549
|
||||||
|
C703.149780,648.080994 694.974915,647.740051 686.821289,648.032654
|
||||||
|
C682.197937,648.198547 680.731934,646.551575 680.854858,641.973938
|
||||||
|
C681.136780,631.484619 680.943665,620.982361 680.937012,610.485352
|
||||||
|
C680.928345,596.707458 680.925720,596.708313 694.772095,596.712830
|
||||||
|
C701.716309,596.715027 708.660583,596.713257 716.156738,596.713257
|
||||||
|
C716.156738,587.047791 716.274170,578.086670 716.100098,569.131104
|
||||||
|
C716.028503,565.448486 717.396729,564.077209 721.057495,564.125061
|
||||||
|
C731.718384,564.264526 742.382324,564.204712 753.044861,564.170166
|
||||||
|
C755.140137,564.163330 757.234619,563.919800 759.374390,563.782654
|
||||||
|
C759.374390,541.682739 759.374390,520.113953 759.374390,498.512848
|
||||||
|
C758.314819,498.283966 757.679260,498.027313 757.043457,498.026611
|
||||||
|
C732.552490,497.999329 708.061401,498.037842 683.570801,497.939880
|
||||||
|
C680.143433,497.926178 679.874512,499.756226 679.904175,502.387390
|
||||||
|
C680.015015,512.215210 679.874512,522.048523 680.153259,531.870544
|
||||||
|
C680.266418,535.860596 679.224426,537.680786 674.914673,537.479248
|
||||||
|
C669.263062,537.215027 663.581482,537.623352 657.925659,537.406860
|
||||||
|
C654.311951,537.268616 652.858887,538.322510 652.991699,542.182861
|
||||||
|
C653.266357,550.170227 652.971191,558.175842 653.164490,566.167786
|
||||||
|
C653.254150,569.872559 651.856873,571.069702 648.200317,571.033142
|
||||||
|
C632.374023,570.874939 616.544556,570.857117 600.718201,571.000671
|
||||||
|
C597.009949,571.034241 595.702759,569.735596 595.771912,566.091858
|
||||||
|
C595.923645,558.098816 595.724426,550.099670 595.818298,542.104797
|
||||||
|
C595.854309,539.035889 594.871948,537.527527 591.554504,537.577759
|
||||||
|
C585.402100,537.671082 579.222473,537.141418 573.097961,537.541626
|
||||||
|
C567.969849,537.876709 566.857544,535.717102 566.994751,531.152100
|
||||||
|
C567.259827,522.329468 567.088135,513.493835 567.107239,504.663605
|
||||||
|
C567.121216,498.211700 567.128967,498.312378 560.831177,498.362671
|
||||||
|
C539.515869,498.532898 518.197937,498.814789 496.885956,498.590637
|
||||||
|
C492.527435,498.544800 491.869690,499.960571 491.895660,503.604095
|
||||||
|
C492.027496,522.097412 492.121552,540.592773 491.953308,559.084961
|
||||||
|
C491.917023,563.073425 493.143402,564.285767 497.052917,564.207214
|
||||||
|
C507.545197,563.996338 518.045471,564.219666 528.540771,564.103271
|
||||||
|
C532.401672,564.060364 534.411804,565.121643 534.089355,569.498840
|
||||||
|
C533.747498,574.138367 534.024414,578.823547 534.031067,584.483826
|
||||||
|
M253.353867,389.499359
|
||||||
|
C253.353867,425.600555 253.353867,461.701721 253.353867,498.020203
|
||||||
|
C273.984375,498.020203 294.234894,498.020203 314.879120,498.020203
|
||||||
|
C314.879120,466.733032 314.879120,435.525452 314.879120,404.231567
|
||||||
|
C325.188171,404.231567 335.102448,404.231567 345.353882,404.231567
|
||||||
|
C345.353882,416.582062 345.353882,428.655182 345.353882,441.171600
|
||||||
|
C356.752899,441.171600 367.816437,441.171600 379.144989,441.171600
|
||||||
|
C379.144989,452.733887 379.144989,463.845337 379.144989,475.463928
|
||||||
|
C387.484772,475.463928 395.556122,475.463928 404.001831,475.463928
|
||||||
|
C404.001831,483.396393 404.001831,490.822571 404.001831,498.353760
|
||||||
|
C424.266266,498.353760 444.136444,498.353760 464.049652,498.353760
|
||||||
|
C464.049652,428.578674 464.049652,359.158661 464.049652,289.508362
|
||||||
|
C443.934937,289.508362 424.057556,289.508362 403.805725,289.508362
|
||||||
|
C403.805725,326.036926 403.805725,362.284485 403.805725,398.714264
|
||||||
|
C393.694244,398.714264 383.958435,398.714264 373.754669,398.714264
|
||||||
|
C373.754669,387.079407 373.754669,375.679657 373.754669,363.763580
|
||||||
|
C364.226013,363.763580 355.157928,363.763580 345.754578,363.763580
|
||||||
|
C345.754578,350.975952 345.754578,338.719330 345.754578,326.013428
|
||||||
|
C335.334686,326.013428 325.264954,326.013428 314.727661,326.013428
|
||||||
|
C314.727661,313.817993 314.727661,302.078217 314.727661,290.294006
|
||||||
|
C294.024200,290.294006 273.800537,290.294006 253.353867,290.294006
|
||||||
|
C253.353867,323.258392 253.353867,355.878815 253.353867,389.499359
|
||||||
|
M253.324738,540.524414
|
||||||
|
C253.324738,609.787170 253.324738,679.049988 253.324738,748.416748
|
||||||
|
C273.913696,748.416748 293.998413,748.416748 314.709351,748.416748
|
||||||
|
C314.709351,719.517822 314.709351,690.832581 314.709351,661.901611
|
||||||
|
C350.772034,661.901611 386.208771,661.901611 421.853760,661.901611
|
||||||
|
C421.853760,645.744873 421.853760,629.843628 421.853760,613.658813
|
||||||
|
C386.514313,613.658813 351.406067,613.658813 315.981873,613.658813
|
||||||
|
C315.981873,602.016785 315.981873,590.659973 315.981873,578.921387
|
||||||
|
C365.640533,578.921387 414.883820,578.921387 464.020874,578.921387
|
||||||
|
C464.020874,562.126465 464.020874,545.717834 464.020874,529.276367
|
||||||
|
C393.615936,529.276367 323.565338,529.276367 253.319580,529.276367
|
||||||
|
C253.319580,532.918762 253.319580,536.226440 253.324738,540.524414
|
||||||
|
M492.003387,396.500000
|
||||||
|
C492.003387,418.075806 492.003387,439.651642 492.003387,461.703796
|
||||||
|
C504.388947,461.703796 516.107178,461.703796 528.313721,461.703796
|
||||||
|
C528.313721,472.416870 528.313721,482.654999 528.313721,492.669708
|
||||||
|
C565.543945,492.669708 602.271057,492.669708 639.460571,492.669708
|
||||||
|
C639.460571,481.874329 639.460571,471.464661 639.460571,460.745056
|
||||||
|
C652.522522,460.745056 665.122131,460.745056 677.734863,460.745056
|
||||||
|
C677.734863,427.692261 677.734863,394.973724 677.734863,361.929138
|
||||||
|
C664.783020,361.929138 652.186462,361.929138 639.150879,361.929138
|
||||||
|
C639.150879,350.688141 639.150879,339.765076 639.150879,328.555176
|
||||||
|
C601.986938,328.555176 565.268433,328.555176 527.973755,328.555176
|
||||||
|
C527.973755,339.860962 527.973755,350.904510 527.973755,362.241943
|
||||||
|
C515.652466,362.241943 503.905975,362.241943 492.002960,362.241943
|
||||||
|
C492.002960,373.544617 492.002960,384.522308 492.003387,396.500000
|
||||||
|
M680.952881,342.679718
|
||||||
|
C699.195862,342.679718 717.438782,342.679718 735.967041,342.679718
|
||||||
|
C735.967041,325.691925 736.031189,309.222900 735.806641,292.757812
|
||||||
|
C735.793213,291.775330 733.476685,289.998322 732.215942,289.983917
|
||||||
|
C715.887634,289.797485 699.555664,289.801270 683.227722,290.003845
|
||||||
|
C682.113281,290.017670 680.069885,291.995911 680.064697,293.068268
|
||||||
|
C679.986694,309.383881 680.176758,325.700775 680.952881,342.679718
|
||||||
|
z"/>
|
||||||
|
<path fill="#F9AF07" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M534.031128,583.986328
|
||||||
|
C534.024414,578.823547 533.747498,574.138367 534.089355,569.498840
|
||||||
|
C534.411804,565.121643 532.401672,564.060364 528.540771,564.103271
|
||||||
|
C518.045471,564.219666 507.545197,563.996338 497.052917,564.207214
|
||||||
|
C493.143402,564.285767 491.917023,563.073425 491.953308,559.084961
|
||||||
|
C492.121552,540.592773 492.027496,522.097412 491.895660,503.604095
|
||||||
|
C491.869690,499.960571 492.527435,498.544800 496.885956,498.590637
|
||||||
|
C518.197937,498.814789 539.515869,498.532898 560.831177,498.362671
|
||||||
|
C567.128967,498.312378 567.121216,498.211700 567.107239,504.663605
|
||||||
|
C567.088135,513.493835 567.259827,522.329468 566.994751,531.152100
|
||||||
|
C566.857544,535.717102 567.969849,537.876709 573.097961,537.541626
|
||||||
|
C579.222473,537.141418 585.402100,537.671082 591.554504,537.577759
|
||||||
|
C594.871948,537.527527 595.854309,539.035889 595.818298,542.104797
|
||||||
|
C595.724426,550.099670 595.923645,558.098816 595.771912,566.091858
|
||||||
|
C595.702759,569.735596 597.009949,571.034241 600.718201,571.000671
|
||||||
|
C616.544556,570.857117 632.374023,570.874939 648.200317,571.033142
|
||||||
|
C651.856873,571.069702 653.254150,569.872559 653.164490,566.167786
|
||||||
|
C652.971191,558.175842 653.266357,550.170227 652.991699,542.182861
|
||||||
|
C652.858887,538.322510 654.311951,537.268616 657.925659,537.406860
|
||||||
|
C663.581482,537.623352 669.263062,537.215027 674.914673,537.479248
|
||||||
|
C679.224426,537.680786 680.266418,535.860596 680.153259,531.870544
|
||||||
|
C679.874512,522.048523 680.015015,512.215210 679.904175,502.387390
|
||||||
|
C679.874512,499.756226 680.143433,497.926178 683.570801,497.939880
|
||||||
|
C708.061401,498.037842 732.552490,497.999329 757.043457,498.026611
|
||||||
|
C757.679260,498.027313 758.314819,498.283966 759.374390,498.512848
|
||||||
|
C759.374390,520.113953 759.374390,541.682739 759.374390,563.782654
|
||||||
|
C757.234619,563.919800 755.140137,564.163330 753.044861,564.170166
|
||||||
|
C742.382324,564.204712 731.718384,564.264526 721.057495,564.125061
|
||||||
|
C717.396729,564.077209 716.028503,565.448486 716.100098,569.131104
|
||||||
|
C716.274170,578.086670 716.156738,587.047791 716.156738,596.713257
|
||||||
|
C708.660583,596.713257 701.716309,596.715027 694.772095,596.712830
|
||||||
|
C680.925720,596.708313 680.928345,596.707458 680.937012,610.485352
|
||||||
|
C680.943665,620.982361 681.136780,631.484619 680.854858,641.973938
|
||||||
|
C680.731934,646.551575 682.197937,648.198547 686.821289,648.032654
|
||||||
|
C694.974915,647.740051 703.149780,648.080994 711.308655,647.875549
|
||||||
|
C714.940552,647.784058 716.354187,648.952393 716.298767,652.697144
|
||||||
|
C716.185547,660.350403 716.636658,668.012939 716.487549,675.664490
|
||||||
|
C716.403687,679.967041 718.237671,681.045227 722.201355,680.970520
|
||||||
|
C733.194397,680.763184 744.195374,680.982727 755.189087,680.794067
|
||||||
|
C758.672913,680.734314 759.837585,681.910950 759.818420,685.377991
|
||||||
|
C759.709839,705.037598 759.709351,724.698547 759.844788,744.357910
|
||||||
|
C759.868530,747.806763 758.820862,748.993835 755.299805,748.975403
|
||||||
|
C731.807556,748.852051 708.314026,748.844055 684.821899,748.976501
|
||||||
|
C681.041504,748.997803 679.937683,747.618469 679.989075,744.012024
|
||||||
|
C680.140930,733.351624 679.897949,722.685486 680.068604,712.025635
|
||||||
|
C680.126648,708.399414 679.086365,706.922974 675.266113,707.076050
|
||||||
|
C669.278931,707.315979 663.269775,706.959106 657.276245,707.107605
|
||||||
|
C653.994019,707.188904 652.917664,705.902710 652.973572,702.705811
|
||||||
|
C653.113281,694.711670 652.946899,686.712646 653.009949,678.716370
|
||||||
|
C653.028992,676.300964 652.399414,675.020081 649.658997,675.034668
|
||||||
|
C632.499634,675.126099 615.339722,675.106445 598.180054,675.147583
|
||||||
|
C597.552307,675.149048 596.925232,675.398560 595.751465,675.649231
|
||||||
|
C595.751465,684.820312 595.503296,693.939453 595.856140,703.035217
|
||||||
|
C596.032410,707.578918 594.476196,708.847839 590.160583,708.579163
|
||||||
|
C584.192078,708.207458 578.188965,708.424683 572.203125,708.286438
|
||||||
|
C568.577148,708.202698 566.847107,709.385132 566.953308,713.457703
|
||||||
|
C567.222534,723.781128 566.966858,734.117310 567.153503,744.444214
|
||||||
|
C567.215881,747.891907 566.173035,749.006165 562.603271,748.980225
|
||||||
|
C540.777771,748.821838 518.949890,748.831421 497.124115,748.978394
|
||||||
|
C493.228638,749.004578 491.941101,747.773010 491.971497,743.838440
|
||||||
|
C492.122192,724.345581 492.094421,704.850281 491.941986,685.357300
|
||||||
|
C491.914062,681.787109 493.243347,680.762817 496.599945,680.810852
|
||||||
|
C507.094879,680.960999 517.601807,680.644897 528.085999,681.018494
|
||||||
|
C533.066345,681.195984 534.375000,679.418884 534.157166,674.727051
|
||||||
|
C533.817566,667.412292 534.308533,660.059753 533.996155,652.742493
|
||||||
|
C533.817627,648.561218 535.514526,647.690369 539.253845,647.796204
|
||||||
|
C547.744873,648.036621 556.247864,647.824707 564.742004,647.996033
|
||||||
|
C568.523926,648.072266 570.077393,646.796997 570.028137,642.789673
|
||||||
|
C569.858154,628.962708 569.908142,615.130981 570.067505,601.303345
|
||||||
|
C570.108704,597.732422 568.675903,596.711914 565.324890,596.757568
|
||||||
|
C555.191895,596.895569 545.055725,596.807495 534.031067,596.807495
|
||||||
|
C534.031067,592.313354 534.031067,588.398560 534.031128,583.986328
|
||||||
|
z"/>
|
||||||
|
<path fill="#020202" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M253.353867,388.999268
|
||||||
|
C253.353867,355.878815 253.353867,323.258392 253.353867,290.294006
|
||||||
|
C273.800537,290.294006 294.024200,290.294006 314.727661,290.294006
|
||||||
|
C314.727661,302.078217 314.727661,313.817993 314.727661,326.013428
|
||||||
|
C325.264954,326.013428 335.334686,326.013428 345.754578,326.013428
|
||||||
|
C345.754578,338.719330 345.754578,350.975952 345.754578,363.763580
|
||||||
|
C355.157928,363.763580 364.226013,363.763580 373.754669,363.763580
|
||||||
|
C373.754669,375.679657 373.754669,387.079407 373.754669,398.714264
|
||||||
|
C383.958435,398.714264 393.694244,398.714264 403.805725,398.714264
|
||||||
|
C403.805725,362.284485 403.805725,326.036926 403.805725,289.508362
|
||||||
|
C424.057556,289.508362 443.934937,289.508362 464.049652,289.508362
|
||||||
|
C464.049652,359.158661 464.049652,428.578674 464.049652,498.353760
|
||||||
|
C444.136444,498.353760 424.266266,498.353760 404.001831,498.353760
|
||||||
|
C404.001831,490.822571 404.001831,483.396393 404.001831,475.463928
|
||||||
|
C395.556122,475.463928 387.484772,475.463928 379.144989,475.463928
|
||||||
|
C379.144989,463.845337 379.144989,452.733887 379.144989,441.171600
|
||||||
|
C367.816437,441.171600 356.752899,441.171600 345.353882,441.171600
|
||||||
|
C345.353882,428.655182 345.353882,416.582062 345.353882,404.231567
|
||||||
|
C335.102448,404.231567 325.188171,404.231567 314.879120,404.231567
|
||||||
|
C314.879120,435.525452 314.879120,466.733032 314.879120,498.020203
|
||||||
|
C294.234894,498.020203 273.984375,498.020203 253.353867,498.020203
|
||||||
|
C253.353867,461.701721 253.353867,425.600555 253.353867,388.999268
|
||||||
|
z"/>
|
||||||
|
<path fill="#020202" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M253.322159,540.029297
|
||||||
|
C253.319580,536.226440 253.319580,532.918762 253.319580,529.276367
|
||||||
|
C323.565338,529.276367 393.615936,529.276367 464.020874,529.276367
|
||||||
|
C464.020874,545.717834 464.020874,562.126465 464.020874,578.921387
|
||||||
|
C414.883820,578.921387 365.640533,578.921387 315.981873,578.921387
|
||||||
|
C315.981873,590.659973 315.981873,602.016785 315.981873,613.658813
|
||||||
|
C351.406067,613.658813 386.514313,613.658813 421.853760,613.658813
|
||||||
|
C421.853760,629.843628 421.853760,645.744873 421.853760,661.901611
|
||||||
|
C386.208771,661.901611 350.772034,661.901611 314.709351,661.901611
|
||||||
|
C314.709351,690.832581 314.709351,719.517822 314.709351,748.416748
|
||||||
|
C293.998413,748.416748 273.913696,748.416748 253.324738,748.416748
|
||||||
|
C253.324738,679.049988 253.324738,609.787170 253.322159,540.029297
|
||||||
|
z"/>
|
||||||
|
<path fill="#020202" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M492.003174,396.000000
|
||||||
|
C492.002960,384.522308 492.002960,373.544617 492.002960,362.241943
|
||||||
|
C503.905975,362.241943 515.652466,362.241943 527.973755,362.241943
|
||||||
|
C527.973755,350.904510 527.973755,339.860962 527.973755,328.555176
|
||||||
|
C565.268433,328.555176 601.986938,328.555176 639.150879,328.555176
|
||||||
|
C639.150879,339.765076 639.150879,350.688141 639.150879,361.929138
|
||||||
|
C652.186462,361.929138 664.783020,361.929138 677.734863,361.929138
|
||||||
|
C677.734863,394.973724 677.734863,427.692261 677.734863,460.745056
|
||||||
|
C665.122131,460.745056 652.522522,460.745056 639.460571,460.745056
|
||||||
|
C639.460571,471.464661 639.460571,481.874329 639.460571,492.669708
|
||||||
|
C602.271057,492.669708 565.543945,492.669708 528.313721,492.669708
|
||||||
|
C528.313721,482.654999 528.313721,472.416870 528.313721,461.703796
|
||||||
|
C516.107178,461.703796 504.388947,461.703796 492.003387,461.703796
|
||||||
|
C492.003387,439.651642 492.003387,418.075806 492.003174,396.000000
|
||||||
|
M614.295959,428.499878
|
||||||
|
C614.295959,410.563873 614.295959,392.627899 614.295959,374.437195
|
||||||
|
C594.356567,374.437195 574.817383,374.437195 555.216553,374.437195
|
||||||
|
C555.216553,399.301392 555.216553,423.880280 555.216553,448.470581
|
||||||
|
C574.879578,448.470581 594.303711,448.470581 614.296204,448.470581
|
||||||
|
C614.296204,442.071533 614.296204,435.785706 614.295959,428.499878
|
||||||
|
z"/>
|
||||||
|
<path fill="#F7AF0C" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M680.628540,342.348541
|
||||||
|
C680.176758,325.700775 679.986694,309.383881 680.064697,293.068268
|
||||||
|
C680.069885,291.995911 682.113281,290.017670 683.227722,290.003845
|
||||||
|
C699.555664,289.801270 715.887634,289.797485 732.215942,289.983917
|
||||||
|
C733.476685,289.998322 735.793213,291.775330 735.806641,292.757812
|
||||||
|
C736.031189,309.222900 735.967041,325.691925 735.967041,342.679718
|
||||||
|
C717.438782,342.679718 699.195862,342.679718 680.628540,342.348541
|
||||||
|
z"/>
|
||||||
|
<path fill="#FCFCFC" opacity="1.000000" stroke="none"
|
||||||
|
d="
|
||||||
|
M614.296082,428.999878
|
||||||
|
C614.296204,435.785706 614.296204,442.071533 614.296204,448.470581
|
||||||
|
C594.303711,448.470581 574.879578,448.470581 555.216553,448.470581
|
||||||
|
C555.216553,423.880280 555.216553,399.301392 555.216553,374.437195
|
||||||
|
C574.817383,374.437195 594.356567,374.437195 614.295959,374.437195
|
||||||
|
C614.295959,392.627899 614.295959,410.563873 614.296082,428.999878
|
||||||
|
z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 18 KiB |
@@ -11,7 +11,6 @@ import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
|||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import { t, type Language } from './i18n/translations';
|
import { t, type Language } from './i18n/translations';
|
||||||
import { useSystemConfig } from './hooks/useSystemConfig';
|
import { useSystemConfig } from './hooks/useSystemConfig';
|
||||||
import { Bot, RefreshCw, TrendingUp, BarChart3, Brain, Download, Upload, Check, X, AlertCircle, Zap, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react';
|
|
||||||
import type {
|
import type {
|
||||||
SystemStatus,
|
SystemStatus,
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
@@ -176,7 +175,10 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
|
<div className="w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center text-3xl animate-spin"
|
||||||
|
style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||||
|
⚡
|
||||||
|
</div>
|
||||||
<p style={{ color: '#EAECEF' }}>加载中...</p>
|
<p style={{ color: '#EAECEF' }}>加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,7 +201,9 @@ function App() {
|
|||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
{/* Left - Logo and Title */}
|
{/* Left - Logo and Title */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-8 h-8" />
|
<div className="w-8 h-8 flex items-center justify-center">
|
||||||
|
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
{t('appTitle', language)}
|
{t('appTitle', language)}
|
||||||
@@ -220,7 +224,7 @@ function App() {
|
|||||||
: { background: 'transparent', color: '#848E9C' }
|
: { background: 'transparent', color: '#848E9C' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
竞赛
|
{t('aiCompetition', language)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage('traders')}
|
onClick={() => setCurrentPage('traders')}
|
||||||
@@ -260,8 +264,7 @@ function App() {
|
|||||||
{/* Admin Mode Indicator */}
|
{/* Admin Mode Indicator */}
|
||||||
{systemConfig?.admin_mode && (
|
{systemConfig?.admin_mode && (
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>⚡ {t('adminMode', language)}</span>
|
||||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>管理员模式</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -426,16 +429,16 @@ function TraderDetailsPage({
|
|||||||
<div className="mb-6 rounded p-6 animate-scale-in" style={{ background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.15) 0%, rgba(252, 213, 53, 0.05) 100%)', border: '1px solid rgba(240, 185, 11, 0.2)', boxShadow: '0 0 30px rgba(240, 185, 11, 0.15)' }}>
|
<div className="mb-6 rounded p-6 animate-scale-in" style={{ background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.15) 0%, rgba(252, 213, 53, 0.05) 100%)', border: '1px solid rgba(240, 185, 11, 0.2)', boxShadow: '0 0 30px rgba(240, 185, 11, 0.15)' }}>
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<h2 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h2 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
<div className="w-10 h-10 rounded-full flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
<span className="w-10 h-10 rounded-full flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||||
<Bot className="w-6 h-6" style={{ color: '#000' }} />
|
🤖
|
||||||
</div>
|
</span>
|
||||||
{selectedTrader.trader_name}
|
{selectedTrader.trader_name}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Trader Selector */}
|
{/* Trader Selector */}
|
||||||
{traders && traders.length > 0 && (
|
{traders && traders.length > 0 && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm" style={{ color: '#848E9C' }}>切换交易员:</span>
|
<span className="text-sm" style={{ color: '#848E9C' }}>{t('switchTrader', language)}:</span>
|
||||||
<select
|
<select
|
||||||
value={selectedTraderId}
|
value={selectedTraderId}
|
||||||
onChange={(e) => onTraderSelect(e.target.value)}
|
onChange={(e) => onTraderSelect(e.target.value)}
|
||||||
@@ -467,9 +470,8 @@ function TraderDetailsPage({
|
|||||||
{/* Debug Info */}
|
{/* Debug Info */}
|
||||||
{account && (
|
{account && (
|
||||||
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||||
<div className="flex items-center gap-2" style={{ color: '#848E9C' }}>
|
<div style={{ color: '#848E9C' }}>
|
||||||
<RefreshCw className="w-3 h-3" />
|
🔄 Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} |
|
||||||
Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} |
|
|
||||||
Available: {account?.available_balance?.toFixed(2) || '0.00'} | P&L: {account?.total_pnl?.toFixed(2) || '0.00'}{' '}
|
Available: {account?.available_balance?.toFixed(2) || '0.00'} | P&L: {account?.total_pnl?.toFixed(2) || '0.00'}{' '}
|
||||||
({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
|
({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
|
||||||
</div>
|
</div>
|
||||||
@@ -515,8 +517,7 @@ function TraderDetailsPage({
|
|||||||
<div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}>
|
<div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
<TrendingUp className="w-5 h-5" style={{ color: '#0ECB81' }} />
|
📈 {t('currentPositions', language)}
|
||||||
{t('currentPositions', language)}
|
|
||||||
</h2>
|
</h2>
|
||||||
{positions && positions.length > 0 && (
|
{positions && positions.length > 0 && (
|
||||||
<div className="text-xs px-3 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
<div className="text-xs px-3 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
||||||
@@ -580,9 +581,7 @@ function TraderDetailsPage({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||||
<div className="mb-4 flex justify-center opacity-50">
|
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||||
<BarChart3 className="w-16 h-16" />
|
|
||||||
</div>
|
|
||||||
<div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
|
<div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
|
||||||
<div className="text-sm">{t('noActivePositions', language)}</div>
|
<div className="text-sm">{t('noActivePositions', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -595,11 +594,11 @@ function TraderDetailsPage({
|
|||||||
<div className="binance-card p-6 animate-slide-in h-fit lg:sticky lg:top-24 lg:max-h-[calc(100vh-120px)]" style={{ animationDelay: '0.2s' }}>
|
<div className="binance-card p-6 animate-slide-in h-fit lg:sticky lg:top-24 lg:max-h-[calc(100vh-120px)]" style={{ animationDelay: '0.2s' }}>
|
||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
<div className="flex items-center gap-3 mb-5 pb-4 border-b" style={{ borderColor: '#2B3139' }}>
|
<div className="flex items-center gap-3 mb-5 pb-4 border-b" style={{ borderColor: '#2B3139' }}>
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-xl" style={{
|
||||||
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
|
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
|
||||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)'
|
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)'
|
||||||
}}>
|
}}>
|
||||||
<Brain className="w-6 h-6" style={{ color: '#FFF' }} />
|
🧠
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>{t('recentDecisions', language)}</h2>
|
<h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>{t('recentDecisions', language)}</h2>
|
||||||
@@ -619,9 +618,7 @@ function TraderDetailsPage({
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="py-16 text-center">
|
<div className="py-16 text-center">
|
||||||
<div className="mb-4 flex justify-center opacity-30">
|
<div className="text-6xl mb-4 opacity-30">🧠</div>
|
||||||
<Brain className="w-16 h-16" style={{ color: '#8B5CF6' }} />
|
|
||||||
</div>
|
|
||||||
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>{t('noDecisionsYet', language)}</div>
|
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>{t('noDecisionsYet', language)}</div>
|
||||||
<div className="text-sm" style={{ color: '#848E9C' }}>{t('aiDecisionsWillAppear', language)}</div>
|
<div className="text-sm" style={{ color: '#848E9C' }}>{t('aiDecisionsWillAppear', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -660,11 +657,10 @@ function StatCard({
|
|||||||
{change !== undefined && (
|
{change !== undefined && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className="text-sm mono font-bold flex items-center gap-1"
|
className="text-sm mono font-bold"
|
||||||
style={{ color: positive ? '#0ECB81' : '#F6465D' }}
|
style={{ color: positive ? '#0ECB81' : '#F6465D' }}
|
||||||
>
|
>
|
||||||
{positive ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />}
|
{positive ? '▲' : '▼'} {positive ? '+' : ''}
|
||||||
{positive ? '+' : ''}
|
|
||||||
{change.toFixed(2)}%
|
{change.toFixed(2)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -708,8 +704,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
|||||||
className="flex items-center gap-2 text-sm transition-colors"
|
className="flex items-center gap-2 text-sm transition-colors"
|
||||||
style={{ color: '#60a5fa' }}
|
style={{ color: '#60a5fa' }}
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<span className="font-semibold">📥 {t('inputPrompt', language)}</span>
|
||||||
<span className="font-semibold">{t('inputPrompt', language)}</span>
|
|
||||||
<span className="text-xs">{showInputPrompt ? t('collapse', language) : t('expand', language)}</span>
|
<span className="text-xs">{showInputPrompt ? t('collapse', language) : t('expand', language)}</span>
|
||||||
</button>
|
</button>
|
||||||
{showInputPrompt && (
|
{showInputPrompt && (
|
||||||
@@ -728,8 +723,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
|||||||
className="flex items-center gap-2 text-sm transition-colors"
|
className="flex items-center gap-2 text-sm transition-colors"
|
||||||
style={{ color: '#F0B90B' }}
|
style={{ color: '#F0B90B' }}
|
||||||
>
|
>
|
||||||
<Upload className="w-4 h-4" />
|
<span className="font-semibold">📤 {t('aiThinking', language)}</span>
|
||||||
<span className="font-semibold">{t('aiThinking', language)}</span>
|
|
||||||
<span className="text-xs">{showCoT ? t('collapse', language) : t('expand', language)}</span>
|
<span className="text-xs">{showCoT ? t('collapse', language) : t('expand', language)}</span>
|
||||||
</button>
|
</button>
|
||||||
{showCoT && (
|
{showCoT && (
|
||||||
@@ -759,11 +753,9 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
|||||||
{action.price > 0 && (
|
{action.price > 0 && (
|
||||||
<span className="font-mono text-xs" style={{ color: '#848E9C' }}>@{action.price.toFixed(4)}</span>
|
<span className="font-mono text-xs" style={{ color: '#848E9C' }}>@{action.price.toFixed(4)}</span>
|
||||||
)}
|
)}
|
||||||
{action.success ? (
|
<span style={{ color: action.success ? '#0ECB81' : '#F6465D' }}>
|
||||||
<Check className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
{action.success ? '✓' : '✗'}
|
||||||
) : (
|
</span>
|
||||||
<X className="w-4 h-4" style={{ color: '#F6465D' }} />
|
|
||||||
)}
|
|
||||||
{action.error && <span className="text-xs ml-2" style={{ color: '#F6465D' }}>{action.error}</span>}
|
{action.error && <span className="text-xs ml-2" style={{ color: '#F6465D' }}>{action.error}</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -797,9 +789,8 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
|||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
{decision.error_message && (
|
{decision.error_message && (
|
||||||
<div className="text-sm rounded px-3 py-2 mt-3 flex items-center gap-2" style={{ color: '#F6465D', background: 'rgba(246, 70, 93, 0.1)' }}>
|
<div className="text-sm rounded px-3 py-2 mt-3" style={{ color: '#F6465D', background: 'rgba(246, 70, 93, 0.1)' }}>
|
||||||
<AlertCircle className="w-4 h-4" />
|
❌ {decision.error_message}
|
||||||
{decision.error_message}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,15 @@ import useSWR from 'swr';
|
|||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { CompetitionTraderData } from '../types';
|
import type { CompetitionTraderData } from '../types';
|
||||||
import { getTraderColor } from '../utils/traderColors';
|
import { getTraderColor } from '../utils/traderColors';
|
||||||
import { BarChart3 } from 'lucide-react';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import { t } from '../i18n/translations';
|
||||||
|
|
||||||
interface ComparisonChartProps {
|
interface ComparisonChartProps {
|
||||||
traders: CompetitionTraderData[];
|
traders: CompetitionTraderData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComparisonChart({ traders }: ComparisonChartProps) {
|
export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||||
|
const { language } = useLanguage();
|
||||||
// 获取所有trader的历史数据 - 使用单个useSWR并发请求所有trader数据
|
// 获取所有trader的历史数据 - 使用单个useSWR并发请求所有trader数据
|
||||||
// 生成唯一的key,当traders变化时会触发重新请求
|
// 生成唯一的key,当traders变化时会触发重新请求
|
||||||
const tradersKey = traders.map(t => t.trader_id).sort().join(',');
|
const tradersKey = traders.map(t => t.trader_id).sort().join(',');
|
||||||
@@ -134,11 +136,9 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
if (combinedData.length === 0) {
|
if (combinedData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||||
<div className="mb-4 flex justify-center opacity-50">
|
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||||
<BarChart3 className="w-16 h-16" />
|
<div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
|
||||||
</div>
|
<div className="text-sm">{t('dataWillAppear', language)}</div>
|
||||||
<div className="text-lg font-semibold mb-2">暂无历史数据</div>
|
|
||||||
<div className="text-sm">运行几个周期后将显示对比曲线</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -314,25 +314,25 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="mt-6 grid grid-cols-4 gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}>
|
<div className="mt-6 grid grid-cols-4 gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}>
|
||||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>对比模式</div>
|
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('comparisonMode', language)}</div>
|
||||||
<div className="text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div>
|
<div className="text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>数据点数</div>
|
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('dataPoints', language)}</div>
|
||||||
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{combinedData.length} 个</div>
|
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{t('count', language, {count: combinedData.length})}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>当前差距</div>
|
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentGap', language)}</div>
|
||||||
<div className="text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}>
|
<div className="text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}>
|
||||||
{currentGap.toFixed(2)}%
|
{currentGap.toFixed(2)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>显示范围</div>
|
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div>
|
||||||
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>
|
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>
|
||||||
{combinedData.length > MAX_DISPLAY_POINTS
|
{combinedData.length > MAX_DISPLAY_POINTS
|
||||||
? `最近 ${MAX_DISPLAY_POINTS}`
|
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
||||||
: '全部数据'}
|
: t('allData', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { CompetitionData } from '../types';
|
import type { CompetitionData } from '../types';
|
||||||
import { ComparisonChart } from './ComparisonChart';
|
import { ComparisonChart } from './ComparisonChart';
|
||||||
|
import { TraderConfigModal } from './TraderConfigModal';
|
||||||
import { getTraderColor } from '../utils/traderColors';
|
import { getTraderColor } from '../utils/traderColors';
|
||||||
import { Trophy, Medal, Circle, CircleDot } from 'lucide-react';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import { t } from '../i18n/translations';
|
||||||
|
|
||||||
export function CompetitionPage() {
|
export function CompetitionPage() {
|
||||||
|
const { language } = useLanguage();
|
||||||
|
const [selectedTrader, setSelectedTrader] = useState<any>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const { data: competition } = useSWR<CompetitionData>(
|
const { data: competition } = useSWR<CompetitionData>(
|
||||||
'competition',
|
'competition',
|
||||||
api.getCompetition,
|
api.getCompetition,
|
||||||
@@ -16,6 +23,21 @@ export function CompetitionPage() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTraderClick = async (traderId: string) => {
|
||||||
|
try {
|
||||||
|
const traderConfig = await api.getTraderConfig(traderId);
|
||||||
|
setSelectedTrader(traderConfig);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch trader config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setSelectedTrader(null);
|
||||||
|
};
|
||||||
|
|
||||||
if (!competition || !competition.traders) {
|
if (!competition || !competition.traders) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -52,26 +74,26 @@ export function CompetitionPage() {
|
|||||||
{/* Competition Header - 精简版 */}
|
{/* Competition Header - 精简版 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
||||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
||||||
}}>
|
}}>
|
||||||
<Trophy className="w-7 h-7" style={{ color: '#000' }} />
|
🏆
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
AI竞赛
|
{t('aiCompetition', language)}
|
||||||
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
|
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
|
||||||
{competition.count} 交易员
|
{competition.count} {t('traders', language)}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
实时对战
|
{t('liveBattle', language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>领先者</div>
|
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
|
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
|
||||||
<div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
<div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||||
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||||
@@ -85,10 +107,10 @@ export function CompetitionPage() {
|
|||||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
表现对比
|
{t('performanceComparison', language)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
实时收益率
|
{t('realTimePnL', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ComparisonChart traders={sortedTraders} />
|
<ComparisonChart traders={sortedTraders} />
|
||||||
@@ -98,10 +120,10 @@ export function CompetitionPage() {
|
|||||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
排行榜
|
{t('leaderboard', language)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-xs px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
<div className="text-xs px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
||||||
实时
|
{t('live', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -112,7 +134,8 @@ export function CompetitionPage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={trader.trader_id}
|
key={trader.trader_id}
|
||||||
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px]"
|
onClick={() => handleTraderClick(trader.trader_id)}
|
||||||
|
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
background: isLeader ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)' : '#0B0E11',
|
background: isLeader ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)' : '#0B0E11',
|
||||||
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
||||||
@@ -122,17 +145,13 @@ export function CompetitionPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Rank & Name */}
|
{/* Rank & Name */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={{
|
<div className="text-2xl w-6">
|
||||||
background: index === 0 ? 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)' :
|
{index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'}
|
||||||
index === 1 ? 'linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%)' :
|
|
||||||
'linear-gradient(135deg, #CD7F32 0%, #8B4513 100%)'
|
|
||||||
}}>
|
|
||||||
<Medal className="w-5 h-5" style={{ color: '#000' }} />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
|
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
|
||||||
<div className="text-xs mono font-semibold" style={{ color: traderColor }}>
|
<div className="text-xs mono font-semibold" style={{ color: traderColor }}>
|
||||||
{trader.ai_model.toUpperCase()}
|
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +160,7 @@ export function CompetitionPage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Total Equity */}
|
{/* Total Equity */}
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>权益</div>
|
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
|
||||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||||
{trader.total_equity?.toFixed(2) || '0.00'}
|
{trader.total_equity?.toFixed(2) || '0.00'}
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +168,7 @@ export function CompetitionPage() {
|
|||||||
|
|
||||||
{/* P&L */}
|
{/* P&L */}
|
||||||
<div className="text-right min-w-[90px]">
|
<div className="text-right min-w-[90px]">
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>收益</div>
|
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
|
||||||
<div
|
<div
|
||||||
className="text-lg font-bold mono"
|
className="text-lg font-bold mono"
|
||||||
style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}
|
style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||||
@@ -164,7 +183,7 @@ export function CompetitionPage() {
|
|||||||
|
|
||||||
{/* Positions */}
|
{/* Positions */}
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>持仓</div>
|
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pos', language)}</div>
|
||||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||||
{trader.position_count}
|
{trader.position_count}
|
||||||
</div>
|
</div>
|
||||||
@@ -176,17 +195,13 @@ export function CompetitionPage() {
|
|||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="px-2 py-1 rounded text-xs font-bold flex items-center justify-center"
|
className="px-2 py-1 rounded text-xs font-bold"
|
||||||
style={trader.is_running
|
style={trader.is_running
|
||||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{trader.is_running ? (
|
{trader.is_running ? '●' : '○'}
|
||||||
<CircleDot className="w-3 h-3" />
|
|
||||||
) : (
|
|
||||||
<Circle className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,7 +217,7 @@ export function CompetitionPage() {
|
|||||||
{competition.traders.length === 2 && (
|
{competition.traders.length === 2 && (
|
||||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.3s' }}>
|
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.3s' }}>
|
||||||
<h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
正面对决
|
{t('headToHead', language)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{sortedTraders.map((trader, index) => {
|
{sortedTraders.map((trader, index) => {
|
||||||
@@ -229,22 +244,25 @@ export function CompetitionPage() {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className="text-base font-bold mb-2"
|
className="text-base font-bold mb-1"
|
||||||
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
|
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
|
||||||
>
|
>
|
||||||
{trader.trader_name}
|
{trader.trader_name}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs mono mb-2" style={{ color: '#848E9C' }}>
|
||||||
|
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
|
||||||
|
</div>
|
||||||
<div className="text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
<div className="text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||||
</div>
|
</div>
|
||||||
{isWinning && gap > 0 && (
|
{isWinning && gap > 0 && (
|
||||||
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
||||||
领先 {gap.toFixed(2)}%
|
{t('leadingBy', language, { gap: gap.toFixed(2) })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isWinning && gap < 0 && (
|
{!isWinning && gap < 0 && (
|
||||||
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}>
|
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}>
|
||||||
落后 {Math.abs(gap).toFixed(2)}%
|
{t('behindBy', language, { gap: Math.abs(gap).toFixed(2) })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -254,6 +272,13 @@ export function CompetitionPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Trader Config Modal */}
|
||||||
|
<TraderConfigModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={closeModal}
|
||||||
|
traderData={selectedTrader}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -14,8 +14,8 @@ export function Header({ simple = false }: HeaderProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Left - Logo and Title */}
|
{/* Left - Logo and Title */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
|
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="h-10 w-auto" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useAuth } from '../contexts/AuthContext';
|
|||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { t } from '../i18n/translations';
|
import { t } from '../i18n/translations';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { Smartphone } from 'lucide-react';
|
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const { language } = useLanguage();
|
const { language } = useLanguage();
|
||||||
@@ -59,7 +58,7 @@ export function LoginPage() {
|
|||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
|
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
{t('loginTitle', language)}
|
{t('loginTitle', language)}
|
||||||
@@ -121,9 +120,7 @@ export function LoginPage() {
|
|||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleOTPVerify} className="space-y-4">
|
<form onSubmit={handleOTPVerify} className="space-y-4">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<div className="mb-2 flex justify-center">
|
<div className="text-4xl mb-2">📱</div>
|
||||||
<Smartphone className="w-10 h-10" style={{ color: '#F0B90B' }} />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
{t('scanQRCodeInstructions', language)}<br />
|
{t('scanQRCodeInstructions', language)}<br />
|
||||||
{t('enterOTPCode', language)}
|
{t('enterOTPCode', language)}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { t } from '../i18n/translations';
|
import { t } from '../i18n/translations';
|
||||||
import { Smartphone, Lock } from 'lucide-react';
|
|
||||||
|
|
||||||
export function RegisterPage() {
|
export function RegisterPage() {
|
||||||
const { language } = useLanguage();
|
const { language } = useLanguage();
|
||||||
@@ -77,7 +76,7 @@ export function RegisterPage() {
|
|||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
|
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
{t('appTitle', language)}
|
{t('appTitle', language)}
|
||||||
@@ -158,9 +157,7 @@ export function RegisterPage() {
|
|||||||
{step === 'setup-otp' && (
|
{step === 'setup-otp' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-2 flex justify-center">
|
<div className="text-4xl mb-2">📱</div>
|
||||||
<Smartphone className="w-10 h-10" style={{ color: '#F0B90B' }} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
<h3 className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||||
{t('setupTwoFactor', language)}
|
{t('setupTwoFactor', language)}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -238,9 +235,7 @@ export function RegisterPage() {
|
|||||||
{step === 'verify-otp' && (
|
{step === 'verify-otp' && (
|
||||||
<form onSubmit={handleOTPVerify} className="space-y-4">
|
<form onSubmit={handleOTPVerify} className="space-y-4">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<div className="mb-2 flex justify-center">
|
<div className="text-4xl mb-2">🔐</div>
|
||||||
<Lock className="w-10 h-10" style={{ color: '#F0B90B' }} />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
{t('enterOTPCode', language)}<br />
|
{t('enterOTPCode', language)}<br />
|
||||||
{t('completeRegistrationSubtitle', language)}
|
{t('completeRegistrationSubtitle', language)}
|
||||||
|
|||||||
443
web/src/components/TraderConfigModal.tsx
Normal file
443
web/src/components/TraderConfigModal.tsx
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import type { AIModel, Exchange, CreateTraderRequest } from '../types';
|
||||||
|
|
||||||
|
// 提取下划线后面的名称部分
|
||||||
|
function getShortName(fullName: string): string {
|
||||||
|
const parts = fullName.split('_');
|
||||||
|
return parts.length > 1 ? parts[parts.length - 1] : fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TraderConfigData {
|
||||||
|
trader_id?: string;
|
||||||
|
trader_name: string;
|
||||||
|
ai_model: string;
|
||||||
|
exchange_id: string;
|
||||||
|
btc_eth_leverage: number;
|
||||||
|
altcoin_leverage: number;
|
||||||
|
trading_symbols: string;
|
||||||
|
custom_prompt: string;
|
||||||
|
override_base_prompt: boolean;
|
||||||
|
is_cross_margin: boolean;
|
||||||
|
use_coin_pool: boolean;
|
||||||
|
use_oi_top: boolean;
|
||||||
|
initial_balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TraderConfigModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
traderData?: TraderConfigData | null;
|
||||||
|
isEditMode?: boolean;
|
||||||
|
availableModels?: AIModel[];
|
||||||
|
availableExchanges?: Exchange[];
|
||||||
|
onSave?: (data: CreateTraderRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TraderConfigModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
traderData,
|
||||||
|
isEditMode = false,
|
||||||
|
availableModels = [],
|
||||||
|
availableExchanges = [],
|
||||||
|
onSave
|
||||||
|
}: TraderConfigModalProps) {
|
||||||
|
const [formData, setFormData] = useState<TraderConfigData>({
|
||||||
|
trader_name: '',
|
||||||
|
ai_model: '',
|
||||||
|
exchange_id: '',
|
||||||
|
btc_eth_leverage: 5,
|
||||||
|
altcoin_leverage: 3,
|
||||||
|
trading_symbols: '',
|
||||||
|
custom_prompt: '',
|
||||||
|
override_base_prompt: false,
|
||||||
|
is_cross_margin: true,
|
||||||
|
use_coin_pool: false,
|
||||||
|
use_oi_top: false,
|
||||||
|
initial_balance: 1000,
|
||||||
|
});
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [availableCoins, setAvailableCoins] = useState<string[]>([]);
|
||||||
|
const [selectedCoins, setSelectedCoins] = useState<string[]>([]);
|
||||||
|
const [showCoinSelector, setShowCoinSelector] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (traderData) {
|
||||||
|
setFormData(traderData);
|
||||||
|
// 设置已选择的币种
|
||||||
|
if (traderData.trading_symbols) {
|
||||||
|
const coins = traderData.trading_symbols.split(',').map(s => s.trim()).filter(s => s);
|
||||||
|
setSelectedCoins(coins);
|
||||||
|
}
|
||||||
|
} else if (!isEditMode) {
|
||||||
|
setFormData({
|
||||||
|
trader_name: '',
|
||||||
|
ai_model: availableModels[0]?.id || '',
|
||||||
|
exchange_id: availableExchanges[0]?.id || '',
|
||||||
|
btc_eth_leverage: 5,
|
||||||
|
altcoin_leverage: 3,
|
||||||
|
trading_symbols: '',
|
||||||
|
custom_prompt: '',
|
||||||
|
override_base_prompt: false,
|
||||||
|
is_cross_margin: true,
|
||||||
|
use_coin_pool: false,
|
||||||
|
use_oi_top: false,
|
||||||
|
initial_balance: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [traderData, isEditMode, availableModels, availableExchanges]);
|
||||||
|
|
||||||
|
// 获取系统配置中的币种列表
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config');
|
||||||
|
const config = await response.json();
|
||||||
|
if (config.default_coins) {
|
||||||
|
setAvailableCoins(config.default_coins);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch config:', error);
|
||||||
|
// 使用默认币种列表
|
||||||
|
setAvailableCoins(['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT', 'DOGEUSDT', 'ADAUSDT']);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 当选择的币种改变时,更新输入框
|
||||||
|
useEffect(() => {
|
||||||
|
const symbolsString = selectedCoins.join(',');
|
||||||
|
setFormData(prev => ({ ...prev, trading_symbols: symbolsString }));
|
||||||
|
}, [selectedCoins]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof TraderConfigData, value: any) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
|
// 如果是直接编辑trading_symbols,同步更新selectedCoins
|
||||||
|
if (field === 'trading_symbols') {
|
||||||
|
const coins = value.split(',').map((s: string) => s.trim()).filter((s: string) => s);
|
||||||
|
setSelectedCoins(coins);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCoinToggle = (coin: string) => {
|
||||||
|
setSelectedCoins(prev => {
|
||||||
|
if (prev.includes(coin)) {
|
||||||
|
return prev.filter(c => c !== coin);
|
||||||
|
} else {
|
||||||
|
return [...prev, coin];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!onSave) return;
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const saveData: CreateTraderRequest = {
|
||||||
|
name: formData.trader_name,
|
||||||
|
ai_model_id: formData.ai_model,
|
||||||
|
exchange_id: formData.exchange_id,
|
||||||
|
btc_eth_leverage: formData.btc_eth_leverage,
|
||||||
|
altcoin_leverage: formData.altcoin_leverage,
|
||||||
|
trading_symbols: formData.trading_symbols,
|
||||||
|
custom_prompt: formData.custom_prompt,
|
||||||
|
override_base_prompt: formData.override_base_prompt,
|
||||||
|
is_cross_margin: formData.is_cross_margin,
|
||||||
|
use_coin_pool: formData.use_coin_pool,
|
||||||
|
use_oi_top: formData.use_oi_top,
|
||||||
|
initial_balance: formData.initial_balance,
|
||||||
|
};
|
||||||
|
await onSave(saveData);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
|
||||||
|
<div
|
||||||
|
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#F0B90B] to-[#E1A706] flex items-center justify-center">
|
||||||
|
<span className="text-lg">{isEditMode ? '✏️' : '➕'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-[#EAECEF]">
|
||||||
|
{isEditMode ? '修改交易员' : '创建交易员'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-[#848E9C] mt-1">
|
||||||
|
{isEditMode ? '修改交易员配置参数' : '配置新的AI交易员'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-8">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||||
|
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||||
|
🤖 基础配置
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">交易员名称</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.trader_name}
|
||||||
|
onChange={(e) => handleInputChange('trader_name', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
placeholder="请输入交易员名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">AI模型</label>
|
||||||
|
<select
|
||||||
|
value={formData.ai_model}
|
||||||
|
onChange={(e) => handleInputChange('ai_model', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
>
|
||||||
|
{availableModels.map(model => (
|
||||||
|
<option key={model.id} value={model.id}>
|
||||||
|
{getShortName(model.name || model.id).toUpperCase()}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">交易所</label>
|
||||||
|
<select
|
||||||
|
value={formData.exchange_id}
|
||||||
|
onChange={(e) => handleInputChange('exchange_id', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
>
|
||||||
|
{availableExchanges.map(exchange => (
|
||||||
|
<option key={exchange.id} value={exchange.id}>
|
||||||
|
{getShortName(exchange.name || exchange.id).toUpperCase()}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trading Configuration */}
|
||||||
|
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||||
|
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||||
|
⚖️ 交易配置
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 第一行:保证金模式和初始余额 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">保证金模式</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleInputChange('is_cross_margin', true)}
|
||||||
|
className={`flex-1 px-3 py-2 rounded text-sm ${
|
||||||
|
formData.is_cross_margin
|
||||||
|
? 'bg-[#F0B90B] text-black'
|
||||||
|
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
全仓
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleInputChange('is_cross_margin', false)}
|
||||||
|
className={`flex-1 px-3 py-2 rounded text-sm ${
|
||||||
|
!formData.is_cross_margin
|
||||||
|
? 'bg-[#F0B90B] text-black'
|
||||||
|
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
逐仓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">初始余额 ($)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.initial_balance}
|
||||||
|
onChange={(e) => handleInputChange('initial_balance', Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
min="100"
|
||||||
|
step="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第二行:杠杆设置 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">BTC/ETH 杠杆</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.btc_eth_leverage}
|
||||||
|
onChange={(e) => handleInputChange('btc_eth_leverage', Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
min="1"
|
||||||
|
max="125"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">山寨币杠杆</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.altcoin_leverage}
|
||||||
|
onChange={(e) => handleInputChange('altcoin_leverage', Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
min="1"
|
||||||
|
max="75"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第三行:交易币种 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="text-sm text-[#EAECEF]">交易币种 (用逗号分隔,留空使用默认)</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowCoinSelector(!showCoinSelector)}
|
||||||
|
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors"
|
||||||
|
>
|
||||||
|
{showCoinSelector ? '收起选择' : '快速选择'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.trading_symbols}
|
||||||
|
onChange={(e) => handleInputChange('trading_symbols', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||||
|
placeholder="例如: BTCUSDT,ETHUSDT,ADAUSDT"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 币种选择器 */}
|
||||||
|
{showCoinSelector && (
|
||||||
|
<div className="mt-3 p-3 bg-[#0B0E11] border border-[#2B3139] rounded">
|
||||||
|
<div className="text-xs text-[#848E9C] mb-2">点击选择币种:</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{availableCoins.map(coin => (
|
||||||
|
<button
|
||||||
|
key={coin}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCoinToggle(coin)}
|
||||||
|
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||||
|
selectedCoins.includes(coin)
|
||||||
|
? 'bg-[#F0B90B] text-black'
|
||||||
|
: 'bg-[#1E2329] text-[#848E9C] border border-[#2B3139] hover:border-[#F0B90B]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{coin.replace('USDT', '')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signal Sources */}
|
||||||
|
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||||
|
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||||
|
📡 信号源配置
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.use_coin_pool}
|
||||||
|
onChange={(e) => handleInputChange('use_coin_pool', e.target.checked)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-[#EAECEF]">使用 Coin Pool 信号</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.use_oi_top}
|
||||||
|
onChange={(e) => handleInputChange('use_oi_top', e.target.checked)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-[#EAECEF]">使用 OI Top 信号</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trading Prompt */}
|
||||||
|
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||||
|
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||||
|
💬 交易策略提示词
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.override_base_prompt}
|
||||||
|
onChange={(e) => handleInputChange('override_base_prompt', e.target.checked)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-[#EAECEF]">覆盖默认提示词</label>
|
||||||
|
<span className="text-xs text-[#F0B90B]">⚠️ 启用后将完全替换默认策略</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||||
|
{formData.override_base_prompt ? '自定义提示词' : '附加提示词'}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.custom_prompt}
|
||||||
|
onChange={(e) => handleInputChange('custom_prompt', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none h-24 resize-none"
|
||||||
|
placeholder={formData.override_base_prompt ? "输入完整的交易策略提示词..." : "输入额外的交易策略提示..."}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex justify-end gap-3 p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
{onSave && (
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving || !formData.trader_name || !formData.ai_model || !formData.exchange_id}
|
||||||
|
className="px-8 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 disabled:bg-[#848E9C] disabled:cursor-not-allowed font-medium shadow-lg"
|
||||||
|
>
|
||||||
|
{isSaving ? '保存中...' : (isEditMode ? '保存修改' : '创建交易员')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ export type Language = 'en' | 'zh';
|
|||||||
export const translations = {
|
export const translations = {
|
||||||
en: {
|
en: {
|
||||||
// Header
|
// Header
|
||||||
appTitle: 'AI Trading System',
|
appTitle: 'NOFX',
|
||||||
subtitle: 'Multi-AI Model Trading Platform',
|
subtitle: 'Multi-AI Model Trading Platform',
|
||||||
aiTraders: 'AI Traders',
|
aiTraders: 'AI Traders',
|
||||||
details: 'Details',
|
details: 'Details',
|
||||||
@@ -64,11 +64,17 @@ export const translations = {
|
|||||||
recent: 'Recent',
|
recent: 'Recent',
|
||||||
allData: 'All Data',
|
allData: 'All Data',
|
||||||
cycles: 'Cycles',
|
cycles: 'Cycles',
|
||||||
|
|
||||||
|
// Comparison Chart
|
||||||
|
comparisonMode: 'Comparison Mode',
|
||||||
|
dataPoints: 'Data Points',
|
||||||
|
currentGap: 'Current Gap',
|
||||||
|
count: '{count} pts',
|
||||||
|
|
||||||
// Competition Page
|
// Competition Page
|
||||||
aiCompetition: 'AI Competition',
|
aiCompetition: 'AI Competition',
|
||||||
traders: 'traders',
|
traders: 'traders',
|
||||||
liveBattle: 'Qwen vs DeepSeek · Live Battle',
|
liveBattle: 'Live Battle',
|
||||||
leader: 'Leader',
|
leader: 'Leader',
|
||||||
leaderboard: 'Leaderboard',
|
leaderboard: 'Leaderboard',
|
||||||
live: 'LIVE',
|
live: 'LIVE',
|
||||||
@@ -147,6 +153,38 @@ export const translations = {
|
|||||||
useTestnet: 'Use Testnet',
|
useTestnet: 'Use Testnet',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
|
||||||
|
// AI Model Configuration
|
||||||
|
officialAPI: 'Official API',
|
||||||
|
customAPI: 'Custom API',
|
||||||
|
apiKey: 'API Key',
|
||||||
|
customAPIURL: 'Custom API URL',
|
||||||
|
enterAPIKey: 'Enter API Key',
|
||||||
|
enterCustomAPIURL: 'Enter custom API endpoint URL',
|
||||||
|
useOfficialAPI: 'Use official API service',
|
||||||
|
useCustomAPI: 'Use custom API endpoint',
|
||||||
|
|
||||||
|
// Trader Configuration
|
||||||
|
positionMode: 'Position Mode',
|
||||||
|
crossMarginMode: 'Cross Margin',
|
||||||
|
isolatedMarginMode: 'Isolated Margin',
|
||||||
|
crossMarginDescription: 'Cross margin: All positions share account balance as collateral',
|
||||||
|
isolatedMarginDescription: 'Isolated margin: Each position manages collateral independently, risk isolation',
|
||||||
|
leverageConfiguration: 'Leverage Configuration',
|
||||||
|
btcEthLeverage: 'BTC/ETH Leverage',
|
||||||
|
altcoinLeverage: 'Altcoin Leverage',
|
||||||
|
leverageRecommendation: 'Recommended: BTC/ETH 5-10x, Altcoins 3-5x for risk control',
|
||||||
|
tradingSymbols: 'Trading Symbols',
|
||||||
|
tradingSymbolsPlaceholder: 'Enter symbols, comma separated (e.g., BTCUSDT,ETHUSDT,SOLUSDT)',
|
||||||
|
selectSymbols: 'Select Symbols',
|
||||||
|
selectTradingSymbols: 'Select Trading Symbols',
|
||||||
|
selectedSymbolsCount: 'Selected {count} symbols',
|
||||||
|
clearSelection: 'Clear All',
|
||||||
|
confirmSelection: 'Confirm',
|
||||||
|
tradingSymbolsDescription: 'Empty = use default symbols. Must end with USDT (e.g., BTCUSDT, ETHUSDT)',
|
||||||
|
btcEthLeverageValidation: 'BTC/ETH leverage must be between 1-50x',
|
||||||
|
altcoinLeverageValidation: 'Altcoin leverage must be between 1-20x',
|
||||||
|
invalidSymbolFormat: 'Invalid symbol format: {symbol}, must end with USDT',
|
||||||
|
|
||||||
// Loading & Error
|
// Loading & Error
|
||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
@@ -207,7 +245,7 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
// Header
|
// Header
|
||||||
appTitle: 'AI交易系统',
|
appTitle: 'NOFX',
|
||||||
subtitle: '多AI模型交易平台',
|
subtitle: '多AI模型交易平台',
|
||||||
aiTraders: 'AI交易员',
|
aiTraders: 'AI交易员',
|
||||||
details: '详情',
|
details: '详情',
|
||||||
@@ -268,22 +306,28 @@ export const translations = {
|
|||||||
recent: '最近',
|
recent: '最近',
|
||||||
allData: '全部数据',
|
allData: '全部数据',
|
||||||
cycles: '个',
|
cycles: '个',
|
||||||
|
|
||||||
|
// Comparison Chart
|
||||||
|
comparisonMode: '对比模式',
|
||||||
|
dataPoints: '数据点数',
|
||||||
|
currentGap: '当前差距',
|
||||||
|
count: '{count} 个',
|
||||||
|
|
||||||
// Competition Page
|
// Competition Page
|
||||||
aiCompetition: 'AI竞赛',
|
aiCompetition: 'AI竞赛',
|
||||||
traders: '位交易者',
|
traders: '交易员',
|
||||||
liveBattle: 'Qwen vs DeepSeek · 实时对战',
|
liveBattle: '实时对战',
|
||||||
leader: '🥇 领先者',
|
leader: '领先者',
|
||||||
leaderboard: '🥇 排行榜',
|
leaderboard: '排行榜',
|
||||||
live: '直播',
|
live: '实时',
|
||||||
performanceComparison: '📈 表现对比',
|
performanceComparison: '表现对比',
|
||||||
realTimePnL: '实时盈亏百分比',
|
realTimePnL: '实时收益率',
|
||||||
headToHead: '⚔️ 正面对决',
|
headToHead: '正面对决',
|
||||||
leadingBy: '领先 {gap}%',
|
leadingBy: '领先 {gap}%',
|
||||||
behindBy: '落后 {gap}%',
|
behindBy: '落后 {gap}%',
|
||||||
equity: '净值',
|
equity: '权益',
|
||||||
pnl: '盈亏',
|
pnl: '收益',
|
||||||
pos: '仓位',
|
pos: '持仓',
|
||||||
|
|
||||||
// AI Learning
|
// AI Learning
|
||||||
aiLearning: 'AI学习与反思',
|
aiLearning: 'AI学习与反思',
|
||||||
@@ -351,6 +395,38 @@ export const translations = {
|
|||||||
useTestnet: '使用测试网',
|
useTestnet: '使用测试网',
|
||||||
enabled: '启用',
|
enabled: '启用',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
|
|
||||||
|
// AI Model Configuration
|
||||||
|
officialAPI: '官方API',
|
||||||
|
customAPI: '自定义API',
|
||||||
|
apiKey: 'API密钥',
|
||||||
|
customAPIURL: '自定义API地址',
|
||||||
|
enterAPIKey: '请输入API密钥',
|
||||||
|
enterCustomAPIURL: '请输入自定义API端点地址',
|
||||||
|
useOfficialAPI: '使用官方API服务',
|
||||||
|
useCustomAPI: '使用自定义API端点',
|
||||||
|
|
||||||
|
// Trader Configuration
|
||||||
|
positionMode: '仓位模式',
|
||||||
|
crossMarginMode: '全仓模式',
|
||||||
|
isolatedMarginMode: '逐仓模式',
|
||||||
|
crossMarginDescription: '全仓模式:所有仓位共享账户余额作为保证金',
|
||||||
|
isolatedMarginDescription: '逐仓模式:每个仓位独立管理保证金,风险隔离',
|
||||||
|
leverageConfiguration: '杠杆配置',
|
||||||
|
btcEthLeverage: 'BTC/ETH杠杆',
|
||||||
|
altcoinLeverage: '山寨币杠杆',
|
||||||
|
leverageRecommendation: '推荐:BTC/ETH 5-10倍,山寨币 3-5倍,控制风险',
|
||||||
|
tradingSymbols: '交易币种',
|
||||||
|
tradingSymbolsPlaceholder: '输入币种,逗号分隔(如:BTCUSDT,ETHUSDT,SOLUSDT)',
|
||||||
|
selectSymbols: '选择币种',
|
||||||
|
selectTradingSymbols: '选择交易币种',
|
||||||
|
selectedSymbolsCount: '已选择 {count} 个币种',
|
||||||
|
clearSelection: '清空选择',
|
||||||
|
confirmSelection: '确认选择',
|
||||||
|
tradingSymbolsDescription: '留空 = 使用默认币种。必须以USDT结尾(如:BTCUSDT, ETHUSDT)',
|
||||||
|
btcEthLeverageValidation: 'BTC/ETH杠杆必须在1-50倍之间',
|
||||||
|
altcoinLeverageValidation: '山寨币杠杆必须在1-20倍之间',
|
||||||
|
invalidSymbolFormat: '无效的币种格式:{symbol},必须以USDT结尾',
|
||||||
|
|
||||||
// Loading & Error
|
// Loading & Error
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
|
|||||||
@@ -82,6 +82,24 @@ export const api = {
|
|||||||
if (!res.ok) throw new Error('更新自定义策略失败');
|
if (!res.ok) throw new Error('更新自定义策略失败');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getTraderConfig(traderId: string): Promise<any> {
|
||||||
|
const res = await fetch(`${API_BASE}/traders/${traderId}/config`, {
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('获取交易员配置失败');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTrader(traderId: string, request: CreateTraderRequest): Promise<TraderInfo> {
|
||||||
|
const res = await fetch(`${API_BASE}/traders/${traderId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('更新交易员失败');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
// AI模型配置接口
|
// AI模型配置接口
|
||||||
async getModelConfigs(): Promise<AIModel[]> {
|
async getModelConfigs(): Promise<AIModel[]> {
|
||||||
const res = await fetch(`${API_BASE}/models`, {
|
const res = await fetch(`${API_BASE}/models`, {
|
||||||
@@ -242,4 +260,25 @@ export const api = {
|
|||||||
if (!res.ok) throw new Error('获取竞赛数据失败');
|
if (!res.ok) throw new Error('获取竞赛数据失败');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 用户信号源配置接口
|
||||||
|
async getUserSignalSource(): Promise<{coin_pool_url: string, oi_top_url: string}> {
|
||||||
|
const res = await fetch(`${API_BASE}/user/signal-sources`, {
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('获取用户信号源配置失败');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUserSignalSource(coinPoolUrl: string, oiTopUrl: string): Promise<void> {
|
||||||
|
const res = await fetch(`${API_BASE}/user/signal-sources`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
coin_pool_url: coinPoolUrl,
|
||||||
|
oi_top_url: oiTopUrl,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('保存用户信号源配置失败');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export interface AIModel {
|
|||||||
provider: string;
|
provider: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
customApiUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Exchange {
|
export interface Exchange {
|
||||||
@@ -123,9 +124,14 @@ export interface CreateTraderRequest {
|
|||||||
ai_model_id: string;
|
ai_model_id: string;
|
||||||
exchange_id: string;
|
exchange_id: string;
|
||||||
initial_balance: number;
|
initial_balance: number;
|
||||||
|
btc_eth_leverage?: number;
|
||||||
|
altcoin_leverage?: number;
|
||||||
|
trading_symbols?: string;
|
||||||
custom_prompt?: string;
|
custom_prompt?: string;
|
||||||
override_base_prompt?: boolean;
|
override_base_prompt?: boolean;
|
||||||
is_cross_margin?: boolean;
|
is_cross_margin?: boolean;
|
||||||
|
use_coin_pool?: boolean;
|
||||||
|
use_oi_top?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateModelConfigRequest {
|
export interface UpdateModelConfigRequest {
|
||||||
@@ -133,6 +139,7 @@ export interface UpdateModelConfigRequest {
|
|||||||
[key: string]: {
|
[key: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
|
custom_api_url?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -159,6 +166,7 @@ export interface CompetitionTraderData {
|
|||||||
trader_id: string;
|
trader_id: string;
|
||||||
trader_name: string;
|
trader_name: string;
|
||||||
ai_model: string;
|
ai_model: string;
|
||||||
|
exchange: string;
|
||||||
total_equity: number;
|
total_equity: number;
|
||||||
total_pnl: number;
|
total_pnl: number;
|
||||||
total_pnl_pct: number;
|
total_pnl_pct: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user