diff --git a/api/debate.go b/api/debate.go index 3eaedd5e..88da5df8 100644 --- a/api/debate.go +++ b/api/debate.go @@ -66,6 +66,10 @@ type CreateDebateRequest struct { AutoExecute bool `json:"auto_execute"` TraderID string `json:"trader_id"` Participants []ParticipantConfig `json:"participants" binding:"required,min=2"` + // OI Ranking data options + EnableOIRanking bool `json:"enable_oi_ranking"` // Whether to include OI ranking data + OIRankingLimit int `json:"oi_ranking_limit"` // Number of OI ranking entries (default 10) + OIDuration string `json:"oi_duration"` // Duration for OI data (1h, 4h, 24h, etc.) } // ParticipantConfig represents a participant configuration @@ -215,6 +219,9 @@ func (h *DebateHandler) HandleCreateDebate(c *gin.Context) { PromptVariant: req.PromptVariant, AutoExecute: req.AutoExecute, TraderID: req.TraderID, + EnableOIRanking: req.EnableOIRanking, + OIRankingLimit: req.OIRankingLimit, + OIDuration: req.OIDuration, } if err := h.debateStore.CreateSession(session); err != nil { diff --git a/api/server.go b/api/server.go index 4c37b1c0..ee24e6f7 100644 --- a/api/server.go +++ b/api/server.go @@ -792,19 +792,24 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { } // Update database + logger.Infof("🔄 Updating trader: ID=%s, Name=%s, AIModelID=%s, StrategyID=%s, req.StrategyID=%s", + traderRecord.ID, traderRecord.Name, traderRecord.AIModelID, traderRecord.StrategyID, req.StrategyID) err = s.store.Trader().Update(traderRecord) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update trader: %v", err)}) return } - // Reload traders into memory + // Remove old trader from memory first to ensure fresh config is loaded + s.traderManager.RemoveTrader(traderID) + + // Reload traders into memory with fresh config err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { logger.Infof("⚠️ Failed to reload user traders into memory: %v", err) } - logger.Infof("✓ Trader updated successfully: %s (model: %s, exchange: %s)", req.Name, req.AIModelID, req.ExchangeID) + logger.Infof("✓ Trader updated successfully: %s (model: %s, exchange: %s, strategy: %s)", req.Name, req.AIModelID, req.ExchangeID, strategyID) c.JSON(http.StatusOK, gin.H{ "trader_id": traderID, @@ -854,54 +859,57 @@ func (s *Server) handleStartTrader(c *gin.Context) { return } - trader, err := s.traderManager.GetTrader(traderID) - if err != nil { - // Trader not in memory, try loading from database - logger.Infof("🔄 Trader %s not in memory, trying to load...", traderID) - if loadErr := s.traderManager.LoadUserTradersFromStore(s.store, userID); loadErr != nil { - logger.Infof("❌ Failed to load user traders: %v", loadErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load trader: " + loadErr.Error()}) - return - } - // Try to get trader again - trader, err = s.traderManager.GetTrader(traderID) - if err != nil { - // Check detailed reason - fullCfg, _ := s.store.Trader().GetFullConfig(userID, traderID) - if fullCfg != nil && fullCfg.Trader != nil { - // Check strategy - if fullCfg.Strategy == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Trader has no strategy configured, please create a strategy in Strategy Studio and associate it with the trader"}) - return - } - // Check AI model - if fullCfg.AIModel == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's AI model does not exist, please check AI model configuration"}) - return - } - if !fullCfg.AIModel.Enabled { - c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's AI model is not enabled, please enable the AI model first"}) - return - } - // Check exchange - if fullCfg.Exchange == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's exchange does not exist, please check exchange configuration"}) - return - } - if !fullCfg.Exchange.Enabled { - c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's exchange is not enabled, please enable the exchange first"}) - return - } - } - c.JSON(http.StatusNotFound, gin.H{"error": "Failed to load trader, please check AI model, exchange and strategy configuration"}) + // Check if trader exists in memory and if it's running + existingTrader, _ := s.traderManager.GetTrader(traderID) + if existingTrader != nil { + status := existingTrader.GetStatus() + if isRunning, ok := status["is_running"].(bool); ok && isRunning { + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader is already running"}) return } + // Trader exists but is stopped - remove from memory to reload fresh config + logger.Infof("🔄 Removing stopped trader %s from memory to reload config...", traderID) + s.traderManager.RemoveTrader(traderID) } - // Check if trader is already running - status := trader.GetStatus() - if isRunning, ok := status["is_running"].(bool); ok && isRunning { - c.JSON(http.StatusBadRequest, gin.H{"error": "Trader is already running"}) + // Load trader from database (always reload to get latest config) + logger.Infof("🔄 Loading trader %s from database...", traderID) + if loadErr := s.traderManager.LoadUserTradersFromStore(s.store, userID); loadErr != nil { + logger.Infof("❌ Failed to load user traders: %v", loadErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load trader: " + loadErr.Error()}) + return + } + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + // Check detailed reason + fullCfg, _ := s.store.Trader().GetFullConfig(userID, traderID) + if fullCfg != nil && fullCfg.Trader != nil { + // Check strategy + if fullCfg.Strategy == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader has no strategy configured, please create a strategy in Strategy Studio and associate it with the trader"}) + return + } + // Check AI model + if fullCfg.AIModel == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's AI model does not exist, please check AI model configuration"}) + return + } + if !fullCfg.AIModel.Enabled { + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's AI model is not enabled, please enable the AI model first"}) + return + } + // Check exchange + if fullCfg.Exchange == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's exchange does not exist, please check exchange configuration"}) + return + } + if !fullCfg.Exchange.Enabled { + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's exchange is not enabled, please enable the exchange first"}) + return + } + } + c.JSON(http.StatusNotFound, gin.H{"error": "Failed to load trader, please check AI model, exchange and strategy configuration"}) return } @@ -1730,6 +1738,7 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { "trader_name": traderConfig.Name, "ai_model": aiModelID, "exchange_id": traderConfig.ExchangeID, + "strategy_id": traderConfig.StrategyID, "initial_balance": traderConfig.InitialBalance, "scan_interval_minutes": traderConfig.ScanIntervalMinutes, "btc_eth_leverage": traderConfig.BTCETHLeverage, diff --git a/api/strategy.go b/api/strategy.go index 3bd97f9b..e9a2e2de 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -449,6 +449,16 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { marketDataMap[coin.Symbol] = data } + // Fetch quantitative data for each candidate coin + symbols := make([]string, 0, len(candidates)) + for _, c := range candidates { + symbols = append(symbols, c.Symbol) + } + quantDataMap := engine.FetchQuantDataBatch(symbols) + + // Fetch OI ranking data (market-wide position changes) + oiRankingData := engine.FetchOIRankingData() + // Build real context (for generating User Prompt) testContext := &decision.Context{ CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), @@ -468,6 +478,8 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { CandidateCoins: candidates, PromptVariant: req.PromptVariant, MarketDataMap: marketDataMap, + QuantDataMap: quantDataMap, + OIRankingData: oiRankingData, } // Build System Prompt diff --git a/debate/engine.go b/debate/engine.go index a48b4bc6..48d50122 100644 --- a/debate/engine.go +++ b/debate/engine.go @@ -182,7 +182,7 @@ func (e *DebateEngine) runDebate(session *store.DebateSessionWithDetails, strate // Build system prompt based on strategy (same as AI Test) baseSystemPrompt := strategyEngine.BuildSystemPrompt(1000.0, session.PromptVariant) - // Build user prompt with market data + // Build user prompt with market data (OI ranking data is included via ctx.OIRankingData) userPrompt := strategyEngine.BuildUserPrompt(ctx) // Run debate rounds @@ -332,6 +332,9 @@ func (e *DebateEngine) buildMarketContext(session *store.DebateSessionWithDetail } quantDataMap := strategyEngine.FetchQuantDataBatch(symbols) + // Fetch OI ranking data (market-wide position changes) + oiRankingData := strategyEngine.FetchOIRankingData() + // Build context ctx := &decision.Context{ CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), @@ -352,6 +355,7 @@ func (e *DebateEngine) buildMarketContext(session *store.DebateSessionWithDetail PromptVariant: session.PromptVariant, MarketDataMap: marketDataMap, QuantDataMap: quantDataMap, + OIRankingData: oiRankingData, } return ctx, nil diff --git a/decision/engine.go b/decision/engine.go index fc50632e..a96db8aa 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -120,6 +120,7 @@ type Context struct { MultiTFMarket map[string]map[string]*market.Data `json:"-"` OITopDataMap map[string]*OITopData `json:"-"` QuantDataMap map[string]*QuantData `json:"-"` + OIRankingData *pool.OIRankingData `json:"-"` // Market-wide OI ranking data BTCETHLeverage int `json:"-"` AltcoinLeverage int `json:"-"` Timeframes []string `json:"-"` @@ -642,6 +643,53 @@ func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*Quant return result } +// FetchOIRankingData fetches market-wide OI ranking data +func (e *StrategyEngine) FetchOIRankingData() *pool.OIRankingData { + indicators := e.config.Indicators + if !indicators.EnableOIRanking { + return nil + } + + baseURL := indicators.OIRankingAPIURL + if baseURL == "" { + baseURL = "http://nofxaios.com:30006" + } + + // Get auth key from existing API URL or use default + authKey := "cm_568c67eae410d912c54c" + if indicators.QuantDataAPIURL != "" { + if idx := strings.Index(indicators.QuantDataAPIURL, "auth="); idx != -1 { + authKey = indicators.QuantDataAPIURL[idx+5:] + if ampIdx := strings.Index(authKey, "&"); ampIdx != -1 { + authKey = authKey[:ampIdx] + } + } + } + + duration := indicators.OIRankingDuration + if duration == "" { + duration = "1h" + } + + limit := indicators.OIRankingLimit + if limit <= 0 { + limit = 10 + } + + logger.Infof("📊 Fetching OI ranking data (duration: %s, limit: %d)", duration, limit) + + data, err := pool.GetOIRankingData(baseURL, authKey, duration, limit) + if err != nil { + logger.Warnf("⚠️ Failed to fetch OI ranking data: %v", err) + return nil + } + + logger.Infof("✓ OI ranking data ready: %d top, %d low positions", + len(data.TopPositions), len(data.LowPositions)) + + return data +} + // ============================================================================ // Prompt Building - System Prompt // ============================================================================ @@ -904,6 +952,11 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string { } sb.WriteString("\n") + // OI Ranking data (market-wide open interest changes) + if ctx.OIRankingData != nil { + sb.WriteString(pool.FormatOIRankingForAI(ctx.OIRankingData)) + } + sb.WriteString("---\n\n") sb.WriteString("Now please analyze and output your decision (Chain of Thought + JSON)\n") diff --git a/manager/trader_manager.go b/manager/trader_manager.go index f3377c00..d63d8b73 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -676,10 +676,14 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg } // Set API keys based on AI model - if aiModelCfg.Provider == "qwen" { + switch aiModelCfg.Provider { + case "qwen": traderConfig.QwenKey = aiModelCfg.APIKey - } else if aiModelCfg.Provider == "deepseek" { + case "deepseek": traderConfig.DeepSeekKey = aiModelCfg.APIKey + default: + // For other providers (grok, openai, claude, gemini, kimi, etc.), use CustomAPIKey + traderConfig.CustomAPIKey = aiModelCfg.APIKey } // Create trader instance diff --git a/pool/coin_pool.go b/pool/coin_pool.go index b436f96c..b519e824 100644 --- a/pool/coin_pool.go +++ b/pool/coin_pool.go @@ -590,6 +590,169 @@ type MergedCoinPool struct { SymbolSources map[string][]string // Source of each coin ("ai500"/"oi_top") } +// OIRankingData OI ranking data for debate (includes both top and low) +type OIRankingData struct { + TimeRange string `json:"time_range"` // e.g., "1小时" + Duration string `json:"duration"` // e.g., "1h" + TopPositions []OIPosition `json:"top_positions"` // 持仓增加排行 + LowPositions []OIPosition `json:"low_positions"` // 持仓减少排行 + FetchedAt time.Time `json:"fetched_at"` +} + +// GetOIRankingData retrieves OI ranking data (both top increase and low decrease) +// duration: "1h", "4h", "24h" etc. limit: number of results +func GetOIRankingData(baseURL, authKey string, duration string, limit int) (*OIRankingData, error) { + if baseURL == "" || authKey == "" { + return nil, fmt.Errorf("OI API URL or auth key not configured") + } + + if duration == "" { + duration = "1h" + } + if limit <= 0 { + limit = 20 + } + + result := &OIRankingData{ + Duration: duration, + FetchedAt: time.Now(), + } + + // Fetch top ranking (持仓增加) + topURL := fmt.Sprintf("%s/api/oi/top-ranking?limit=%d&duration=%s&auth=%s", baseURL, limit, duration, authKey) + topPositions, timeRange, err := fetchOIRanking(topURL) + if err != nil { + log.Printf("⚠️ Failed to fetch OI top ranking: %v", err) + } else { + result.TopPositions = topPositions + result.TimeRange = timeRange + } + + // Fetch low ranking (持仓减少) + lowURL := fmt.Sprintf("%s/api/oi/low-ranking?limit=%d&duration=%s&auth=%s", baseURL, limit, duration, authKey) + lowPositions, _, err := fetchOIRanking(lowURL) + if err != nil { + log.Printf("⚠️ Failed to fetch OI low ranking: %v", err) + } else { + result.LowPositions = lowPositions + } + + log.Printf("✓ Fetched OI ranking data: %d top, %d low (duration: %s)", + len(result.TopPositions), len(result.LowPositions), duration) + + return result, nil +} + +// fetchOIRanking fetches OI ranking from a single endpoint +func fetchOIRanking(url string) ([]OIPosition, string, error) { + client := &http.Client{Timeout: 30 * time.Second} + + resp, err := client.Get(url) + if err != nil { + return nil, "", fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, "", fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("API returned error (status %d): %s", resp.StatusCode, string(body)) + } + + var response OITopAPIResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, "", fmt.Errorf("JSON parsing failed: %w", err) + } + + if response.Code != 0 { + return nil, "", fmt.Errorf("API returned error code: %d", response.Code) + } + + return response.Data.Positions, response.Data.TimeRange, nil +} + +// FormatOIRankingForAI formats OI ranking data for AI consumption +func FormatOIRankingForAI(data *OIRankingData) string { + if data == nil { + return "" + } + + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("## 📊 市场持仓量变化数据 (Open Interest Changes in %s / %s)\n\n", data.TimeRange, data.Duration)) + + // Top rankings (持仓增加) + if len(data.TopPositions) > 0 { + sb.WriteString("### 🔺 持仓量增加排行 (OI Increase Ranking)\n") + sb.WriteString("市场资金正在流入以下币种,可能表示趋势延续或新仓位建立:\n\n") + sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 | 多头 | 空头 |\n") + sb.WriteString("|------|------|------------------|----------|----------|------|------|\n") + for _, pos := range data.TopPositions { + sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% | %.0f | %.0f |\n", + pos.Rank, + pos.Symbol, + formatOIValue(pos.OIDeltaValue), + pos.OIDeltaPercent, + pos.PriceDeltaPercent, + pos.NetLong, + pos.NetShort, + )) + } + sb.WriteString("\n") + + // Market interpretation + sb.WriteString("**解读**: 持仓增加 + 价格上涨 = 多头主导; 持仓增加 + 价格下跌 = 空头主导\n\n") + } + + // Low rankings (持仓减少) + if len(data.LowPositions) > 0 { + sb.WriteString("### 🔻 持仓量减少排行 (OI Decrease Ranking)\n") + sb.WriteString("市场资金正在流出以下币种,可能表示趋势反转或仓位平仓:\n\n") + sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 | 多头 | 空头 |\n") + sb.WriteString("|------|------|------------------|----------|----------|------|------|\n") + for _, pos := range data.LowPositions { + sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% | %.0f | %.0f |\n", + pos.Rank, + pos.Symbol, + formatOIValue(pos.OIDeltaValue), + pos.OIDeltaPercent, + pos.PriceDeltaPercent, + pos.NetLong, + pos.NetShort, + )) + } + sb.WriteString("\n") + + // Market interpretation + sb.WriteString("**解读**: 持仓减少 + 价格上涨 = 空头平仓(反弹); 持仓减少 + 价格下跌 = 多头平仓(回调)\n\n") + } + + return sb.String() +} + +// formatOIValue formats OI value for display +func formatOIValue(v float64) string { + sign := "" + if v >= 0 { + sign = "+" + } + absV := v + if absV < 0 { + absV = -absV + } + if absV >= 1e9 { + return fmt.Sprintf("%s%.2fB", sign, v/1e9) + } else if absV >= 1e6 { + return fmt.Sprintf("%s%.2fM", sign, v/1e6) + } else if absV >= 1e3 { + return fmt.Sprintf("%s%.2fK", sign, v/1e3) + } + return fmt.Sprintf("%s%.2f", sign, v) +} + // GetMergedCoinPool retrieves merged coin pool (AI500 + OI Top, deduplicated) func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) { // 1. Get AI500 data diff --git a/store/debate.go b/store/debate.go index 7914310e..3f57d2fb 100644 --- a/store/debate.go +++ b/store/debate.go @@ -65,6 +65,10 @@ type DebateSession struct { FinalDecisions []*DebateDecision `json:"final_decisions,omitempty"` // Multi-coin decisions AutoExecute bool `json:"auto_execute"` TraderID string `json:"trader_id,omitempty"` // Trader to use for auto-execute + // OI Ranking data options + EnableOIRanking bool `json:"enable_oi_ranking"` // Whether to include OI ranking data + OIRankingLimit int `json:"oi_ranking_limit"` // Number of OI ranking entries (default 10) + OIDuration string `json:"oi_duration"` // Duration for OI data (1h, 4h, 24h, etc.) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -239,6 +243,9 @@ func (s *DebateStore) InitSchema() error { `ALTER TABLE debate_sessions ADD COLUMN interval_minutes INTEGER DEFAULT 5`, `ALTER TABLE debate_sessions ADD COLUMN prompt_variant TEXT DEFAULT 'balanced'`, `ALTER TABLE debate_sessions ADD COLUMN trader_id TEXT`, + `ALTER TABLE debate_sessions ADD COLUMN enable_oi_ranking BOOLEAN DEFAULT 0`, + `ALTER TABLE debate_sessions ADD COLUMN oi_ranking_limit INTEGER DEFAULT 10`, + `ALTER TABLE debate_sessions ADD COLUMN oi_duration TEXT DEFAULT '1h'`, `ALTER TABLE debate_votes ADD COLUMN leverage INTEGER DEFAULT 5`, `ALTER TABLE debate_votes ADD COLUMN position_pct REAL DEFAULT 0.2`, `ALTER TABLE debate_votes ADD COLUMN stop_loss_pct REAL DEFAULT 0.03`, @@ -266,15 +273,22 @@ func (s *DebateStore) CreateSession(session *DebateSession) error { if session.PromptVariant == "" { session.PromptVariant = "balanced" } + if session.OIRankingLimit == 0 { + session.OIRankingLimit = 10 + } + if session.OIDuration == "" { + session.OIDuration = "1h" + } session.CreatedAt = time.Now() session.UpdatedAt = time.Now() _, err := s.db.Exec(` - INSERT INTO debate_sessions (id, user_id, name, strategy_id, status, symbol, max_rounds, current_round, interval_minutes, prompt_variant, auto_execute, trader_id, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + INSERT INTO debate_sessions (id, user_id, name, strategy_id, status, symbol, max_rounds, current_round, interval_minutes, prompt_variant, auto_execute, trader_id, enable_oi_ranking, oi_ranking_limit, oi_duration, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, session.ID, session.UserID, session.Name, session.StrategyID, session.Status, session.Symbol, session.MaxRounds, session.CurrentRound, session.IntervalMinutes, session.PromptVariant, - session.AutoExecute, session.TraderID, session.CreatedAt, session.UpdatedAt, + session.AutoExecute, session.TraderID, session.EnableOIRanking, session.OIRankingLimit, session.OIDuration, + session.CreatedAt, session.UpdatedAt, ) return err } @@ -286,17 +300,22 @@ func (s *DebateStore) GetSession(id string) (*DebateSession, error) { var traderID sql.NullString var intervalMinutes sql.NullInt64 var promptVariant sql.NullString + var enableOIRanking sql.NullBool + var oiRankingLimit sql.NullInt64 + var oiDuration sql.NullString // Try new schema first err := s.db.QueryRow(` SELECT id, user_id, name, strategy_id, status, symbol, max_rounds, current_round, - interval_minutes, prompt_variant, final_decision, auto_execute, trader_id, created_at, updated_at + interval_minutes, prompt_variant, final_decision, auto_execute, trader_id, + enable_oi_ranking, oi_ranking_limit, oi_duration, created_at, updated_at FROM debate_sessions WHERE id = ?`, id, ).Scan( &session.ID, &session.UserID, &session.Name, &session.StrategyID, &session.Status, &session.Symbol, &session.MaxRounds, &session.CurrentRound, &intervalMinutes, &promptVariant, - &finalDecisionJSON, &session.AutoExecute, &traderID, &session.CreatedAt, &session.UpdatedAt, + &finalDecisionJSON, &session.AutoExecute, &traderID, + &enableOIRanking, &oiRankingLimit, &oiDuration, &session.CreatedAt, &session.UpdatedAt, ) // Fallback to basic schema if new columns don't exist @@ -316,6 +335,8 @@ func (s *DebateStore) GetSession(id string) (*DebateSession, error) { // Set defaults for new fields session.IntervalMinutes = 5 session.PromptVariant = "balanced" + session.OIRankingLimit = 10 + session.OIDuration = "1h" } else { // Set defaults for nullable fields session.IntervalMinutes = 5 @@ -329,6 +350,17 @@ func (s *DebateStore) GetSession(id string) (*DebateSession, error) { if traderID.Valid { session.TraderID = traderID.String } + if enableOIRanking.Valid { + session.EnableOIRanking = enableOIRanking.Bool + } + session.OIRankingLimit = 10 + if oiRankingLimit.Valid { + session.OIRankingLimit = int(oiRankingLimit.Int64) + } + session.OIDuration = "1h" + if oiDuration.Valid { + session.OIDuration = oiDuration.String + } } if finalDecisionJSON.Valid && finalDecisionJSON.String != "" { diff --git a/store/strategy.go b/store/strategy.go index 551a0299..a016e2b2 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -98,6 +98,11 @@ type IndicatorConfig struct { QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address EnableQuantOI bool `json:"enable_quant_oi"` // whether to show OI data EnableQuantNetflow bool `json:"enable_quant_netflow"` // whether to show Netflow data + // OI ranking data (market-wide open interest increase/decrease rankings) + EnableOIRanking bool `json:"enable_oi_ranking"` // whether to enable OI ranking data + OIRankingAPIURL string `json:"oi_ranking_api_url,omitempty"` // OI ranking API base URL + OIRankingDuration string `json:"oi_ranking_duration,omitempty"` // duration: 1h, 4h, 24h + OIRankingLimit int `json:"oi_ranking_limit,omitempty"` // number of entries (default 10) } // KlineConfig K-line configuration @@ -246,6 +251,11 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig { QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c", EnableQuantOI: true, EnableQuantNetflow: true, + // OI ranking data - market-wide OI increase/decrease rankings + EnableOIRanking: true, + OIRankingAPIURL: "http://nofxaios.com:30006", + OIRankingDuration: "1h", + OIRankingLimit: 10, }, RiskControl: RiskControlConfig{ MaxPositions: 3, // Max 3 coins simultaneously (CODE ENFORCED) diff --git a/store/trader.go b/store/trader.go index 436be826..de72dc13 100644 --- a/store/trader.go +++ b/store/trader.go @@ -2,6 +2,7 @@ package store import ( "database/sql" + "fmt" "strings" "time" ) @@ -262,14 +263,25 @@ func (s *TraderStore) UpdateShowInCompetition(userID, id string, showInCompetiti // Update updates trader configuration func (s *TraderStore) Update(trader *Trader) error { + fmt.Printf("📝 TraderStore.Update: ID=%s, Name=%s, AIModelID=%s, StrategyID=%s\n", + trader.ID, trader.Name, trader.AIModelID, trader.StrategyID) _, err := s.db.Exec(` UPDATE traders SET - name = ?, ai_model_id = ?, exchange_id = ?, strategy_id = ?, - scan_interval_minutes = ?, is_cross_margin = ?, show_in_competition = ?, + name = ?, + ai_model_id = ?, + exchange_id = ?, + strategy_id = ?, + initial_balance = CASE WHEN ? > 0 THEN ? ELSE initial_balance END, + scan_interval_minutes = CASE WHEN ? > 0 THEN ? ELSE scan_interval_minutes END, + is_cross_margin = ?, + show_in_competition = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, trader.Name, trader.AIModelID, trader.ExchangeID, trader.StrategyID, - trader.ScanIntervalMinutes, trader.IsCrossMargin, trader.ShowInCompetition, trader.ID, trader.UserID) + trader.InitialBalance, trader.InitialBalance, + trader.ScanIntervalMinutes, trader.ScanIntervalMinutes, + trader.IsCrossMargin, trader.ShowInCompetition, + trader.ID, trader.UserID) return err } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 46505f35..9128a7ab 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -773,6 +773,16 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { logger.Infof("📊 [%s] Successfully fetched quantitative data for %d symbols", at.name, len(ctx.QuantDataMap)) } + // 9. Get OI ranking data (market-wide position changes) + if strategyConfig.Indicators.EnableOIRanking { + logger.Infof("📊 [%s] Fetching OI ranking data...", at.name) + ctx.OIRankingData = at.strategyEngine.FetchOIRankingData() + if ctx.OIRankingData != nil { + logger.Infof("📊 [%s] OI ranking data ready: %d top, %d low positions", + at.name, len(ctx.OIRankingData.TopPositions), len(ctx.OIRankingData.LowPositions)) + } + } + return ctx, nil } diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 732093f8..89f98302 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -290,6 +290,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } const handleSaveEditTrader = async (data: CreateTraderRequest) => { + console.log('🔥🔥🔥 handleSaveEditTrader CALLED with data:', data) if (!editingTrader) return try { @@ -310,19 +311,17 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { name: data.name, ai_model_id: data.ai_model_id, exchange_id: data.exchange_id, + strategy_id: data.strategy_id, initial_balance: data.initial_balance, scan_interval_minutes: data.scan_interval_minutes, - btc_eth_leverage: data.btc_eth_leverage, - altcoin_leverage: data.altcoin_leverage, - trading_symbols: data.trading_symbols, - custom_prompt: data.custom_prompt, - override_base_prompt: data.override_base_prompt, - system_prompt_template: data.system_prompt_template, is_cross_margin: data.is_cross_margin, - use_coin_pool: data.use_coin_pool, - use_oi_top: data.use_oi_top, + show_in_competition: data.show_in_competition, } + console.log('🔥 handleSaveEditTrader - data:', data) + console.log('🔥 handleSaveEditTrader - data.strategy_id:', data.strategy_id) + console.log('🔥 handleSaveEditTrader - request:', request) + await toast.promise(api.updateTrader(editingTrader.trader_id, request), { loading: '正在保存…', success: '保存成功', diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 9904302a..25c191e2 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -163,7 +163,7 @@ export function TraderConfigModal({ name: formData.trader_name, ai_model_id: formData.ai_model, exchange_id: formData.exchange_id, - strategy_id: formData.strategy_id || undefined, + strategy_id: formData.strategy_id, is_cross_margin: formData.is_cross_margin, show_in_competition: formData.show_in_competition, scan_interval_minutes: formData.scan_interval_minutes, diff --git a/web/src/components/strategy/IndicatorEditor.tsx b/web/src/components/strategy/IndicatorEditor.tsx index 760ca47b..afa78ef4 100644 --- a/web/src/components/strategy/IndicatorEditor.tsx +++ b/web/src/components/strategy/IndicatorEditor.tsx @@ -1,8 +1,10 @@ -import { Clock, Activity, Database, TrendingUp, BarChart2, Info, Lock } from 'lucide-react' +import { Clock, Activity, Database, TrendingUp, BarChart2, Info, Lock, LineChart } from 'lucide-react' import type { IndicatorConfig } from '../../types' // Default API URL for quant data (must contain {symbol} placeholder) const DEFAULT_QUANT_DATA_API_URL = 'http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c' +// Default API base URL for OI ranking data +const DEFAULT_OI_RANKING_API_URL = 'http://nofxaios.com:30006' interface IndicatorEditorProps { config: IndicatorConfig @@ -82,6 +84,13 @@ export function IndicatorEditor({ fillDefault: { zh: '填入默认', en: 'Fill Default' }, symbolPlaceholder: { zh: '{symbol} 会被替换为币种', en: '{symbol} will be replaced with coin' }, + // OI Ranking + oiRanking: { zh: 'OI 排行数据', en: 'OI Ranking Data' }, + oiRankingDesc: { zh: '市场持仓量增减排行,反映资金流向', en: 'Market-wide OI changes, reflects capital flow' }, + oiRankingDuration: { zh: '时间周期', en: 'Duration' }, + oiRankingLimit: { zh: '排行数量', en: 'Top N' }, + oiRankingNote: { zh: '显示持仓量增加/减少的币种排行,帮助发现资金流向', en: 'Shows coins with OI increase/decrease, helps identify capital flow' }, + // Tips aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' }, } @@ -456,6 +465,82 @@ export function IndicatorEditor({ )} + + {/* Section 5: OI Ranking Data (Market-wide) */} +
+
+ + {t('oiRanking')} + - {t('oiRankingDesc')} +
+ +
+ {/* Enable Toggle */} +
+
+
+ {t('oiRanking')} +
+ !disabled && onChange({ + ...config, + enable_oi_ranking: e.target.checked, + // Set defaults when enabling + ...(e.target.checked && !config.oi_ranking_api_url ? { oi_ranking_api_url: DEFAULT_OI_RANKING_API_URL } : {}), + ...(e.target.checked && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}), + ...(e.target.checked && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}), + })} + disabled={disabled} + className="w-4 h-4 rounded accent-green-500" + /> +
+ + {/* Settings */} + {config.enable_oi_ranking && ( +
+
+ {/* Duration */} +
+ + +
+ {/* Limit */} +
+ + +
+
+

{t('oiRankingNote')}

+
+ )} +
+
) } diff --git a/web/src/hooks/useTraderActions.ts b/web/src/hooks/useTraderActions.ts deleted file mode 100644 index ad5493e7..00000000 --- a/web/src/hooks/useTraderActions.ts +++ /dev/null @@ -1,649 +0,0 @@ -import { api } from '../lib/api' -import type { - TraderInfo, - CreateTraderRequest, - TraderConfigData, - AIModel, - Exchange, -} from '../types' -import { t } from '../i18n/translations' -import { confirmToast } from '../lib/notify' -import { toast } from 'sonner' -import type { Language } from '../i18n/translations' - -interface UseTraderActionsParams { - traders: TraderInfo[] | undefined - allModels: AIModel[] - allExchanges: Exchange[] - supportedModels: AIModel[] - supportedExchanges: Exchange[] - language: Language - mutateTraders: () => Promise - setAllModels: (models: AIModel[]) => void - setAllExchanges: (exchanges: Exchange[]) => void - setShowCreateModal: (show: boolean) => void - setShowEditModal: (show: boolean) => void - setShowModelModal: (show: boolean) => void - setShowExchangeModal: (show: boolean) => void - setEditingModel: (modelId: string | null) => void - setEditingExchange: (exchangeId: string | null) => void - editingTrader: TraderConfigData | null - setEditingTrader: (trader: TraderConfigData | null) => void -} - -export function useTraderActions({ - traders, - allModels, - allExchanges, - supportedModels, - supportedExchanges, - language, - mutateTraders, - setAllModels, - setAllExchanges, - setShowCreateModal, - setShowEditModal, - setShowModelModal, - setShowExchangeModal, - setEditingModel, - setEditingExchange, - editingTrader, - setEditingTrader, -}: UseTraderActionsParams) { - // 检查模型是否正在被运行中的交易员使用(用于UI禁用) - const isModelInUse = (modelId: string) => { - return traders?.some((t) => t.ai_model === modelId && t.is_running) || false - } - - // 检查交易所是否正在被运行中的交易员使用(用于UI禁用) - const isExchangeInUse = (exchangeId: string) => { - return ( - traders?.some((t) => t.exchange_id === exchangeId && t.is_running) || - false - ) - } - - // 检查模型是否被任何交易员使用(包括停止状态的) - const isModelUsedByAnyTrader = (modelId: string) => { - return traders?.some((t) => t.ai_model === modelId) || false - } - - // 检查交易所是否被任何交易员使用(包括停止状态的) - const isExchangeUsedByAnyTrader = (exchangeId: string) => { - return traders?.some((t) => t.exchange_id === exchangeId) || false - } - - // 获取使用特定模型的交易员列表 - const getTradersUsingModel = (modelId: string) => { - return traders?.filter((t) => t.ai_model === modelId) || [] - } - - // 获取使用特定交易所的交易员列表 - const getTradersUsingExchange = (exchangeId: string) => { - return traders?.filter((t) => t.exchange_id === exchangeId) || [] - } - - const handleCreateTrader = async (data: CreateTraderRequest) => { - try { - const model = allModels?.find((m) => m.id === data.ai_model_id) - const exchange = allExchanges?.find((e) => e.id === data.exchange_id) - - if (!model?.enabled) { - toast.error(t('modelNotConfigured', language)) - return - } - - if (!exchange?.enabled) { - toast.error(t('exchangeNotConfigured', language)) - return - } - - await toast.promise(api.createTrader(data), { - loading: '正在创建…', - success: '创建成功', - error: '创建失败', - }) - setShowCreateModal(false) - // Immediately refresh traders list for better UX - await mutateTraders() - } catch (error) { - console.error('Failed to create trader:', error) - toast.error(t('createTraderFailed', language)) - } - } - - const handleEditTrader = async (traderId: string) => { - try { - const traderConfig = await api.getTraderConfig(traderId) - setEditingTrader(traderConfig) - setShowEditModal(true) - } catch (error) { - console.error('Failed to fetch trader config:', error) - toast.error(t('getTraderConfigFailed', language)) - } - } - - const handleSaveEditTrader = async (data: CreateTraderRequest) => { - if (!editingTrader || !editingTrader.trader_id) return - - try { - const enabledModels = allModels?.filter((m) => m.enabled) || [] - const enabledExchanges = - allExchanges?.filter((e) => { - if (!e.enabled) return false - - // Aster 交易所需要特殊字段 - if (e.id === 'aster') { - return ( - e.asterUser && - e.asterUser.trim() !== '' && - e.asterSigner && - e.asterSigner.trim() !== '' - ) - } - - // Hyperliquid 需要钱包地址 - if (e.id === 'hyperliquid') { - return ( - e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' - ) - } - - return true - }) || [] - - const model = enabledModels?.find((m) => m.id === data.ai_model_id) - const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id) - - if (!model) { - toast.error(t('modelConfigNotExist', language)) - return - } - - if (!exchange) { - toast.error(t('exchangeConfigNotExist', language)) - return - } - - const request = { - name: data.name, - ai_model_id: data.ai_model_id, - exchange_id: data.exchange_id, - initial_balance: data.initial_balance, - scan_interval_minutes: data.scan_interval_minutes, - btc_eth_leverage: data.btc_eth_leverage, - altcoin_leverage: data.altcoin_leverage, - trading_symbols: data.trading_symbols, - custom_prompt: data.custom_prompt, - override_base_prompt: data.override_base_prompt, - system_prompt_template: data.system_prompt_template, - is_cross_margin: data.is_cross_margin, - use_coin_pool: data.use_coin_pool, - use_oi_top: data.use_oi_top, - } - - await toast.promise(api.updateTrader(editingTrader.trader_id, request), { - loading: '正在保存…', - success: '保存成功', - error: '保存失败', - }) - setShowEditModal(false) - setEditingTrader(null) - // Immediately refresh traders list for better UX - await mutateTraders() - } catch (error) { - console.error('Failed to update trader:', error) - toast.error(t('updateTraderFailed', language)) - } - } - - const handleDeleteTrader = async (traderId: string) => { - { - const ok = await confirmToast(t('confirmDeleteTrader', language)) - if (!ok) return - } - - try { - await toast.promise(api.deleteTrader(traderId), { - loading: '正在删除…', - success: '删除成功', - error: '删除失败', - }) - - // Immediately refresh traders list for better UX - await mutateTraders() - } catch (error) { - console.error('Failed to delete trader:', error) - toast.error(t('deleteTraderFailed', language)) - } - } - - const handleToggleTrader = async (traderId: string, running: boolean) => { - try { - if (running) { - await toast.promise(api.stopTrader(traderId), { - loading: '正在停止…', - success: '已停止', - error: '停止失败', - }) - } else { - await toast.promise(api.startTrader(traderId), { - loading: '正在启动…', - success: '已启动', - error: '启动失败', - }) - } - - // Immediately refresh traders list to update running status - await mutateTraders() - } catch (error) { - console.error('Failed to toggle trader:', error) - toast.error(t('operationFailed', language)) - } - } - - const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => { - try { - const newValue = !currentShowInCompetition - await toast.promise(api.toggleCompetition(traderId, newValue), { - loading: '正在更新…', - success: newValue ? '已在竞技场显示' : '已在竞技场隐藏', - error: '更新失败', - }) - - // Immediately refresh traders list to update status - await mutateTraders() - } catch (error) { - console.error('Failed to toggle competition visibility:', error) - toast.error(t('operationFailed', language)) - } - } - - const handleModelClick = (modelId: string) => { - if (!isModelInUse(modelId)) { - setEditingModel(modelId) - setShowModelModal(true) - } - } - - const handleExchangeClick = (exchangeId: string) => { - if (!isExchangeInUse(exchangeId)) { - setEditingExchange(exchangeId) - setShowExchangeModal(true) - } - } - - // 通用删除配置处理函数 - const handleDeleteConfig = async (config: { - id: string - type: 'model' | 'exchange' - checkInUse: (id: string) => boolean - getUsingTraders: (id: string) => any[] - cannotDeleteKey: string - confirmDeleteKey: string - allItems: T[] | undefined - clearFields: (item: T) => T - buildRequest: (items: T[]) => any - updateApi: (request: any) => Promise - refreshApi: () => Promise - setItems: (items: T[]) => void - closeModal: () => void - errorKey: string - }) => { - // 检查是否有交易员正在使用 - if (config.checkInUse(config.id)) { - const usingTraders = config.getUsingTraders(config.id) - const traderNames = usingTraders.map((t) => t.trader_name).join(', ') - toast.error( - `${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}` - ) - return - } - - { - const ok = await confirmToast(t(config.confirmDeleteKey, language)) - if (!ok) return - } - - try { - const updatedItems = - config.allItems?.map((item) => - item.id === config.id ? config.clearFields(item) : item - ) || [] - - const request = config.buildRequest(updatedItems) - await toast.promise(config.updateApi(request), { - loading: '正在更新配置…', - success: '配置已更新', - error: '更新配置失败', - }) - - // 重新获取用户配置以确保数据同步 - const refreshedItems = await config.refreshApi() - config.setItems(refreshedItems) - - config.closeModal() - } catch (error) { - console.error(`Failed to delete ${config.type} config:`, error) - toast.error(t(config.errorKey, language)) - } - } - - const handleDeleteModel = async (modelId: string) => { - await handleDeleteConfig({ - id: modelId, - type: 'model', - checkInUse: isModelUsedByAnyTrader, - getUsingTraders: getTradersUsingModel, - cannotDeleteKey: 'cannotDeleteModelInUse', - confirmDeleteKey: 'confirmDeleteModel', - allItems: allModels, - clearFields: (m) => ({ - ...m, - apiKey: '', - customApiUrl: '', - customModelName: '', - enabled: false, - }), - buildRequest: (models) => ({ - models: Object.fromEntries( - models.map((model) => [ - model.provider, - { - enabled: model.enabled, - api_key: model.apiKey || '', - custom_api_url: model.customApiUrl || '', - custom_model_name: model.customModelName || '', - }, - ]) - ), - }), - updateApi: api.updateModelConfigs, - refreshApi: api.getModelConfigs, - setItems: (items) => { - // 使用函数式更新确保状态正确更新 - setAllModels([...items]) - }, - closeModal: () => { - setShowModelModal(false) - setEditingModel(null) - }, - errorKey: 'deleteConfigFailed', - }) - } - - const handleSaveModel = async ( - modelId: string, - apiKey: string, - customApiUrl?: string, - customModelName?: string - ) => { - try { - // 创建或更新用户的模型配置 - const existingModel = allModels?.find((m) => m.id === modelId) - let updatedModels - - // 找到要配置的模型(优先从已配置列表,其次从支持列表) - const modelToUpdate = - existingModel || supportedModels?.find((m) => m.id === modelId) - if (!modelToUpdate) { - toast.error(t('modelNotExist', language)) - return - } - - if (existingModel) { - // 更新现有配置 - updatedModels = - allModels?.map((m) => - m.id === modelId - ? { - ...m, - apiKey, - customApiUrl: customApiUrl || '', - customModelName: customModelName || '', - enabled: true, - } - : m - ) || [] - } else { - // 添加新配置 - const newModel = { - ...modelToUpdate, - apiKey, - customApiUrl: customApiUrl || '', - customModelName: customModelName || '', - enabled: true, - } - updatedModels = [...(allModels || []), newModel] - } - - const request = { - models: Object.fromEntries( - updatedModels.map((model) => [ - model.provider, // 使用 provider 而不是 id - { - enabled: model.enabled, - api_key: model.apiKey || '', - custom_api_url: model.customApiUrl || '', - custom_model_name: model.customModelName || '', - }, - ]) - ), - } - - await toast.promise(api.updateModelConfigs(request), { - loading: '正在更新模型配置…', - success: '模型配置已更新', - error: '更新模型配置失败', - }) - - // 重新获取用户配置以确保数据同步 - const refreshedModels = await api.getModelConfigs() - setAllModels(refreshedModels) - - setShowModelModal(false) - setEditingModel(null) - } catch (error) { - console.error('Failed to save model config:', error) - toast.error(t('saveConfigFailed', language)) - } - } - - const handleDeleteExchange = async (exchangeId: string) => { - await handleDeleteConfig({ - id: exchangeId, - type: 'exchange', - checkInUse: isExchangeUsedByAnyTrader, - getUsingTraders: getTradersUsingExchange, - cannotDeleteKey: 'cannotDeleteExchangeInUse', - confirmDeleteKey: 'confirmDeleteExchange', - allItems: allExchanges, - clearFields: (e) => ({ - ...e, - apiKey: '', - secretKey: '', - passphrase: '', // OKX专用 - hyperliquidWalletAddr: '', - asterUser: '', - asterSigner: '', - asterPrivateKey: '', - enabled: false, - }), - buildRequest: (exchanges) => ({ - exchanges: Object.fromEntries( - exchanges.map((exchange) => [ - exchange.id, - { - enabled: exchange.enabled, - api_key: exchange.apiKey || '', - secret_key: exchange.secretKey || '', - passphrase: exchange.passphrase || '', // OKX专用 - testnet: exchange.testnet || false, - hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '', - aster_user: exchange.asterUser || '', - aster_signer: exchange.asterSigner || '', - aster_private_key: exchange.asterPrivateKey || '', - }, - ]) - ), - }), - updateApi: api.updateExchangeConfigsEncrypted, - refreshApi: api.getExchangeConfigs, - setItems: (items) => { - // 使用函数式更新确保状态正确更新 - setAllExchanges([...items]) - }, - closeModal: () => { - setShowExchangeModal(false) - setEditingExchange(null) - }, - errorKey: 'deleteExchangeConfigFailed', - }) - } - - const handleSaveExchange = async ( - exchangeId: string, - apiKey: string, - secretKey?: string, - passphrase?: string, // OKX专用 - testnet?: boolean, - hyperliquidWalletAddr?: string, - asterUser?: string, - asterSigner?: string, - asterPrivateKey?: string, - lighterWalletAddr?: string, - lighterPrivateKey?: string, - lighterApiKeyPrivateKey?: string - ) => { - try { - // 找到要配置的交易所(从supportedExchanges中) - const exchangeToUpdate = supportedExchanges?.find( - (e) => e.id === exchangeId - ) - if (!exchangeToUpdate) { - toast.error(t('exchangeNotExist', language)) - return - } - - // 创建或更新用户的交易所配置 - const existingExchange = allExchanges?.find((e) => e.id === exchangeId) - let updatedExchanges - - if (existingExchange) { - // 更新现有配置 - updatedExchanges = - allExchanges?.map((e) => - e.id === exchangeId - ? { - ...e, - apiKey, - secretKey, - passphrase, // OKX专用 - testnet, - hyperliquidWalletAddr, - asterUser, - asterSigner, - asterPrivateKey, - lighterWalletAddr, - lighterPrivateKey, - lighterApiKeyPrivateKey, - enabled: true, - } - : e - ) || [] - } else { - // 添加新配置 - const newExchange = { - ...exchangeToUpdate, - apiKey, - secretKey, - passphrase, // OKX专用 - testnet, - hyperliquidWalletAddr, - asterUser, - asterSigner, - asterPrivateKey, - lighterWalletAddr, - lighterPrivateKey, - lighterApiKeyPrivateKey, - enabled: true, - } - updatedExchanges = [...(allExchanges || []), newExchange] - } - - const request = { - exchanges: Object.fromEntries( - updatedExchanges.map((exchange) => [ - exchange.id, - { - enabled: exchange.enabled, - api_key: exchange.apiKey || '', - secret_key: exchange.secretKey || '', - passphrase: exchange.passphrase || '', // OKX专用 - testnet: exchange.testnet || false, - hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '', - aster_user: exchange.asterUser || '', - aster_signer: exchange.asterSigner || '', - aster_private_key: exchange.asterPrivateKey || '', - lighter_wallet_addr: exchange.lighterWalletAddr || '', - lighter_private_key: exchange.lighterPrivateKey || '', - lighter_api_key_private_key: exchange.lighterApiKeyPrivateKey || '', - }, - ]) - ), - } - - await toast.promise(api.updateExchangeConfigsEncrypted(request), { - loading: '正在更新交易所配置…', - success: '交易所配置已更新', - error: '更新交易所配置失败', - }) - - // 重新获取用户配置以确保数据同步 - const refreshedExchanges = await api.getExchangeConfigs() - setAllExchanges(refreshedExchanges) - - setShowExchangeModal(false) - setEditingExchange(null) - } catch (error) { - console.error('Failed to save exchange config:', error) - toast.error(t('saveConfigFailed', language)) - } - } - - const handleAddModel = () => { - setEditingModel(null) - setShowModelModal(true) - } - - const handleAddExchange = () => { - setEditingExchange(null) - setShowExchangeModal(true) - } - - return { - // 辅助函数 - isModelInUse, - isExchangeInUse, - isModelUsedByAnyTrader, - isExchangeUsedByAnyTrader, - getTradersUsingModel, - getTradersUsingExchange, - - // 事件处理函数 - handleCreateTrader, - handleEditTrader, - handleSaveEditTrader, - handleDeleteTrader, - handleToggleTrader, - handleToggleCompetition, - handleAddModel, - handleAddExchange, - handleModelClick, - handleExchangeClick, - handleSaveModel, - handleDeleteModel, - handleSaveExchange, - handleDeleteExchange, - } -} diff --git a/web/src/types.ts b/web/src/types.ts index c7c2ac36..cb32ead3 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -438,6 +438,11 @@ export interface IndicatorConfig { quant_data_api_url?: string; enable_quant_oi?: boolean; enable_quant_netflow?: boolean; + // OI 排行数据(市场持仓量增减排行) + enable_oi_ranking?: boolean; + oi_ranking_api_url?: string; + oi_ranking_duration?: string; // "1h", "4h", "24h" + oi_ranking_limit?: number; } export interface KlineConfig { @@ -579,6 +584,10 @@ export interface CreateDebateRequest { prompt_variant?: string; // balanced, aggressive, conservative, scalping auto_execute?: boolean; trader_id?: string; // Trader to use for auto-execute + // OI Ranking data options + enable_oi_ranking?: boolean; // Whether to include OI ranking data + oi_ranking_limit?: number; // Number of OI ranking entries (default 10) + oi_duration?: string; // Duration for OI data (1h, 4h, 24h, etc.) participants: { ai_model_id: string; personality: DebatePersonality;