mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
8e294a5eed
- Delete llm/ dead code (3 files, zero references) - Split mcp/ into sub-packages: mcp/provider/ (8 providers) and mcp/payment/ (4 payment clients) with registry pattern - Export Client internal fields and ClientHooks interface for sub-package access - Split api/server.go (3892 lines) into 8 domain-specific handler files - Split trader/auto_trader.go (2296 lines) into 5 focused files - Reorganize web/src/components/ flat files into auth/, charts/, trader/, common/, modals/, backtest/ subdirectories - Update all consumer imports to use registry-based provider creation
403 lines
10 KiB
Go
403 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"nofx/logger"
|
|
"nofx/market"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// handleTraderList Trader list
|
|
func (s *Server) handleTraderList(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
traders, err := s.store.Trader().List(userID)
|
|
if err != nil {
|
|
SafeInternalError(c, "Failed to get trader list", err)
|
|
return
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(traders))
|
|
for _, trader := range traders {
|
|
// Get real-time running status
|
|
isRunning := trader.IsRunning
|
|
if at, err := s.traderManager.GetTrader(trader.ID); err == nil {
|
|
status := at.GetStatus()
|
|
if running, ok := status["is_running"].(bool); ok {
|
|
isRunning = running
|
|
}
|
|
}
|
|
|
|
// Get strategy name if strategy_id is set
|
|
var strategyName string
|
|
if trader.StrategyID != "" {
|
|
if strategy, err := s.store.Strategy().Get(userID, trader.StrategyID); err == nil {
|
|
strategyName = strategy.Name
|
|
}
|
|
}
|
|
|
|
// Return complete AIModelID (e.g. "admin_deepseek"), don't truncate
|
|
// Frontend needs complete ID to verify model exists (consistent with handleGetTraderConfig)
|
|
result = append(result, map[string]interface{}{
|
|
"trader_id": trader.ID,
|
|
"trader_name": trader.Name,
|
|
"ai_model": trader.AIModelID, // Use complete ID
|
|
"exchange_id": trader.ExchangeID,
|
|
"is_running": isRunning,
|
|
"show_in_competition": trader.ShowInCompetition,
|
|
"initial_balance": trader.InitialBalance,
|
|
"strategy_id": trader.StrategyID,
|
|
"strategy_name": strategyName,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// handleGetTraderConfig Get trader detailed configuration
|
|
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": "Trader ID cannot be empty"})
|
|
return
|
|
}
|
|
|
|
fullCfg, err := s.store.Trader().GetFullConfig(userID, traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader config")
|
|
return
|
|
}
|
|
traderConfig := fullCfg.Trader
|
|
|
|
// Get real-time running status
|
|
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
|
|
}
|
|
}
|
|
|
|
// Return complete model ID without conversion, consistent with frontend model list
|
|
aiModelID := traderConfig.AIModelID
|
|
|
|
result := map[string]interface{}{
|
|
"trader_id": traderConfig.ID,
|
|
"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,
|
|
"altcoin_leverage": traderConfig.AltcoinLeverage,
|
|
"trading_symbols": traderConfig.TradingSymbols,
|
|
"custom_prompt": traderConfig.CustomPrompt,
|
|
"override_base_prompt": traderConfig.OverrideBasePrompt,
|
|
"is_cross_margin": traderConfig.IsCrossMargin,
|
|
"use_ai500": traderConfig.UseAI500,
|
|
"use_oi_top": traderConfig.UseOITop,
|
|
"is_running": isRunning,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// handleStatus System status
|
|
func (s *Server) handleStatus(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
status := trader.GetStatus()
|
|
c.JSON(http.StatusOK, status)
|
|
}
|
|
|
|
// handleAccount Account information
|
|
func (s *Server) handleAccount(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
logger.Infof("📊 Received account info request [%s]", trader.GetName())
|
|
account, err := trader.GetAccountInfo()
|
|
if err != nil {
|
|
SafeInternalError(c, "Get account info", err)
|
|
return
|
|
}
|
|
|
|
logger.Infof("✓ Returning account info [%s]: equity=%.2f, available=%.2f, pnl=%.2f (%.2f%%)",
|
|
trader.GetName(),
|
|
account["total_equity"],
|
|
account["available_balance"],
|
|
account["total_pnl"],
|
|
account["total_pnl_pct"])
|
|
c.JSON(http.StatusOK, account)
|
|
}
|
|
|
|
// handlePositions Position list
|
|
func (s *Server) handlePositions(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
positions, err := trader.GetPositions()
|
|
if err != nil {
|
|
SafeInternalError(c, "Get positions", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, positions)
|
|
}
|
|
|
|
// handlePositionHistory Historical closed positions with statistics
|
|
func (s *Server) handlePositionHistory(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
// Get optional query parameters
|
|
limitStr := c.DefaultQuery("limit", "100")
|
|
limit := 100
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 500 {
|
|
limit = l
|
|
}
|
|
|
|
// Get store
|
|
store := trader.GetStore()
|
|
if store == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Store not available"})
|
|
return
|
|
}
|
|
|
|
// Get closed positions
|
|
positions, err := store.Position().GetClosedPositions(trader.GetID(), limit)
|
|
if err != nil {
|
|
SafeInternalError(c, "Get position history", err)
|
|
return
|
|
}
|
|
|
|
// Get statistics
|
|
stats, _ := store.Position().GetFullStats(trader.GetID())
|
|
|
|
// Get symbol stats
|
|
symbolStats, _ := store.Position().GetSymbolStats(trader.GetID(), 10)
|
|
|
|
// Get direction stats
|
|
directionStats, _ := store.Position().GetDirectionStats(trader.GetID())
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"positions": positions,
|
|
"stats": stats,
|
|
"symbol_stats": symbolStats,
|
|
"direction_stats": directionStats,
|
|
})
|
|
}
|
|
|
|
// handleTrades Historical trades list
|
|
func (s *Server) handleTrades(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
// Get optional query parameters
|
|
symbol := c.Query("symbol")
|
|
limitStr := c.DefaultQuery("limit", "100")
|
|
limit := 100
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
|
limit = l
|
|
}
|
|
|
|
// Normalize symbol (add USDT suffix if not present)
|
|
if symbol != "" {
|
|
symbol = market.Normalize(symbol)
|
|
}
|
|
|
|
// Get trades from store
|
|
store := trader.GetStore()
|
|
if store == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Store not available"})
|
|
return
|
|
}
|
|
|
|
allTrades, err := store.Position().GetRecentTrades(trader.GetID(), limit)
|
|
if err != nil {
|
|
SafeInternalError(c, "Get trades", err)
|
|
return
|
|
}
|
|
|
|
// Filter by symbol if specified
|
|
if symbol != "" {
|
|
var result []interface{}
|
|
for _, trade := range allTrades {
|
|
if trade.Symbol == symbol {
|
|
result = append(result, trade)
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, allTrades)
|
|
}
|
|
|
|
// handleOrders Order list (all orders including open, close, stop loss, take profit, etc.)
|
|
func (s *Server) handleOrders(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
// Get optional query parameters
|
|
symbol := c.Query("symbol")
|
|
statusFilter := c.Query("status") // NEW, FILLED, CANCELED, etc.
|
|
limitStr := c.DefaultQuery("limit", "100")
|
|
limit := 100
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
|
limit = l
|
|
}
|
|
|
|
// Normalize symbol (add USDT suffix if not present)
|
|
if symbol != "" {
|
|
symbol = market.Normalize(symbol)
|
|
}
|
|
|
|
// Get orders from store
|
|
store := trader.GetStore()
|
|
if store == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Store not available"})
|
|
return
|
|
}
|
|
|
|
// Get orders with filters applied at database level
|
|
orders, err := store.Order().GetTraderOrdersFiltered(trader.GetID(), symbol, statusFilter, limit)
|
|
if err != nil {
|
|
SafeInternalError(c, "Get orders", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, orders)
|
|
}
|
|
|
|
// handleOrderFills Order fill details (all fills for a specific order)
|
|
func (s *Server) handleOrderFills(c *gin.Context) {
|
|
orderIDStr := c.Param("id")
|
|
orderID, err := strconv.ParseInt(orderIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid order ID"})
|
|
return
|
|
}
|
|
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
store := trader.GetStore()
|
|
if store == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Store not available"})
|
|
return
|
|
}
|
|
|
|
// Get fills for this order
|
|
fills, err := store.Order().GetOrderFills(orderID)
|
|
if err != nil {
|
|
SafeInternalError(c, "Get order fills", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, fills)
|
|
}
|
|
|
|
// handleOpenOrders Get open orders (pending SL/TP) from exchange
|
|
func (s *Server) handleOpenOrders(c *gin.Context) {
|
|
_, traderID, err := s.getTraderFromQuery(c)
|
|
if err != nil {
|
|
SafeBadRequest(c, "Invalid trader ID")
|
|
return
|
|
}
|
|
|
|
trader, err := s.traderManager.GetTrader(traderID)
|
|
if err != nil {
|
|
SafeNotFound(c, "Trader")
|
|
return
|
|
}
|
|
|
|
// Get symbol parameter (required for exchange query)
|
|
symbol := c.Query("symbol")
|
|
if symbol == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "symbol parameter is required"})
|
|
return
|
|
}
|
|
|
|
// Normalize symbol
|
|
symbol = market.Normalize(symbol)
|
|
|
|
// Get open orders from exchange
|
|
openOrders, err := trader.GetOpenOrders(symbol)
|
|
if err != nil {
|
|
SafeInternalError(c, "Get open orders", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, openOrders)
|
|
}
|