mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 07:16:56 +08:00
Dev backtest (#1134)
This commit is contained in:
583
api/backtest.go
Normal file
583
api/backtest.go
Normal file
@@ -0,0 +1,583 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nofx/backtest"
|
||||
"nofx/config"
|
||||
"nofx/decision"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (s *Server) registerBacktestRoutes(router *gin.RouterGroup) {
|
||||
router.POST("/start", s.handleBacktestStart)
|
||||
router.POST("/pause", s.handleBacktestPause)
|
||||
router.POST("/resume", s.handleBacktestResume)
|
||||
router.POST("/stop", s.handleBacktestStop)
|
||||
router.POST("/label", s.handleBacktestLabel)
|
||||
router.POST("/delete", s.handleBacktestDelete)
|
||||
router.GET("/status", s.handleBacktestStatus)
|
||||
router.GET("/runs", s.handleBacktestRuns)
|
||||
router.GET("/equity", s.handleBacktestEquity)
|
||||
router.GET("/trades", s.handleBacktestTrades)
|
||||
router.GET("/metrics", s.handleBacktestMetrics)
|
||||
router.GET("/trace", s.handleBacktestTrace)
|
||||
router.GET("/decisions", s.handleBacktestDecisions)
|
||||
router.GET("/export", s.handleBacktestExport)
|
||||
}
|
||||
|
||||
type backtestStartRequest struct {
|
||||
Config backtest.BacktestConfig `json:"config"`
|
||||
}
|
||||
|
||||
type runIDRequest struct {
|
||||
RunID string `json:"run_id"`
|
||||
}
|
||||
|
||||
type labelRequest struct {
|
||||
RunID string `json:"run_id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestStart(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
var req backtestStartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cfg := req.Config
|
||||
if cfg.RunID == "" {
|
||||
cfg.RunID = "bt_" + time.Now().UTC().Format("20060102_150405")
|
||||
}
|
||||
cfg.PromptTemplate = strings.TrimSpace(cfg.PromptTemplate)
|
||||
if cfg.PromptTemplate == "" {
|
||||
cfg.PromptTemplate = "default"
|
||||
}
|
||||
if _, err := decision.GetPromptTemplate(cfg.PromptTemplate); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("提示词模板不存在: %s", cfg.PromptTemplate)})
|
||||
return
|
||||
}
|
||||
cfg.CustomPrompt = strings.TrimSpace(cfg.CustomPrompt)
|
||||
cfg.UserID = normalizeUserID(c.GetString("user_id"))
|
||||
if err := s.hydrateBacktestAIConfig(&cfg); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
runner, err := s.backtestManager.Start(context.Background(), cfg)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
meta := runner.CurrentMetadata()
|
||||
c.JSON(http.StatusOK, meta)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestPause(c *gin.Context) {
|
||||
s.handleBacktestControl(c, s.backtestManager.Pause)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestResume(c *gin.Context) {
|
||||
s.handleBacktestControl(c, s.backtestManager.Resume)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestStop(c *gin.Context) {
|
||||
s.handleBacktestControl(c, s.backtestManager.Stop)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestControl(c *gin.Context, fn func(string) error) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
|
||||
var req runIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.RunID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.ensureBacktestRunOwnership(req.RunID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := fn(req.RunID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := s.backtestManager.LoadMetadata(req.RunID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "ok"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, meta)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestLabel(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
var req labelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.RunID) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
if _, err := s.ensureBacktestRunOwnership(req.RunID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
meta, err := s.backtestManager.UpdateLabel(req.RunID, req.Label)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, meta)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestDelete(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
var req runIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.RunID) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
if _, err := s.ensureBacktestRunOwnership(req.RunID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
if err := s.backtestManager.Delete(req.RunID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestStatus(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := s.ensureBacktestRunOwnership(runID, userID)
|
||||
if writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
|
||||
status := s.backtestManager.Status(runID)
|
||||
if status != nil {
|
||||
c.JSON(http.StatusOK, status)
|
||||
return
|
||||
}
|
||||
|
||||
payload := backtest.StatusPayload{
|
||||
RunID: meta.RunID,
|
||||
State: meta.State,
|
||||
ProgressPct: meta.Summary.ProgressPct,
|
||||
ProcessedBars: meta.Summary.ProcessedBars,
|
||||
CurrentTime: 0,
|
||||
DecisionCycle: meta.Summary.ProcessedBars,
|
||||
Equity: meta.Summary.EquityLast,
|
||||
UnrealizedPnL: 0,
|
||||
RealizedPnL: 0,
|
||||
Note: meta.Summary.LiquidationNote,
|
||||
LastUpdatedIso: meta.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestRuns(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
rawUserID := strings.TrimSpace(c.GetString("user_id"))
|
||||
userID := normalizeUserID(rawUserID)
|
||||
filterByUser := rawUserID != "" && rawUserID != "admin"
|
||||
|
||||
metas, err := s.backtestManager.ListRuns()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
stateFilter := strings.ToLower(strings.TrimSpace(c.Query("state")))
|
||||
search := strings.ToLower(strings.TrimSpace(c.Query("search")))
|
||||
limit := queryInt(c, "limit", 50)
|
||||
offset := queryInt(c, "offset", 0)
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
filtered := make([]*backtest.RunMetadata, 0, len(metas))
|
||||
for _, meta := range metas {
|
||||
if stateFilter != "" && !strings.EqualFold(string(meta.State), stateFilter) {
|
||||
continue
|
||||
}
|
||||
if search != "" {
|
||||
target := strings.ToLower(meta.RunID + " " + meta.Summary.DecisionTF + " " + meta.Label + " " + meta.LastError)
|
||||
if !strings.Contains(target, search) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if filterByUser {
|
||||
owner := strings.TrimSpace(meta.UserID)
|
||||
if owner != "" && owner != userID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, meta)
|
||||
}
|
||||
|
||||
total := len(filtered)
|
||||
start := offset
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := offset + limit
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
page := filtered[start:end]
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"total": total,
|
||||
"items": page,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestEquity(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
if _, err := s.ensureBacktestRunOwnership(runID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
timeframe := c.Query("tf")
|
||||
limit := queryInt(c, "limit", 1000)
|
||||
|
||||
points, err := s.backtestManager.LoadEquity(runID, timeframe, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, points)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestTrades(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
if _, err := s.ensureBacktestRunOwnership(runID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
limit := queryInt(c, "limit", 1000)
|
||||
|
||||
events, err := s.backtestManager.LoadTrades(runID, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, events)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestMetrics(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
if _, err := s.ensureBacktestRunOwnership(runID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
|
||||
metrics, err := s.backtestManager.GetMetrics(runID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, os.ErrNotExist) {
|
||||
c.JSON(http.StatusAccepted, gin.H{"error": "metrics not ready yet"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, metrics)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestTrace(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
if _, err := s.ensureBacktestRunOwnership(runID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
cycle := queryInt(c, "cycle", 0)
|
||||
record, err := s.backtestManager.GetTrace(runID, cycle)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, record)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestDecisions(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
if _, err := s.ensureBacktestRunOwnership(runID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
limit := queryInt(c, "limit", 20)
|
||||
offset := queryInt(c, "offset", 0)
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
records, err := backtest.LoadDecisionRecords(runID, limit, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
||||
func (s *Server) handleBacktestExport(c *gin.Context) {
|
||||
if s.backtestManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "backtest manager unavailable"})
|
||||
return
|
||||
}
|
||||
userID := normalizeUserID(c.GetString("user_id"))
|
||||
runID := c.Query("run_id")
|
||||
if runID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"})
|
||||
return
|
||||
}
|
||||
if _, err := s.ensureBacktestRunOwnership(runID, userID); writeBacktestAccessError(c, err) {
|
||||
return
|
||||
}
|
||||
path, err := s.backtestManager.ExportRun(runID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer os.Remove(path)
|
||||
filename := fmt.Sprintf("%s_export.zip", runID)
|
||||
c.FileAttachment(path, filename)
|
||||
}
|
||||
|
||||
func queryInt(c *gin.Context, name string, fallback int) int {
|
||||
if value := c.Query(name); value != "" {
|
||||
if v, err := strconv.Atoi(value); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
var errBacktestForbidden = errors.New("backtest run forbidden")
|
||||
|
||||
func normalizeUserID(id string) string {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return "default"
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *Server) ensureBacktestRunOwnership(runID, userID string) (*backtest.RunMetadata, error) {
|
||||
if s.backtestManager == nil {
|
||||
return nil, fmt.Errorf("backtest manager unavailable")
|
||||
}
|
||||
meta, err := s.backtestManager.LoadMetadata(runID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userID == "" || userID == "admin" {
|
||||
return meta, nil
|
||||
}
|
||||
owner := strings.TrimSpace(meta.UserID)
|
||||
if owner == "" {
|
||||
return meta, nil
|
||||
}
|
||||
if owner == "default" && userID == "admin" {
|
||||
return meta, nil
|
||||
}
|
||||
if owner != userID {
|
||||
return nil, errBacktestForbidden
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func writeBacktestAccessError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, errBacktestForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该回测任务"})
|
||||
case errors.Is(err, os.ErrNotExist), errors.Is(err, sql.ErrNoRows):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "回测任务不存在"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) resolveBacktestAIConfig(cfg *backtest.BacktestConfig, userID string) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config is nil")
|
||||
}
|
||||
if s.database == nil {
|
||||
return fmt.Errorf("系统数据库未就绪,无法加载AI模型配置")
|
||||
}
|
||||
|
||||
cfg.UserID = normalizeUserID(userID)
|
||||
|
||||
return s.hydrateBacktestAIConfig(cfg)
|
||||
}
|
||||
|
||||
func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config is nil")
|
||||
}
|
||||
if s.database == nil {
|
||||
return fmt.Errorf("系统数据库未就绪,无法加载AI模型配置")
|
||||
}
|
||||
|
||||
cfg.UserID = normalizeUserID(cfg.UserID)
|
||||
modelID := strings.TrimSpace(cfg.AIModelID)
|
||||
|
||||
var (
|
||||
model *config.AIModelConfig
|
||||
err error
|
||||
)
|
||||
|
||||
if modelID != "" {
|
||||
model, err = s.database.GetAIModel(cfg.UserID, modelID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载AI模型失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
model, err = s.database.GetDefaultAIModel(cfg.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("未找到可用的AI模型: %w", err)
|
||||
}
|
||||
cfg.AIModelID = model.ID
|
||||
}
|
||||
|
||||
if !model.Enabled {
|
||||
return fmt.Errorf("AI模型 %s 尚未启用", model.Name)
|
||||
}
|
||||
|
||||
apiKey := strings.TrimSpace(model.APIKey)
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("AI模型 %s 缺少API Key,请先在系统中配置", model.Name)
|
||||
}
|
||||
|
||||
cfg.AICfg.Provider = strings.ToLower(model.Provider)
|
||||
cfg.AICfg.APIKey = apiKey
|
||||
cfg.AICfg.BaseURL = strings.TrimSpace(model.CustomAPIURL)
|
||||
modelName := strings.TrimSpace(model.CustomModelName)
|
||||
if cfg.AICfg.Model == "" {
|
||||
cfg.AICfg.Model = modelName
|
||||
}
|
||||
cfg.AICfg.Model = strings.TrimSpace(cfg.AICfg.Model)
|
||||
|
||||
if cfg.AICfg.Provider == "custom" {
|
||||
if cfg.AICfg.BaseURL == "" {
|
||||
return fmt.Errorf("自定义AI模型需要配置 API 地址")
|
||||
}
|
||||
if cfg.AICfg.Model == "" {
|
||||
return fmt.Errorf("自定义AI模型需要配置模型名称")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
api/server.go
100
api/server.go
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"nofx/auth"
|
||||
"nofx/backtest"
|
||||
"nofx/config"
|
||||
"nofx/crypto"
|
||||
"nofx/decision"
|
||||
@@ -25,16 +26,23 @@ import (
|
||||
|
||||
// Server HTTP API服务器
|
||||
type Server struct {
|
||||
router *gin.Engine
|
||||
httpServer *http.Server
|
||||
traderManager *manager.TraderManager
|
||||
database *config.Database
|
||||
cryptoHandler *CryptoHandler
|
||||
port int
|
||||
router *gin.Engine
|
||||
httpServer *http.Server
|
||||
traderManager *manager.TraderManager
|
||||
database *config.Database
|
||||
cryptoHandler *CryptoHandler
|
||||
backtestManager *backtest.Manager
|
||||
port int
|
||||
}
|
||||
|
||||
// NewServer 创建API服务器
|
||||
func NewServer(traderManager *manager.TraderManager, database *config.Database, cryptoService *crypto.CryptoService, port int) *Server {
|
||||
func NewServer(
|
||||
traderManager *manager.TraderManager,
|
||||
database *config.Database,
|
||||
cryptoService *crypto.CryptoService,
|
||||
backtestManager *backtest.Manager,
|
||||
port int,
|
||||
) *Server {
|
||||
// 设置为Release模式(减少日志输出)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
@@ -47,11 +55,15 @@ func NewServer(traderManager *manager.TraderManager, database *config.Database,
|
||||
cryptoHandler := NewCryptoHandler(cryptoService)
|
||||
|
||||
s := &Server{
|
||||
router: router,
|
||||
traderManager: traderManager,
|
||||
database: database,
|
||||
cryptoHandler: cryptoHandler,
|
||||
port: port,
|
||||
router: router,
|
||||
traderManager: traderManager,
|
||||
database: database,
|
||||
cryptoHandler: cryptoHandler,
|
||||
backtestManager: backtestManager,
|
||||
port: port,
|
||||
}
|
||||
if s.backtestManager != nil {
|
||||
s.backtestManager.SetAIResolver(s.hydrateBacktestAIConfig)
|
||||
}
|
||||
|
||||
// 设置路由
|
||||
@@ -118,6 +130,11 @@ func (s *Server) setupRoutes() {
|
||||
// 需要认证的路由
|
||||
protected := api.Group("/", s.authMiddleware())
|
||||
{
|
||||
if s.backtestManager != nil {
|
||||
backtestGroup := protected.Group("/backtest")
|
||||
s.registerBacktestRoutes(backtestGroup)
|
||||
}
|
||||
|
||||
// 注销(加入黑名单)
|
||||
protected.POST("/logout", s.handleLogout)
|
||||
|
||||
@@ -154,6 +171,7 @@ func (s *Server) setupRoutes() {
|
||||
protected.GET("/decisions/latest", s.handleLatestDecisions)
|
||||
protected.GET("/statistics", s.handleStatistics)
|
||||
protected.GET("/performance", s.handlePerformance)
|
||||
protected.GET("/competition/full", s.handleCompetition)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1996,28 +2014,42 @@ func (s *Server) Start() error {
|
||||
addr := fmt.Sprintf(":%d", s.port)
|
||||
log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
|
||||
log.Printf("📊 API文档:")
|
||||
log.Printf(" • GET /api/health - 健康检查")
|
||||
log.Printf(" • GET /api/traders - 公开的AI交易员排行榜前50名(无需认证)")
|
||||
log.Printf(" • GET /api/competition - 公开的竞赛数据(无需认证)")
|
||||
log.Printf(" • GET /api/top-traders - 前5名交易员数据(无需认证,表现对比用)")
|
||||
log.Printf(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)")
|
||||
log.Printf(" • GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化)")
|
||||
log.Printf(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息)")
|
||||
log.Printf(" • POST /api/traders - 创建新的AI交易员")
|
||||
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
|
||||
log.Printf(" • POST /api/traders/:id/start - 启动AI交易员")
|
||||
log.Printf(" • POST /api/traders/:id/stop - 停止AI交易员")
|
||||
log.Printf(" • GET /api/models - 获取AI模型配置")
|
||||
log.Printf(" • PUT /api/models - 更新AI模型配置")
|
||||
log.Printf(" • GET /api/exchanges - 获取交易所配置")
|
||||
log.Printf(" • PUT /api/exchanges - 更新交易所配置")
|
||||
log.Printf(" • GET /api/status?trader_id=xxx - 指定trader的系统状态")
|
||||
log.Printf(" • GET /api/account?trader_id=xxx - 指定trader的账户信息")
|
||||
log.Printf(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表")
|
||||
log.Printf(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志")
|
||||
log.Printf(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策")
|
||||
log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息")
|
||||
log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析")
|
||||
log.Printf(" • GET /api/health - 健康检查")
|
||||
log.Printf(" • 公共竞赛/排行榜相关接口")
|
||||
log.Printf(" - GET /api/traders - 公开的AI交易员排行榜(无需认证)")
|
||||
log.Printf(" - GET /api/competition - 公开竞赛数据(无需认证)")
|
||||
log.Printf(" - GET /api/top-traders - 前5名交易员(无需认证)")
|
||||
log.Printf(" - GET /api/equity-history - 指定trader收益率历史(无需认证)")
|
||||
log.Printf(" - POST /api/equity-history-batch - 批量获取收益率历史(无需认证)")
|
||||
log.Printf(" - GET /api/traders/:id/public-config - 公开交易员配置(无需认证)")
|
||||
log.Printf(" • Backtest")
|
||||
log.Printf(" - GET /api/backtest/runs - 回测运行列表")
|
||||
log.Printf(" - POST /api/backtest/start - 启动新的回测")
|
||||
log.Printf(" - POST /api/backtest/pause - 暂停指定回测")
|
||||
log.Printf(" - POST /api/backtest/resume - 恢复指定回测")
|
||||
log.Printf(" - POST /api/backtest/stop - 停止指定回测")
|
||||
log.Printf(" - GET /api/backtest/status - 查询回测状态")
|
||||
log.Printf(" - GET /api/backtest/equity - 回测净值曲线")
|
||||
log.Printf(" - GET /api/backtest/trades - 回测交易记录")
|
||||
log.Printf(" - GET /api/backtest/metrics - 回测统计指标")
|
||||
log.Printf(" - GET /api/backtest/trace - 回测AI Trace")
|
||||
log.Printf(" - GET /api/backtest/export - 导出回测数据ZIP")
|
||||
log.Printf(" • Trader / 配置(需认证)")
|
||||
log.Printf(" - POST /api/traders - 创建AI交易员")
|
||||
log.Printf(" - DELETE /api/traders/:id - 删除AI交易员")
|
||||
log.Printf(" - POST /api/traders/:id/start - 启动AI交易员")
|
||||
log.Printf(" - POST /api/traders/:id/stop - 停止AI交易员")
|
||||
log.Printf(" - GET /api/models - 获取AI模型配置")
|
||||
log.Printf(" - PUT /api/models - 更新AI模型配置")
|
||||
log.Printf(" - GET /api/exchanges - 获取交易所配置")
|
||||
log.Printf(" - PUT /api/exchanges - 更新交易所配置")
|
||||
log.Printf(" - GET /api/status?trader_id=xxx - 指定trader的系统状态")
|
||||
log.Printf(" - GET /api/account?trader_id=xxx - 指定trader的账户信息")
|
||||
log.Printf(" - GET /api/positions?trader_id=xxx - 指定trader的持仓列表")
|
||||
log.Printf(" - GET /api/decisions?trader_id=xxx - 指定trader的决策日志")
|
||||
log.Printf(" - GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策")
|
||||
log.Printf(" - GET /api/statistics?trader_id=xxx - 指定trader的统计信息")
|
||||
log.Printf(" - GET /api/performance?trader_id=xxx - AI学习表现分析")
|
||||
log.Println()
|
||||
|
||||
// 创建 http.Server 以支持 graceful shutdown
|
||||
|
||||
@@ -97,17 +97,23 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) {
|
||||
AsterUser string `json:"aster_user"`
|
||||
AsterSigner string `json:"aster_signer"`
|
||||
AsterPrivateKey string `json:"aster_private_key"`
|
||||
LighterWalletAddr string `json:"lighter_wallet_addr"`
|
||||
LighterPrivateKey string `json:"lighter_private_key"`
|
||||
}{
|
||||
"binance": {
|
||||
Enabled: true,
|
||||
APIKey: "binance_api_key_1234567890abcdef",
|
||||
SecretKey: "binance_secret_key_1234567890abcdef",
|
||||
Testnet: false,
|
||||
LighterWalletAddr: "",
|
||||
LighterPrivateKey: "",
|
||||
},
|
||||
"hyperliquid": {
|
||||
Enabled: true,
|
||||
HyperliquidWalletAddr: "0x1234567890abcdef1234567890abcdef12345678",
|
||||
Testnet: false,
|
||||
LighterWalletAddr: "",
|
||||
LighterPrivateKey: "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user