mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 02:21:19 +08:00
313
api/server.go
313
api/server.go
@@ -1,12 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"nofx/auth"
|
||||
"nofx/config"
|
||||
"nofx/manager"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -87,7 +89,9 @@ func (s *Server) setupRoutes() {
|
||||
{
|
||||
// AI交易员管理
|
||||
protected.GET("/traders", s.handleTraderList)
|
||||
protected.GET("/traders/:id/config", s.handleGetTraderConfig)
|
||||
protected.POST("/traders", s.handleCreateTrader)
|
||||
protected.PUT("/traders/:id", s.handleUpdateTrader)
|
||||
protected.DELETE("/traders/:id", s.handleDeleteTrader)
|
||||
protected.POST("/traders/:id/start", s.handleStartTrader)
|
||||
protected.POST("/traders/:id/stop", s.handleStopTrader)
|
||||
@@ -101,6 +105,10 @@ func (s *Server) setupRoutes() {
|
||||
protected.GET("/exchanges", s.handleGetExchangeConfigs)
|
||||
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
|
||||
|
||||
// 用户信号源配置
|
||||
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
|
||||
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
|
||||
|
||||
// 竞赛总览
|
||||
protected.GET("/competition", s.handleCompetition)
|
||||
|
||||
@@ -127,8 +135,36 @@ func (s *Server) handleHealth(c *gin.Context) {
|
||||
|
||||
// handleGetSystemConfig 获取系统配置(客户端需要知道的配置)
|
||||
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{
|
||||
"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交易员管理相关结构体
|
||||
type CreateTraderRequest 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"`
|
||||
CustomPrompt string `json:"custom_prompt"`
|
||||
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"` // 指针类型,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 {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"apiKey,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"apiKey,omitempty"`
|
||||
CustomAPIURL string `json:"customApiUrl,omitempty"`
|
||||
}
|
||||
|
||||
type ExchangeConfig struct {
|
||||
@@ -193,8 +235,9 @@ type ExchangeConfig struct {
|
||||
|
||||
type UpdateModelConfigRequest struct {
|
||||
Models map[string]struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"api_key"`
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"api_key"`
|
||||
CustomAPIURL string `json:"custom_api_url"`
|
||||
} `json:"models"`
|
||||
}
|
||||
|
||||
@@ -220,6 +263,28 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// 设置杠杆默认值(从系统配置获取)
|
||||
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{
|
||||
ID: traderID,
|
||||
@@ -237,6 +326,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
AIModelID: req.AIModelID,
|
||||
ExchangeID: req.ExchangeID,
|
||||
InitialBalance: req.InitialBalance,
|
||||
BTCETHLeverage: btcEthLeverage,
|
||||
AltcoinLeverage: altcoinLeverage,
|
||||
TradingSymbols: req.TradingSymbols,
|
||||
UseCoinPool: req.UseCoinPool,
|
||||
UseOITop: req.UseOITop,
|
||||
CustomPrompt: req.CustomPrompt,
|
||||
OverrideBasePrompt: req.OverrideBasePrompt,
|
||||
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 删除交易员
|
||||
func (s *Server) handleDeleteTrader(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
@@ -419,7 +615,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
||||
|
||||
// 更新每个模型的配置
|
||||
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 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
|
||||
return
|
||||
@@ -467,6 +663,48 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
||||
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列表
|
||||
func (s *Server) handleTraderList(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
@@ -500,6 +738,51 @@ func (s *Server) handleTraderList(c *gin.Context) {
|
||||
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 系统状态
|
||||
func (s *Server) handleStatus(c *gin.Context) {
|
||||
_, traderID, err := s.getTraderFromQuery(c)
|
||||
@@ -668,7 +951,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
|
||||
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
||||
}
|
||||
|
||||
competition, err := s.traderManager.GetCompetitionData(userID)
|
||||
competition, err := s.traderManager.GetCompetitionData()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": fmt.Sprintf("获取竞赛数据失败: %v", err),
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
"ADAUSDT",
|
||||
"HYPEUSDT"
|
||||
],
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
"api_server_port": 8080,
|
||||
"max_daily_loss": 10.0,
|
||||
"max_drawdown": 20.0,
|
||||
|
||||
@@ -55,8 +55,6 @@ type Config struct {
|
||||
Traders []TraderConfig `json:"traders"`
|
||||
UseDefaultCoins bool `json:"use_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"`
|
||||
MaxDailyLoss float64 `json:"max_daily_loss"`
|
||||
MaxDrawdown float64 `json:"max_drawdown"`
|
||||
@@ -76,8 +74,8 @@ func LoadConfig(filename string) (*Config, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,18 @@ func (d *Database) createTables() error {
|
||||
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 (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -82,6 +94,11 @@ func (d *Database) createTables() error {
|
||||
initial_balance REAL NOT NULL,
|
||||
scan_interval_minutes INTEGER DEFAULT 3,
|
||||
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,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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;
|
||||
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
|
||||
AFTER UPDATE ON system_config
|
||||
BEGIN
|
||||
@@ -154,6 +177,14 @@ func (d *Database) createTables() error {
|
||||
`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 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 {
|
||||
@@ -215,8 +246,6 @@ func (d *Database) initDefaultData() error {
|
||||
"api_server_port": "8080", // 默认API端口
|
||||
"use_default_coins": "true", // 默认使用内置币种列表
|
||||
"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_drawdown": "20.0", // 最大回撤百分比
|
||||
"stop_trading_minutes": "60", // 停止交易时间(分钟)
|
||||
@@ -333,14 +362,15 @@ type User struct {
|
||||
|
||||
// AIModelConfig AI模型配置
|
||||
type AIModelConfig struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"apiKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"apiKey"`
|
||||
CustomAPIURL string `json:"customApiUrl"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ExchangeConfig 交易所配置
|
||||
@@ -373,13 +403,28 @@ type TraderRecord struct {
|
||||
InitialBalance float64 `json:"initial_balance"`
|
||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||
IsRunning bool `json:"is_running"`
|
||||
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
||||
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
||||
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓)
|
||||
BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数
|
||||
AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数
|
||||
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"`
|
||||
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密钥
|
||||
func GenerateOTPSecret() (string, error) {
|
||||
secret := make([]byte, 20)
|
||||
@@ -457,6 +502,25 @@ func (d *Database) GetUserByID(userID string) (*User, error) {
|
||||
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验证状态
|
||||
func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
|
||||
_, 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模型配置
|
||||
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||||
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
|
||||
`, userID)
|
||||
if err != nil {
|
||||
@@ -480,7 +544,7 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||||
var model AIModelConfig
|
||||
err := rows.Scan(
|
||||
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
||||
&model.Enabled, &model.APIKey,
|
||||
&model.Enabled, &model.APIKey, &model.CustomAPIURL,
|
||||
&model.CreatedAt, &model.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -493,11 +557,11 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||||
}
|
||||
|
||||
// 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(`
|
||||
UPDATE ai_models SET enabled = ?, api_key = ? WHERE id = ? AND user_id = ?
|
||||
`, enabled, apiKey, id, userID)
|
||||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ? WHERE id = ? AND user_id = ?
|
||||
`, enabled, apiKey, customAPIURL, id, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -532,9 +596,9 @@ func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey string)
|
||||
// 创建用户特定的配置
|
||||
userModelID := fmt.Sprintf("%s_%s", userID, id)
|
||||
_, err = d.db.Exec(`
|
||||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
`, userModelID, userID, name, provider, enabled, apiKey)
|
||||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
`, userModelID, userID, name, provider, enabled, apiKey, customAPIURL)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -643,11 +707,11 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
|
||||
}
|
||||
|
||||
// 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(`
|
||||
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, id, userID, name, provider, enabled, apiKey)
|
||||
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, id, userID, name, provider, enabled, apiKey, customAPIURL)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -663,9 +727,9 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap
|
||||
// CreateTrader 创建交易员
|
||||
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
||||
_, 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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.CustomPrompt, trader.OverrideBasePrompt, trader.IsCrossMargin)
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, 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
|
||||
}
|
||||
|
||||
@@ -673,6 +737,9 @@ func (d *Database) CreateTrader(trader *TraderRecord) error {
|
||||
func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
||||
rows, err := d.db.Query(`
|
||||
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(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
||||
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(
|
||||
&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,
|
||||
&trader.CreatedAt, &trader.UpdatedAt,
|
||||
)
|
||||
@@ -706,6 +775,22 @@ func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error {
|
||||
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
|
||||
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)
|
||||
@@ -772,6 +857,40 @@ func (d *Database) SetSystemConfig(key, value string) error {
|
||||
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 关闭数据库连接
|
||||
func (d *Database) Close() error {
|
||||
return d.db.Close()
|
||||
|
||||
25
main.go
25
main.go
@@ -208,26 +208,37 @@ func main() {
|
||||
log.Fatalf("❌ 加载交易员失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取数据库中的所有交易员配置(用于显示,使用default用户)
|
||||
traders, err := database.GetTraders("default")
|
||||
// 获取所有用户的交易员配置(用于显示)
|
||||
userIDs, err := database.GetAllUsers()
|
||||
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("🤖 数据库中的AI交易员配置:")
|
||||
if len(traders) == 0 {
|
||||
if len(allTraders) == 0 {
|
||||
fmt.Println(" • 暂无配置的交易员,请通过Web界面创建")
|
||||
} else {
|
||||
for _, trader := range traders {
|
||||
for _, trader := range allTraders {
|
||||
status := "停止"
|
||||
if trader.IsRunning {
|
||||
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.InitialBalance, status)
|
||||
trader.UserID, trader.InitialBalance, status)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/config"
|
||||
"nofx/trader"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -28,28 +30,33 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
// 根据admin_mode确定用户ID
|
||||
adminModeStr, _ := database.GetSystemConfig("admin_mode")
|
||||
userID := "default"
|
||||
if adminModeStr != "false" { // 默认为true
|
||||
userID = "admin"
|
||||
}
|
||||
|
||||
// 获取数据库中的所有交易员
|
||||
traders, err := database.GetTraders(userID)
|
||||
// 获取所有用户
|
||||
userIDs, err := database.GetAllUsers()
|
||||
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))
|
||||
|
||||
// 获取系统配置
|
||||
coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url")
|
||||
var allTraders []*config.TraderRecord
|
||||
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")
|
||||
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
|
||||
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
|
||||
btcEthLeverageStr, _ := database.GetSystemConfig("btc_eth_leverage")
|
||||
altcoinLeverageStr, _ := database.GetSystemConfig("altcoin_leverage")
|
||||
defaultCoinsStr, _ := database.GetSystemConfig("default_coins")
|
||||
|
||||
// 解析配置
|
||||
maxDailyLoss := 10.0 // 默认值
|
||||
@@ -67,20 +74,19 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
||||
stopTradingMinutes = val
|
||||
}
|
||||
|
||||
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
|
||||
// 解析默认币种列表
|
||||
var defaultCoins []string
|
||||
if defaultCoinsStr != "" {
|
||||
if err := json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins); err != nil {
|
||||
log.Printf("⚠️ 解析默认币种配置失败: %v,使用空列表", err)
|
||||
defaultCoins = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个交易员获取AI模型和交易所配置
|
||||
for _, traderCfg := range traders {
|
||||
// 获取AI模型配置
|
||||
aiModels, err := database.GetAIModels(userID)
|
||||
for _, traderCfg := range allTraders {
|
||||
// 获取AI模型配置(使用交易员所属的用户ID)
|
||||
aiModels, err := database.GetAIModels(traderCfg.UserID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取AI模型配置失败: %v", err)
|
||||
continue
|
||||
@@ -104,8 +110,8 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取交易所配置
|
||||
exchanges, err := database.GetExchanges(userID)
|
||||
// 获取交易所配置(使用交易员所属的用户ID)
|
||||
exchanges, err := database.GetExchanges(traderCfg.UserID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取交易所配置失败: %v", err)
|
||||
continue
|
||||
@@ -129,8 +135,18 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
||||
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
|
||||
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 {
|
||||
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
|
||||
continue
|
||||
@@ -142,11 +158,36 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
traderConfig := trader.AutoTraderConfig{
|
||||
ID: traderCfg.ID,
|
||||
@@ -157,18 +198,20 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
||||
BinanceSecretKey: "",
|
||||
HyperliquidPrivateKey: "",
|
||||
HyperliquidTestnet: exchangeCfg.Testnet,
|
||||
CoinPoolAPIURL: coinPoolURL,
|
||||
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||
UseQwen: aiModelCfg.Provider == "qwen",
|
||||
DeepSeekKey: "",
|
||||
QwenKey: "",
|
||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||
InitialBalance: traderCfg.InitialBalance,
|
||||
BTCETHLeverage: btcEthLeverage,
|
||||
AltcoinLeverage: altcoinLeverage,
|
||||
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||
MaxDailyLoss: maxDailyLoss,
|
||||
MaxDrawdown: maxDrawdown,
|
||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||
DefaultCoins: defaultCoins,
|
||||
TradingCoins: tradingCoins,
|
||||
}
|
||||
|
||||
// 根据交易所类型设置API密钥
|
||||
@@ -216,7 +259,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
||||
// AddTrader 从数据库配置添加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()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
@@ -224,6 +267,31 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
||||
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
|
||||
traderConfig := trader.AutoTraderConfig{
|
||||
ID: traderCfg.ID,
|
||||
@@ -234,18 +302,20 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
||||
BinanceSecretKey: "",
|
||||
HyperliquidPrivateKey: "",
|
||||
HyperliquidTestnet: exchangeCfg.Testnet,
|
||||
CoinPoolAPIURL: coinPoolURL,
|
||||
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||
UseQwen: aiModelCfg.Provider == "qwen",
|
||||
DeepSeekKey: "",
|
||||
QwenKey: "",
|
||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||
InitialBalance: traderCfg.InitialBalance,
|
||||
BTCETHLeverage: btcEthLeverage,
|
||||
AltcoinLeverage: altcoinLeverage,
|
||||
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||
MaxDailyLoss: maxDailyLoss,
|
||||
MaxDrawdown: maxDrawdown,
|
||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||
DefaultCoins: defaultCoins,
|
||||
TradingCoins: tradingCoins,
|
||||
}
|
||||
|
||||
// 根据交易所类型设置API密钥
|
||||
@@ -373,6 +443,7 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
|
||||
"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"],
|
||||
@@ -389,42 +460,55 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
|
||||
return comparison, nil
|
||||
}
|
||||
|
||||
// GetCompetitionData 获取竞赛数据(特定用户的所有交易员)
|
||||
func (tm *TraderManager) GetCompetitionData(userID string) (map[string]interface{}, error) {
|
||||
// GetCompetitionData 获取竞赛数据(全平台所有交易员)
|
||||
func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
comparison := make(map[string]interface{})
|
||||
traders := make([]map[string]interface{}, 0)
|
||||
|
||||
// 只获取该用户的交易员
|
||||
for traderID, t := range tm.traders {
|
||||
// 检查trader是否属于该用户(通过ID前缀判断)
|
||||
// 格式:userID_traderName
|
||||
if !isUserTrader(traderID, userID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取全平台所有交易员
|
||||
for _, t := range tm.traders {
|
||||
account, err := t.GetAccountInfo()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", traderID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
status := t.GetStatus()
|
||||
traders = append(traders, map[string]interface{}{
|
||||
"trader_id": t.GetID(),
|
||||
"trader_name": t.GetName(),
|
||||
"ai_model": t.GetAIModel(),
|
||||
"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"],
|
||||
})
|
||||
|
||||
var traderData map[string]interface{}
|
||||
|
||||
if err != nil {
|
||||
// 如果获取账户信息失败,使用默认值但仍然显示交易员
|
||||
log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", t.GetID(), err)
|
||||
traderData = map[string]interface{}{
|
||||
"trader_id": t.GetID(),
|
||||
"trader_name": t.GetName(),
|
||||
"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["count"] = len(traders)
|
||||
|
||||
@@ -474,13 +558,21 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
||||
|
||||
log.Printf("📋 为用户 %s 加载交易员配置: %d 个", userID, len(traders))
|
||||
|
||||
// 获取系统配置
|
||||
coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url")
|
||||
// 获取系统配置(不包含信号源,信号源现在为用户级别)
|
||||
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
|
||||
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
|
||||
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
|
||||
btcEthLeverageStr, _ := database.GetSystemConfig("btc_eth_leverage")
|
||||
altcoinLeverageStr, _ := database.GetSystemConfig("altcoin_leverage")
|
||||
defaultCoinsStr, _ := database.GetSystemConfig("default_coins")
|
||||
|
||||
// 获取用户信号源配置
|
||||
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 // 默认值
|
||||
@@ -498,14 +590,13 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
||||
stopTradingMinutes = val
|
||||
}
|
||||
|
||||
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
|
||||
// 解析默认币种列表
|
||||
var defaultCoins []string
|
||||
if defaultCoinsStr != "" {
|
||||
if err := json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins); err != nil {
|
||||
log.Printf("⚠️ 解析默认币种配置失败: %v,使用空列表", err)
|
||||
defaultCoins = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个交易员获取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 {
|
||||
log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err)
|
||||
}
|
||||
@@ -577,7 +668,32 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
||||
}
|
||||
|
||||
// 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
|
||||
traderConfig := trader.AutoTraderConfig{
|
||||
ID: traderCfg.ID,
|
||||
@@ -585,14 +701,16 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
|
||||
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
||||
Exchange: exchangeCfg.ID, // 使用exchange ID
|
||||
InitialBalance: traderCfg.InitialBalance,
|
||||
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||
CoinPoolAPIURL: coinPoolURL,
|
||||
BTCETHLeverage: btcEthLeverage,
|
||||
AltcoinLeverage: altcoinLeverage,
|
||||
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||
MaxDailyLoss: maxDailyLoss,
|
||||
MaxDrawdown: maxDrawdown,
|
||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||
DefaultCoins: defaultCoins,
|
||||
TradingCoins: tradingCoins,
|
||||
}
|
||||
|
||||
// 根据交易所类型设置API密钥
|
||||
|
||||
@@ -66,6 +66,10 @@ type AutoTraderConfig struct {
|
||||
|
||||
// 仓位模式
|
||||
IsCrossMargin bool // true=全仓模式, false=逐仓模式
|
||||
|
||||
// 币种配置
|
||||
DefaultCoins []string // 默认币种列表(从数据库获取)
|
||||
TradingCoins []string // 实际交易币种列表
|
||||
}
|
||||
|
||||
// AutoTrader 自动交易器
|
||||
@@ -82,6 +86,8 @@ type AutoTrader struct {
|
||||
dailyPnL float64
|
||||
customPrompt string // 自定义交易策略prompt
|
||||
overrideBasePrompt bool // 是否覆盖基础prompt
|
||||
defaultCoins []string // 默认币种列表(从数据库获取)
|
||||
tradingCoins []string // 实际交易币种列表
|
||||
lastResetTime time.Time
|
||||
stopUntil time.Time
|
||||
isRunning bool
|
||||
@@ -184,6 +190,8 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
mcpClient: mcpClient,
|
||||
decisionLogger: decisionLogger,
|
||||
initialBalance: config.InitialBalance,
|
||||
defaultCoins: config.DefaultCoins,
|
||||
tradingCoins: config.TradingCoins,
|
||||
lastResetTime: time.Now(),
|
||||
startTime: time.Now(),
|
||||
callCount: 0,
|
||||
@@ -486,30 +494,12 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 获取合并的候选币种池(AI500 + OI Top,去重)
|
||||
// 无论有没有持仓,都分析相同数量的币种(让AI看到所有好机会)
|
||||
// AI会根据保证金使用率和现有持仓情况,自己决定是否要换仓
|
||||
const ai500Limit = 20 // AI500取前20个评分最高的币种
|
||||
|
||||
// 获取合并后的币种池(AI500 + OI Top)
|
||||
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
|
||||
// 3. 获取交易员的候选币种池
|
||||
candidateCoins, err := at.getCandidateCoins()
|
||||
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. 计算总盈亏
|
||||
totalPnL := totalEquity - at.initialBalance
|
||||
totalPnLPct := 0.0
|
||||
@@ -759,6 +749,11 @@ func (at *AutoTrader) GetAIModel() string {
|
||||
return at.aiModel
|
||||
}
|
||||
|
||||
// GetExchange 获取交易所
|
||||
func (at *AutoTrader) GetExchange() string {
|
||||
return at.exchange
|
||||
}
|
||||
|
||||
// SetCustomPrompt 设置自定义交易策略prompt
|
||||
func (at *AutoTrader) SetCustomPrompt(prompt string) {
|
||||
at.customPrompt = prompt
|
||||
@@ -968,3 +963,74 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision
|
||||
|
||||
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">
|
||||
<head>
|
||||
<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" />
|
||||
<title>NOFX - AI Auto Trading Dashboard</title>
|
||||
</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 { t, type Language } from './i18n/translations';
|
||||
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 {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
@@ -176,7 +175,10 @@ function App() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,7 +201,9 @@ function App() {
|
||||
<div className="relative flex items-center">
|
||||
{/* Left - Logo and Title */}
|
||||
<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>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('appTitle', language)}
|
||||
@@ -220,7 +224,7 @@ function App() {
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
竞赛
|
||||
{t('aiCompetition', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('traders')}
|
||||
@@ -260,8 +264,7 @@ function App() {
|
||||
{/* Admin Mode Indicator */}
|
||||
{systemConfig?.admin_mode && (
|
||||
<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' }}>管理员模式</span>
|
||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>⚡ {t('adminMode', language)}</span>
|
||||
</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="flex items-start justify-between mb-3">
|
||||
<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%)' }}>
|
||||
<Bot className="w-6 h-6" style={{ color: '#000' }} />
|
||||
</div>
|
||||
<span className="w-10 h-10 rounded-full flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||
🤖
|
||||
</span>
|
||||
{selectedTrader.trader_name}
|
||||
</h2>
|
||||
|
||||
{/* Trader Selector */}
|
||||
{traders && traders.length > 0 && (
|
||||
<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
|
||||
value={selectedTraderId}
|
||||
onChange={(e) => onTraderSelect(e.target.value)}
|
||||
@@ -467,9 +470,8 @@ function TraderDetailsPage({
|
||||
{/* Debug Info */}
|
||||
{account && (
|
||||
<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' }}>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} |
|
||||
<div style={{ color: '#848E9C' }}>
|
||||
🔄 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'}{' '}
|
||||
({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
|
||||
</div>
|
||||
@@ -515,8 +517,7 @@ function TraderDetailsPage({
|
||||
<div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<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>
|
||||
{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)' }}>
|
||||
@@ -580,9 +581,7 @@ function TraderDetailsPage({
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="mb-4 flex justify-center opacity-50">
|
||||
<BarChart3 className="w-16 h-16" />
|
||||
</div>
|
||||
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||
<div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
|
||||
<div className="text-sm">{t('noActivePositions', language)}</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="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%)',
|
||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)'
|
||||
}}>
|
||||
<Brain className="w-6 h-6" style={{ color: '#FFF' }} />
|
||||
🧠
|
||||
</div>
|
||||
<div>
|
||||
<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="mb-4 flex justify-center opacity-30">
|
||||
<Brain className="w-16 h-16" style={{ color: '#8B5CF6' }} />
|
||||
</div>
|
||||
<div className="text-6xl mb-4 opacity-30">🧠</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>
|
||||
@@ -660,11 +657,10 @@ function StatCard({
|
||||
{change !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className="text-sm mono font-bold flex items-center gap-1"
|
||||
className="text-sm mono font-bold"
|
||||
style={{ color: positive ? '#0ECB81' : '#F6465D' }}
|
||||
>
|
||||
{positive ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />}
|
||||
{positive ? '+' : ''}
|
||||
{positive ? '▲' : '▼'} {positive ? '+' : ''}
|
||||
{change.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -708,8 +704,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
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>
|
||||
</button>
|
||||
{showInputPrompt && (
|
||||
@@ -728,8 +723,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
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>
|
||||
</button>
|
||||
{showCoT && (
|
||||
@@ -759,11 +753,9 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
{action.price > 0 && (
|
||||
<span className="font-mono text-xs" style={{ color: '#848E9C' }}>@{action.price.toFixed(4)}</span>
|
||||
)}
|
||||
{action.success ? (
|
||||
<Check className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
) : (
|
||||
<X className="w-4 h-4" style={{ color: '#F6465D' }} />
|
||||
)}
|
||||
<span style={{ color: action.success ? '#0ECB81' : '#F6465D' }}>
|
||||
{action.success ? '✓' : '✗'}
|
||||
</span>
|
||||
{action.error && <span className="text-xs ml-2" style={{ color: '#F6465D' }}>{action.error}</span>}
|
||||
</div>
|
||||
))}
|
||||
@@ -797,9 +789,8 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
|
||||
{/* 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)' }}>
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{decision.error_message}
|
||||
<div className="text-sm rounded px-3 py-2 mt-3" style={{ color: '#F6465D', background: 'rgba(246, 70, 93, 0.1)' }}>
|
||||
❌ {decision.error_message}
|
||||
</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 type { CompetitionTraderData } from '../types';
|
||||
import { getTraderColor } from '../utils/traderColors';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
|
||||
interface ComparisonChartProps {
|
||||
traders: CompetitionTraderData[];
|
||||
}
|
||||
|
||||
export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
const { language } = useLanguage();
|
||||
// 获取所有trader的历史数据 - 使用单个useSWR并发请求所有trader数据
|
||||
// 生成唯一的key,当traders变化时会触发重新请求
|
||||
const tradersKey = traders.map(t => t.trader_id).sort().join(',');
|
||||
@@ -134,11 +136,9 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
if (combinedData.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="mb-4 flex justify-center opacity-50">
|
||||
<BarChart3 className="w-16 h-16" />
|
||||
</div>
|
||||
<div className="text-lg font-semibold mb-2">暂无历史数据</div>
|
||||
<div className="text-sm">运行几个周期后将显示对比曲线</div>
|
||||
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||
<div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
|
||||
<div className="text-sm">{t('dataWillAppear', language)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -314,25 +314,25 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
{/* Stats */}
|
||||
<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="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>
|
||||
<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-base font-bold mono" style={{ color: '#EAECEF' }}>{combinedData.length} 个</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' }}>{t('count', language, {count: combinedData.length})}</div>
|
||||
</div>
|
||||
<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' }}>
|
||||
{currentGap.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
<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' }}>
|
||||
{combinedData.length > MAX_DISPLAY_POINTS
|
||||
? `最近 ${MAX_DISPLAY_POINTS}`
|
||||
: '全部数据'}
|
||||
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
||||
: t('allData', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { api } from '../lib/api';
|
||||
import type { CompetitionData } from '../types';
|
||||
import { ComparisonChart } from './ComparisonChart';
|
||||
import { TraderConfigModal } from './TraderConfigModal';
|
||||
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() {
|
||||
const { language } = useLanguage();
|
||||
const [selectedTrader, setSelectedTrader] = useState<any>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { data: competition } = useSWR<CompetitionData>(
|
||||
'competition',
|
||||
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) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -52,26 +74,26 @@ export function CompetitionPage() {
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<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%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
||||
}}>
|
||||
<Trophy className="w-7 h-7" style={{ color: '#000' }} />
|
||||
🏆
|
||||
</div>
|
||||
<div>
|
||||
<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' }}>
|
||||
{competition.count} 交易员
|
||||
{competition.count} {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
实时对战
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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-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'}%
|
||||
@@ -85,10 +107,10 @@ export function CompetitionPage() {
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
表现对比
|
||||
{t('performanceComparison', language)}
|
||||
</h2>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
实时收益率
|
||||
{t('realTimePnL', language)}
|
||||
</div>
|
||||
</div>
|
||||
<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="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
排行榜
|
||||
{t('leaderboard', language)}
|
||||
</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)' }}>
|
||||
实时
|
||||
{t('live', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -112,7 +134,8 @@ export function CompetitionPage() {
|
||||
return (
|
||||
<div
|
||||
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={{
|
||||
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'}`,
|
||||
@@ -122,17 +145,13 @@ export function CompetitionPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Rank & Name */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={{
|
||||
background: index === 0 ? 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)' :
|
||||
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 className="text-2xl w-6">
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
|
||||
<div className="text-xs mono font-semibold" style={{ color: traderColor }}>
|
||||
{trader.ai_model.toUpperCase()}
|
||||
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +160,7 @@ export function CompetitionPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Total Equity */}
|
||||
<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' }}>
|
||||
{trader.total_equity?.toFixed(2) || '0.00'}
|
||||
</div>
|
||||
@@ -149,7 +168,7 @@ export function CompetitionPage() {
|
||||
|
||||
{/* P&L */}
|
||||
<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
|
||||
className="text-lg font-bold mono"
|
||||
style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||
@@ -164,7 +183,7 @@ export function CompetitionPage() {
|
||||
|
||||
{/* Positions */}
|
||||
<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' }}>
|
||||
{trader.position_count}
|
||||
</div>
|
||||
@@ -176,17 +195,13 @@ export function CompetitionPage() {
|
||||
{/* Status */}
|
||||
<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
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
}
|
||||
>
|
||||
{trader.is_running ? (
|
||||
<CircleDot className="w-3 h-3" />
|
||||
) : (
|
||||
<Circle className="w-3 h-3" />
|
||||
)}
|
||||
{trader.is_running ? '●' : '○'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,7 +217,7 @@ export function CompetitionPage() {
|
||||
{competition.traders.length === 2 && (
|
||||
<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' }}>
|
||||
正面对决
|
||||
{t('headToHead', language)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
@@ -229,22 +244,25 @@ export function CompetitionPage() {
|
||||
>
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-base font-bold mb-2"
|
||||
className="text-base font-bold mb-1"
|
||||
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</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' }}>
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
{isWinning && gap > 0 && (
|
||||
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
||||
领先 {gap.toFixed(2)}%
|
||||
{t('leadingBy', language, { gap: gap.toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
{!isWinning && gap < 0 && (
|
||||
<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>
|
||||
@@ -254,6 +272,13 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trader Config Modal */}
|
||||
<TraderConfigModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
traderData={selectedTrader}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,8 +14,8 @@ export function Header({ simple = false }: HeaderProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
|
||||
<div className="flex items-center justify-center">
|
||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="h-10 w-auto" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { Header } from './Header';
|
||||
import { Smartphone } from 'lucide-react';
|
||||
|
||||
export function LoginPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -59,7 +58,7 @@ export function LoginPage() {
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('loginTitle', language)}
|
||||
@@ -121,9 +120,7 @@ export function LoginPage() {
|
||||
) : (
|
||||
<form onSubmit={handleOTPVerify} className="space-y-4">
|
||||
<div className="text-center mb-4">
|
||||
<div className="mb-2 flex justify-center">
|
||||
<Smartphone className="w-10 h-10" style={{ color: '#F0B90B' }} />
|
||||
</div>
|
||||
<div className="text-4xl mb-2">📱</div>
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('scanQRCodeInstructions', language)}<br />
|
||||
{t('enterOTPCode', language)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { Smartphone, Lock } from 'lucide-react';
|
||||
|
||||
export function RegisterPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -77,7 +76,7 @@ export function RegisterPage() {
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('appTitle', language)}
|
||||
@@ -158,9 +157,7 @@ export function RegisterPage() {
|
||||
{step === 'setup-otp' && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 flex justify-center">
|
||||
<Smartphone className="w-10 h-10" style={{ color: '#F0B90B' }} />
|
||||
</div>
|
||||
<div className="text-4xl mb-2">📱</div>
|
||||
<h3 className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('setupTwoFactor', language)}
|
||||
</h3>
|
||||
@@ -238,9 +235,7 @@ export function RegisterPage() {
|
||||
{step === 'verify-otp' && (
|
||||
<form onSubmit={handleOTPVerify} className="space-y-4">
|
||||
<div className="text-center mb-4">
|
||||
<div className="mb-2 flex justify-center">
|
||||
<Lock className="w-10 h-10" style={{ color: '#F0B90B' }} />
|
||||
</div>
|
||||
<div className="text-4xl mb-2">🔐</div>
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('enterOTPCode', language)}<br />
|
||||
{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 = {
|
||||
en: {
|
||||
// Header
|
||||
appTitle: 'AI Trading System',
|
||||
appTitle: 'NOFX',
|
||||
subtitle: 'Multi-AI Model Trading Platform',
|
||||
aiTraders: 'AI Traders',
|
||||
details: 'Details',
|
||||
@@ -64,11 +64,17 @@ export const translations = {
|
||||
recent: 'Recent',
|
||||
allData: 'All Data',
|
||||
cycles: 'Cycles',
|
||||
|
||||
// Comparison Chart
|
||||
comparisonMode: 'Comparison Mode',
|
||||
dataPoints: 'Data Points',
|
||||
currentGap: 'Current Gap',
|
||||
count: '{count} pts',
|
||||
|
||||
// Competition Page
|
||||
aiCompetition: 'AI Competition',
|
||||
traders: 'traders',
|
||||
liveBattle: 'Qwen vs DeepSeek · Live Battle',
|
||||
liveBattle: 'Live Battle',
|
||||
leader: 'Leader',
|
||||
leaderboard: 'Leaderboard',
|
||||
live: 'LIVE',
|
||||
@@ -147,6 +153,38 @@ export const translations = {
|
||||
useTestnet: 'Use Testnet',
|
||||
enabled: 'Enabled',
|
||||
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: 'Loading...',
|
||||
@@ -207,7 +245,7 @@ export const translations = {
|
||||
},
|
||||
zh: {
|
||||
// Header
|
||||
appTitle: 'AI交易系统',
|
||||
appTitle: 'NOFX',
|
||||
subtitle: '多AI模型交易平台',
|
||||
aiTraders: 'AI交易员',
|
||||
details: '详情',
|
||||
@@ -268,22 +306,28 @@ export const translations = {
|
||||
recent: '最近',
|
||||
allData: '全部数据',
|
||||
cycles: '个',
|
||||
|
||||
// Comparison Chart
|
||||
comparisonMode: '对比模式',
|
||||
dataPoints: '数据点数',
|
||||
currentGap: '当前差距',
|
||||
count: '{count} 个',
|
||||
|
||||
// Competition Page
|
||||
aiCompetition: 'AI竞赛',
|
||||
traders: '位交易者',
|
||||
liveBattle: 'Qwen vs DeepSeek · 实时对战',
|
||||
leader: '🥇 领先者',
|
||||
leaderboard: '🥇 排行榜',
|
||||
live: '直播',
|
||||
performanceComparison: '📈 表现对比',
|
||||
realTimePnL: '实时盈亏百分比',
|
||||
headToHead: '⚔️ 正面对决',
|
||||
traders: '交易员',
|
||||
liveBattle: '实时对战',
|
||||
leader: '领先者',
|
||||
leaderboard: '排行榜',
|
||||
live: '实时',
|
||||
performanceComparison: '表现对比',
|
||||
realTimePnL: '实时收益率',
|
||||
headToHead: '正面对决',
|
||||
leadingBy: '领先 {gap}%',
|
||||
behindBy: '落后 {gap}%',
|
||||
equity: '净值',
|
||||
pnl: '盈亏',
|
||||
pos: '仓位',
|
||||
equity: '权益',
|
||||
pnl: '收益',
|
||||
pos: '持仓',
|
||||
|
||||
// AI Learning
|
||||
aiLearning: 'AI学习与反思',
|
||||
@@ -351,6 +395,38 @@ export const translations = {
|
||||
useTestnet: '使用测试网',
|
||||
enabled: '启用',
|
||||
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: '加载中...',
|
||||
|
||||
@@ -82,6 +82,24 @@ export const api = {
|
||||
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模型配置接口
|
||||
async getModelConfigs(): Promise<AIModel[]> {
|
||||
const res = await fetch(`${API_BASE}/models`, {
|
||||
@@ -242,4 +260,25 @@ export const api = {
|
||||
if (!res.ok) throw new Error('获取竞赛数据失败');
|
||||
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;
|
||||
enabled: boolean;
|
||||
apiKey?: string;
|
||||
customApiUrl?: string;
|
||||
}
|
||||
|
||||
export interface Exchange {
|
||||
@@ -123,9 +124,14 @@ export interface CreateTraderRequest {
|
||||
ai_model_id: string;
|
||||
exchange_id: string;
|
||||
initial_balance: number;
|
||||
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;
|
||||
}
|
||||
|
||||
export interface UpdateModelConfigRequest {
|
||||
@@ -133,6 +139,7 @@ export interface UpdateModelConfigRequest {
|
||||
[key: string]: {
|
||||
enabled: boolean;
|
||||
api_key: string;
|
||||
custom_api_url?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -159,6 +166,7 @@ export interface CompetitionTraderData {
|
||||
trader_id: string;
|
||||
trader_name: string;
|
||||
ai_model: string;
|
||||
exchange: string;
|
||||
total_equity: number;
|
||||
total_pnl: number;
|
||||
total_pnl_pct: number;
|
||||
|
||||
Reference in New Issue
Block a user