From 1a6b88d77f3a97bfa75cf654c41fbc16bf24be2a Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 16 Mar 2026 07:33:05 +0800 Subject: [PATCH 1/8] feat: add X-Client-ID header for claw402 monitoring (#1414) --- mcp/payment/x402.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mcp/payment/x402.go b/mcp/payment/x402.go index 7116f503..ef424c4d 100644 --- a/mcp/payment/x402.go +++ b/mcp/payment/x402.go @@ -236,6 +236,7 @@ func X402BuildRequest(url string, jsonData []byte) (*http.Request, error) { return nil, fmt.Errorf("fail to build request: %w", err) } req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Client-ID", "nofx") return req, nil } From 21a15f98eb1d4014e6dab93f902e7cff8bf0fe09 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Mon, 16 Mar 2026 07:38:01 +0800 Subject: [PATCH 2/8] refactor: remove all backtest module code and references Delete backtest/ engine (19 files), api/backtest.go, store/backtest.go, web backtest components (7 files), API client, types, docs, screenshot. Clean all backtest references from main.go, api/server.go, store/store.go, App.tsx, HeaderBar.tsx, LandingPage.tsx, translations, README and docs. --- README.md | 14 +- api/backtest.go | 863 ------------------ api/server.go | 18 +- backtest/account.go | 267 ------ backtest/ai_client.go | 72 -- backtest/aicache.go | 168 ---- backtest/config.go | 285 ------ backtest/datafeed.go | 206 ----- backtest/equity.go | 95 -- backtest/lock.go | 100 -- backtest/manager.go | 493 ---------- backtest/metrics.go | 239 ----- backtest/persistence_db.go | 40 - backtest/registry.go | 160 ---- backtest/retention.go | 101 -- backtest/runner.go | 341 ------- backtest/runner_loop.go | 563 ------------ backtest/runner_metrics.go | 239 ----- backtest/runner_orders.go | 420 --------- backtest/storage.go | 561 ------------ backtest/storage_db_impl.go | 498 ---------- backtest/types.go | 179 ---- docs/architecture/BACKTEST_MODULE.md | 624 ------------- docs/architecture/BACKTEST_MODULE.zh-CN.md | 624 ------------- docs/architecture/README.md | 25 +- docs/architecture/README.zh-CN.md | 25 +- docs/i18n/vi/README.md | 1 - docs/i18n/zh-CN/README.md | 1 - .../2026-03-06-telegram-agent-redesign.md | 1 - docs/plans/2026-03-06-telegram-bot.md | 2 +- docs/research/AI-Trader-Analysis-Report.md | 1 - main.go | 22 +- market/data.go | 2 +- screenshots/backtest-lab.png | Bin 287909 -> 0 bytes store/backtest.go | 574 ------------ store/store.go | 14 - web/src/App.tsx | 9 - .../components/backtest/BacktestChartTab.tsx | 434 --------- .../backtest/BacktestConfigForm.tsx | 597 ------------ .../backtest/BacktestDecisionsTab.tsx | 36 - .../backtest/BacktestOverviewTab.tsx | 325 ------- web/src/components/backtest/BacktestPage.tsx | 579 ------------ .../components/backtest/BacktestRunList.tsx | 151 --- .../components/backtest/BacktestTradesTab.tsx | 104 --- web/src/components/common/HeaderBar.tsx | 3 - web/src/i18n/translations.ts | 723 +-------------- web/src/lib/api/backtest.ts | 197 ---- web/src/lib/api/index.ts | 2 - web/src/pages/LandingPage.tsx | 1 - web/src/types/backtest.ts | 167 ---- web/src/types/index.ts | 1 - 51 files changed, 37 insertions(+), 11130 deletions(-) delete mode 100644 api/backtest.go delete mode 100644 backtest/account.go delete mode 100644 backtest/ai_client.go delete mode 100644 backtest/aicache.go delete mode 100644 backtest/config.go delete mode 100644 backtest/datafeed.go delete mode 100644 backtest/equity.go delete mode 100644 backtest/lock.go delete mode 100644 backtest/manager.go delete mode 100644 backtest/metrics.go delete mode 100644 backtest/persistence_db.go delete mode 100644 backtest/registry.go delete mode 100644 backtest/retention.go delete mode 100644 backtest/runner.go delete mode 100644 backtest/runner_loop.go delete mode 100644 backtest/runner_metrics.go delete mode 100644 backtest/runner_orders.go delete mode 100644 backtest/storage.go delete mode 100644 backtest/storage_db_impl.go delete mode 100644 backtest/types.go delete mode 100644 docs/architecture/BACKTEST_MODULE.md delete mode 100644 docs/architecture/BACKTEST_MODULE.zh-CN.md delete mode 100644 screenshots/backtest-lab.png delete mode 100644 store/backtest.go delete mode 100644 web/src/components/backtest/BacktestChartTab.tsx delete mode 100644 web/src/components/backtest/BacktestConfigForm.tsx delete mode 100644 web/src/components/backtest/BacktestDecisionsTab.tsx delete mode 100644 web/src/components/backtest/BacktestOverviewTab.tsx delete mode 100644 web/src/components/backtest/BacktestPage.tsx delete mode 100644 web/src/components/backtest/BacktestRunList.tsx delete mode 100644 web/src/components/backtest/BacktestTradesTab.tsx delete mode 100644 web/src/lib/api/backtest.ts delete mode 100644 web/src/types/backtest.ts diff --git a/README.md b/README.md index 5741acfd..369f695b 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,6 @@ Also compatible with **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** | **Strategy Studio** | Visual builder — coin sources, indicators, risk controls | | **AI Competition** | AIs compete in real-time, leaderboard ranks performance | | **Telegram Agent** | Chat with your trading assistant — streaming, tool calling, memory | -| **Backtest Lab** | Historical simulation with equity curves and performance metrics | | **Dashboard** | Live positions, P/L, AI decision logs with Chain of Thought | ### Markets @@ -158,11 +157,11 @@ Crypto · US Stocks · Forex · Metals
-Competition & Backtest +Competition -| Competition Mode | Backtest Lab | -|:---:|:---:| -| | | +| Competition Mode | +|:---:| +| |
--- @@ -253,8 +252,8 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas ├─────────────────────────────────────────────────┤ │ API Server (Go) │ ├──────────┬──────────┬──────────┬────────────────┤ - │ Strategy │ Backtest │ Telegram │ - │ Engine │ Lab │ Agent │ + │ Strategy │ Telegram │ + │ Engine │ Agent │ ├──────────┴──────────┴──────────┴────────────────┤ │ MCP AI Client Layer │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ @@ -277,7 +276,6 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas |:--|:--| | [Architecture](docs/architecture/README.md) | System design and module index | | [Strategy Module](docs/architecture/STRATEGY_MODULE.md) | Coin selection, AI prompts, execution | -| [Backtest Module](docs/architecture/BACKTEST_MODULE.md) | Historical simulation, metrics | | [FAQ](docs/faq/README.md) | Common questions | | [Getting Started](docs/getting-started/README.md) | Deployment guide | diff --git a/api/backtest.go b/api/backtest.go deleted file mode 100644 index 82b5eecb..00000000 --- a/api/backtest.go +++ /dev/null @@ -1,863 +0,0 @@ -package api - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "strconv" - "strings" - "time" - - "nofx/backtest" - "nofx/logger" - "nofx/market" - "nofx/provider/nofxos" - "nofx/store" - - "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) - router.GET("/klines", s.handleBacktestKlines) -} - -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 { - SafeBadRequest(c, "Invalid request parameters") - return - } - - cfg := req.Config - if cfg.RunID == "" { - cfg.RunID = "bt_" + time.Now().UTC().Format("20060102_150405") - } - cfg.CustomPrompt = strings.TrimSpace(cfg.CustomPrompt) - cfg.UserID = normalizeUserID(c.GetString("user_id")) - - logger.Infof("📊 Backtest request - symbols from request: %v (count=%d), strategyID: %s", - cfg.Symbols, len(cfg.Symbols), cfg.StrategyID) - - // Load strategy config if strategy_id is provided - if cfg.StrategyID != "" { - strategy, err := s.store.Strategy().Get(cfg.UserID, cfg.StrategyID) - if err != nil { - SafeBadRequest(c, "Failed to load strategy") - return - } - if strategy == nil { - SafeBadRequest(c, "Strategy not found") - return - } - var strategyConfig store.StrategyConfig - if err := json.Unmarshal([]byte(strategy.Config), &strategyConfig); err != nil { - SafeBadRequest(c, "Failed to parse strategy config") - return - } - cfg.SetLoadedStrategy(&strategyConfig) - logger.Infof("📊 Backtest using saved strategy: %s (%s)", strategy.Name, strategy.ID) - logger.Infof("📊 Strategy coin source: type=%s, use_ai500=%v, use_oi_top=%v, static_coins=%v", - strategyConfig.CoinSource.SourceType, - strategyConfig.CoinSource.UseAI500, - strategyConfig.CoinSource.UseOITop, - strategyConfig.CoinSource.StaticCoins) - - // If no symbols provided, fetch from strategy's coin source - if len(cfg.Symbols) == 0 { - symbols, err := s.resolveStrategyCoins(&strategyConfig) - if err != nil { - SafeBadRequest(c, "Failed to resolve coins from strategy") - return - } - cfg.Symbols = symbols - logger.Infof("📊 Resolved %d coins from strategy: %v", len(symbols), symbols) - } - } - - if err := s.hydrateBacktestAIConfig(&cfg); err != nil { - SafeBadRequest(c, "Failed to configure AI model") - return - } - - logger.Infof("📊 Starting backtest with final config: runID=%s, symbols=%v (count=%d), strategyID=%s", - cfg.RunID, cfg.Symbols, len(cfg.Symbols), cfg.StrategyID) - - runner, err := s.backtestManager.Start(context.Background(), cfg) - if err != nil { - SafeError(c, http.StatusBadRequest, "Failed to start backtest", err) - 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 { - SafeBadRequest(c, "Invalid request parameters") - return - } - if req.RunID == "" { - SafeBadRequest(c, "run_id is required") - return - } - - if _, err := s.ensureBacktestRunOwnership(req.RunID, userID); writeBacktestAccessError(c, err) { - return - } - - if err := fn(req.RunID); err != nil { - SafeError(c, http.StatusBadRequest, "Failed to execute backtest operation", err) - 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 { - SafeBadRequest(c, "Invalid request parameters") - return - } - if strings.TrimSpace(req.RunID) == "" { - SafeBadRequest(c, "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 { - SafeInternalError(c, "Update backtest label", err) - 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 { - SafeBadRequest(c, "Invalid request parameters") - return - } - if strings.TrimSpace(req.RunID) == "" { - SafeBadRequest(c, "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 { - SafeInternalError(c, "Delete backtest run", err) - 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 { - SafeInternalError(c, "List backtest runs", err) - 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 { - SafeError(c, http.StatusBadRequest, "Failed to load equity data", err) - 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 { - SafeError(c, http.StatusBadRequest, "Failed to load trades", err) - 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 - } - SafeError(c, http.StatusBadRequest, "Failed to load metrics", err) - 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 { - SafeNotFound(c, "Trace record") - 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 { - SafeInternalError(c, "Load decision records", err) - 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 { - SafeError(c, http.StatusBadRequest, "Failed to export backtest", err) - return - } - defer os.Remove(path) - filename := fmt.Sprintf("%s_export.zip", runID) - c.FileAttachment(path, filename) -} - -func (s *Server) handleBacktestKlines(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") - symbol := c.Query("symbol") - timeframe := c.Query("timeframe") - - if runID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"}) - return - } - if symbol == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "symbol is required"}) - return - } - - meta, err := s.ensureBacktestRunOwnership(runID, userID) - if writeBacktestAccessError(c, err) { - return - } - - // Load config to get time range - cfg, err := backtest.LoadConfig(runID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "failed to load backtest config"}) - return - } - - // Use decision timeframe if not specified - if timeframe == "" { - timeframe = cfg.DecisionTimeframe - if timeframe == "" { - timeframe = "15m" - } - } - - // Fetch klines for the backtest time range - startTime := time.Unix(cfg.StartTS, 0) - endTime := time.Unix(cfg.EndTS, 0) - - klines, err := market.GetKlinesRange(symbol, timeframe, startTime, endTime) - if err != nil { - SafeInternalError(c, "Fetch klines", err) - return - } - - // Convert to response format - type KlineResponse struct { - Time int64 `json:"time"` - Open float64 `json:"open"` - High float64 `json:"high"` - Low float64 `json:"low"` - Close float64 `json:"close"` - Volume float64 `json:"volume"` - } - - result := make([]KlineResponse, len(klines)) - for i, k := range klines { - result[i] = KlineResponse{ - Time: k.OpenTime / 1000, // Convert to seconds for lightweight-charts - Open: k.Open, - High: k.High, - Low: k.Low, - Close: k.Close, - Volume: k.Volume, - } - } - - c.JSON(http.StatusOK, gin.H{ - "symbol": symbol, - "timeframe": timeframe, - "start_ts": cfg.StartTS, - "end_ts": cfg.EndTS, - "count": len(result), - "klines": result, - "run_id": meta.RunID, - }) -} - -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 != 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): - SafeForbidden(c, "No permission to access this backtest task") - case errors.Is(err, os.ErrNotExist), errors.Is(err, sql.ErrNoRows): - SafeNotFound(c, "Backtest task") - default: - SafeInternalError(c, "Access backtest", err) - } - return true -} - -// resolveStrategyCoins fetches coins based on strategy's coin source configuration -func (s *Server) resolveStrategyCoins(strategyConfig *store.StrategyConfig) ([]string, error) { - if strategyConfig == nil { - return nil, fmt.Errorf("strategy config is nil") - } - - coinSource := strategyConfig.CoinSource - var symbols []string - symbolSet := make(map[string]bool) - - // Handle empty source_type - check flags for backward compatibility - sourceType := coinSource.SourceType - if sourceType == "" { - if coinSource.UseAI500 && coinSource.UseOITop { - sourceType = "mixed" - } else if coinSource.UseAI500 { - sourceType = "ai500" - } else if coinSource.UseOITop { - sourceType = "oi_top" - } else if len(coinSource.StaticCoins) > 0 { - sourceType = "static" - } else { - return nil, fmt.Errorf("strategy has no coin source configured") - } - logger.Infof("📊 Inferred source_type=%s from flags", sourceType) - } - - switch sourceType { - case "static": - for _, sym := range coinSource.StaticCoins { - sym = market.Normalize(sym) - if !symbolSet[sym] { - symbols = append(symbols, sym) - symbolSet[sym] = true - } - } - - case "ai500": - limit := coinSource.AI500Limit - if limit <= 0 { - limit = 30 - } - logger.Infof("📊 Fetching AI500 coins with limit=%d", limit) - coins, err := nofxos.DefaultClient().GetTopRatedCoins(limit) - if err != nil { - return nil, fmt.Errorf("failed to get AI500 coins: %w", err) - } - logger.Infof("📊 Got %d coins from AI500: %v", len(coins), coins) - for _, sym := range coins { - sym = market.Normalize(sym) - if !symbolSet[sym] { - symbols = append(symbols, sym) - symbolSet[sym] = true - } - } - - case "oi_top": - coins, err := nofxos.DefaultClient().GetOITopSymbols() - if err != nil { - return nil, fmt.Errorf("failed to get OI Top coins: %w", err) - } - limit := coinSource.OITopLimit - if limit <= 0 || limit > len(coins) { - limit = len(coins) - } - for i, sym := range coins { - if i >= limit { - break - } - sym = market.Normalize(sym) - if !symbolSet[sym] { - symbols = append(symbols, sym) - symbolSet[sym] = true - } - } - - case "mixed": - // Get from AI500 - if coinSource.UseAI500 { - limit := coinSource.AI500Limit - if limit <= 0 { - limit = 30 - } - coins, err := nofxos.DefaultClient().GetTopRatedCoins(limit) - if err != nil { - logger.Warnf("Failed to get AI500 coins: %v", err) - } else { - for _, sym := range coins { - sym = market.Normalize(sym) - if !symbolSet[sym] { - symbols = append(symbols, sym) - symbolSet[sym] = true - } - } - } - } - - // Get from OI Top - if coinSource.UseOITop { - coins, err := nofxos.DefaultClient().GetOITopSymbols() - if err != nil { - logger.Warnf("Failed to get OI Top coins: %v", err) - } else { - limit := coinSource.OITopLimit - if limit <= 0 || limit > len(coins) { - limit = len(coins) - } - for i, sym := range coins { - if i >= limit { - break - } - sym = market.Normalize(sym) - if !symbolSet[sym] { - symbols = append(symbols, sym) - symbolSet[sym] = true - } - } - } - } - - // Add static coins - for _, sym := range coinSource.StaticCoins { - sym = market.Normalize(sym) - if !symbolSet[sym] { - symbols = append(symbols, sym) - symbolSet[sym] = true - } - } - - default: - return nil, fmt.Errorf("unknown coin source type: %s", sourceType) - } - - if len(symbols) == 0 { - return nil, fmt.Errorf("no coins resolved from strategy") - } - - logger.Infof("📊 Final resolved symbols: %d coins - %v", len(symbols), symbols) - return symbols, nil -} - -func (s *Server) resolveBacktestAIConfig(cfg *backtest.BacktestConfig, userID string) error { - if cfg == nil { - return fmt.Errorf("config is nil") - } - if s.store == nil { - return fmt.Errorf("System database not ready, cannot load AI model configuration") - } - - 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.store == nil { - return fmt.Errorf("System database not ready, cannot load AI model configuration") - } - - cfg.UserID = normalizeUserID(cfg.UserID) - modelID := strings.TrimSpace(cfg.AIModelID) - - var ( - model *store.AIModel - err error - ) - - if modelID != "" { - model, err = s.store.AIModel().Get(cfg.UserID, modelID) - if err != nil { - return fmt.Errorf("Failed to load AI model: %w", err) - } - } else { - model, err = s.store.AIModel().GetDefault(cfg.UserID) - if err != nil { - return fmt.Errorf("No available AI model found: %w", err) - } - cfg.AIModelID = model.ID - } - - if !model.Enabled { - return fmt.Errorf("AI model %s is not enabled yet", model.Name) - } - - apiKey := strings.TrimSpace(string(model.APIKey)) - if apiKey == "" { - return fmt.Errorf("AI model %s is missing API Key, please configure it in the system first", model.Name) - } - - provider := strings.ToLower(strings.TrimSpace(model.Provider)) - // Ensure provider is never empty or "inherit" - infer from model name if needed - if provider == "" || provider == "inherit" { - modelNameLower := strings.ToLower(model.Name) - if strings.Contains(modelNameLower, "claude") || strings.Contains(modelNameLower, "anthropic") { - provider = "anthropic" - } else if strings.Contains(modelNameLower, "gpt") || strings.Contains(modelNameLower, "openai") { - provider = "openai" - } else if strings.Contains(modelNameLower, "gemini") || strings.Contains(modelNameLower, "google") { - provider = "google" - } else if strings.Contains(modelNameLower, "deepseek") { - provider = "deepseek" - } else if strings.Contains(modelNameLower, "minimax") { - provider = "minimax" - } else if model.CustomAPIURL != "" { - provider = "custom" - } else { - provider = "openai" // default fallback - } - logger.Infof("📊 Inferred AI provider '%s' from model name '%s'", provider, model.Name) - } - cfg.AICfg.Provider = 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("Custom AI model requires API URL configuration") - } - if cfg.AICfg.Model == "" { - return fmt.Errorf("Custom AI model requires model name configuration") - } - } - - return nil -} diff --git a/api/server.go b/api/server.go index 55944022..cfe8be2d 100644 --- a/api/server.go +++ b/api/server.go @@ -6,7 +6,6 @@ import ( "net" "net/http" "nofx/auth" - "nofx/backtest" "nofx/crypto" "nofx/logger" "nofx/manager" @@ -23,14 +22,13 @@ type Server struct { traderManager *manager.TraderManager store *store.Store cryptoHandler *CryptoHandler - backtestManager *backtest.Manager httpServer *http.Server port int telegramReloadCh chan<- struct{} // signal Telegram bot to reload } // NewServer Creates API server -func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoService *crypto.CryptoService, backtestManager *backtest.Manager, port int) *Server { +func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoService *crypto.CryptoService, port int) *Server { // Set to Release mode (reduce log output) gin.SetMode(gin.ReleaseMode) @@ -43,12 +41,11 @@ func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoServ cryptoHandler := NewCryptoHandler(cryptoService) s := &Server{ - router: router, - traderManager: traderManager, - store: st, - cryptoHandler: cryptoHandler, - backtestManager: backtestManager, - port: port, + router: router, + traderManager: traderManager, + store: st, + cryptoHandler: cryptoHandler, + port: port, } // Setup routes @@ -342,9 +339,6 @@ Returns the most recent AI decision for each symbol analyzed in the last scan cy Returns: {"total_trades":,"winning_trades":,"win_rate":,"total_pnl":,"sharpe_ratio":,"max_drawdown":}`, s.handleStatistics) - // Backtest routes - backtest := protected.Group("/backtest") - s.registerBacktestRoutes(backtest) } } } diff --git a/backtest/account.go b/backtest/account.go deleted file mode 100644 index 49468c1a..00000000 --- a/backtest/account.go +++ /dev/null @@ -1,267 +0,0 @@ -package backtest - -import ( - "fmt" - "math" - "strings" -) - -const epsilon = 1e-8 - -type position struct { - Symbol string - Side string - Quantity float64 - EntryPrice float64 - Leverage int - Margin float64 - Notional float64 - LiquidationPrice float64 - OpenTime int64 - AccumulatedFee float64 // Total fees paid (opening + any additions) -} - -type BacktestAccount struct { - initialBalance float64 - cash float64 - feeRate float64 - slippageRate float64 - positions map[string]*position - realizedPnL float64 -} - -func NewBacktestAccount(initialBalance, feeBps, slippageBps float64) *BacktestAccount { - return &BacktestAccount{ - initialBalance: initialBalance, - cash: initialBalance, - feeRate: feeBps / 10000.0, - slippageRate: slippageBps / 10000.0, - positions: make(map[string]*position), - } -} - -func positionKey(symbol, side string) string { - return strings.ToUpper(symbol) + ":" + side -} - -func (acc *BacktestAccount) ensurePosition(symbol, side string) *position { - key := positionKey(symbol, side) - if pos, ok := acc.positions[key]; ok { - return pos - } - pos := &position{Symbol: strings.ToUpper(symbol), Side: side} - acc.positions[key] = pos - return pos -} - -func (acc *BacktestAccount) removePosition(pos *position) { - key := positionKey(pos.Symbol, pos.Side) - delete(acc.positions, key) -} - -func (acc *BacktestAccount) Open(symbol, side string, quantity float64, leverage int, price float64, ts int64) (*position, float64, float64, error) { - if quantity <= 0 { - return nil, 0, 0, fmt.Errorf("quantity must be positive") - } - if leverage <= 0 { - return nil, 0, 0, fmt.Errorf("leverage must be positive") - } - - execPrice := applySlippage(price, acc.slippageRate, side, true) - notional := execPrice * quantity - margin := notional / float64(leverage) - fee := notional * acc.feeRate - - if margin+fee > acc.cash+epsilon { - return nil, 0, 0, fmt.Errorf("insufficient cash: need %.2f", margin+fee) - } - - acc.cash -= margin + fee - - pos := acc.ensurePosition(symbol, side) - - if pos.Quantity < epsilon { - pos.Quantity = quantity - pos.EntryPrice = execPrice - pos.Leverage = leverage - pos.Margin = margin - pos.Notional = notional - pos.OpenTime = ts - pos.LiquidationPrice = computeLiquidation(execPrice, leverage, side) - pos.AccumulatedFee = fee // Track opening fee - } else { - if leverage != pos.Leverage { - // Use weighted average leverage (approximate) - weightedMargin := pos.Margin + margin - pos.Leverage = int(math.Round((pos.Notional + notional) / weightedMargin)) - } - pos.Notional += notional - pos.Margin += margin - pos.EntryPrice = ((pos.EntryPrice * pos.Quantity) + execPrice*quantity) / (pos.Quantity + quantity) - pos.Quantity += quantity - pos.LiquidationPrice = computeLiquidation(pos.EntryPrice, pos.Leverage, side) - pos.AccumulatedFee += fee // Add to accumulated fee for position additions - } - - return pos, fee, execPrice, nil -} - -func (acc *BacktestAccount) Close(symbol, side string, quantity float64, price float64) (float64, float64, float64, error) { - key := positionKey(symbol, side) - pos, ok := acc.positions[key] - if !ok || pos.Quantity <= epsilon { - return 0, 0, 0, fmt.Errorf("no active %s position for %s", side, symbol) - } - - if quantity <= 0 || quantity > pos.Quantity+epsilon { - if math.Abs(quantity) <= epsilon { - quantity = pos.Quantity - } else { - return 0, 0, 0, fmt.Errorf("invalid close quantity") - } - } - - execPrice := applySlippage(price, acc.slippageRate, side, false) - closeNotional := execPrice * quantity // Notional at close price (for fee calculation) - closingFee := closeNotional * acc.feeRate - - // Calculate proportional values based on the portion being closed - closePortion := quantity / pos.Quantity - openingFeePortion := pos.AccumulatedFee * closePortion - totalFee := closingFee + openingFeePortion - - realized := realizedPnL(pos, quantity, execPrice) - - marginPortion := pos.Margin * closePortion - // BUG FIX: Calculate notional portion based on ENTRY price, not close price - // pos.Notional tracks the total notional at entry, so we must subtract proportionally - entryNotionalPortion := pos.Notional * closePortion - - // Note: Opening fee was already deducted from cash when opening, so we only deduct closing fee here - acc.cash += marginPortion + realized - closingFee - // But for realized P&L tracking, we include both fees - acc.realizedPnL += realized - totalFee - - pos.Quantity -= quantity - pos.Notional -= entryNotionalPortion // FIX: Use entry notional portion, not close notional - pos.Margin -= marginPortion - pos.AccumulatedFee -= openingFeePortion // Reduce tracked opening fee - - if pos.Quantity <= epsilon { - acc.removePosition(pos) - } - - // Return total fee (opening + closing) so caller can calculate accurate P&L - return realized, totalFee, execPrice, nil -} - -func (acc *BacktestAccount) TotalEquity(priceMap map[string]float64) (float64, float64, map[string]float64) { - unrealized := 0.0 - margin := 0.0 - perSymbol := make(map[string]float64) - for _, pos := range acc.positions { - price := priceMap[pos.Symbol] - pnl := unrealizedPnL(pos, price) - unrealized += pnl - margin += pos.Margin - perSymbol[pos.Symbol+":"+pos.Side] = pnl - } - return acc.cash + margin + unrealized, unrealized, perSymbol -} - -func applySlippage(price float64, rate float64, side string, isOpen bool) float64 { - if rate <= 0 { - return price - } - adjust := 1.0 - if side == "long" { - if isOpen { - adjust += rate - } else { - adjust -= rate - } - } else { - if isOpen { - adjust -= rate - } else { - adjust += rate - } - } - return price * adjust -} - -func computeLiquidation(entry float64, leverage int, side string) float64 { - if leverage <= 0 { - return 0 - } - lev := float64(leverage) - if side == "long" { - return entry * (1.0 - 1.0/lev) - } - return entry * (1.0 + 1.0/lev) -} - -func realizedPnL(pos *position, qty, price float64) float64 { - if pos.Side == "long" { - return (price - pos.EntryPrice) * qty - } - return (pos.EntryPrice - price) * qty -} - -func unrealizedPnL(pos *position, price float64) float64 { - if pos.Side == "long" { - return (price - pos.EntryPrice) * pos.Quantity - } - return (pos.EntryPrice - price) * pos.Quantity -} - -func (acc *BacktestAccount) Positions() []*position { - list := make([]*position, 0, len(acc.positions)) - for _, pos := range acc.positions { - list = append(list, pos) - } - return list -} - -func (acc *BacktestAccount) positionLeverage(symbol, side string) int { - key := positionKey(symbol, side) - if pos, ok := acc.positions[key]; ok && pos.Quantity > epsilon { - return pos.Leverage - } - return 0 -} - -func (acc *BacktestAccount) Cash() float64 { - return acc.cash -} - -func (acc *BacktestAccount) InitialBalance() float64 { - return acc.initialBalance -} - -func (acc *BacktestAccount) RealizedPnL() float64 { - return acc.realizedPnL -} - -// RestoreFromSnapshots restores account state from checkpoint. -func (acc *BacktestAccount) RestoreFromSnapshots(cash float64, realized float64, snaps []PositionSnapshot) { - acc.cash = cash - acc.realizedPnL = realized - acc.positions = make(map[string]*position) - for _, snap := range snaps { - pos := &position{ - Symbol: snap.Symbol, - Side: snap.Side, - Quantity: snap.Quantity, - EntryPrice: snap.AvgPrice, - Leverage: snap.Leverage, - Margin: snap.MarginUsed, - Notional: snap.Quantity * snap.AvgPrice, - LiquidationPrice: snap.LiquidationPrice, - OpenTime: snap.OpenTime, - AccumulatedFee: snap.AccumulatedFee, - } - key := positionKey(pos.Symbol, pos.Side) - acc.positions[key] = pos - } -} diff --git a/backtest/ai_client.go b/backtest/ai_client.go deleted file mode 100644 index 5395b430..00000000 --- a/backtest/ai_client.go +++ /dev/null @@ -1,72 +0,0 @@ -package backtest - -import ( - "fmt" - "strings" - - "nofx/mcp" - _ "nofx/mcp/payment" - _ "nofx/mcp/provider" -) - -// configureMCPClient creates/clones an MCP client based on configuration (returns mcp.AIClient interface). -// Note: mcp.New() returns an interface type; here we convert to concrete implementation before copying to avoid concurrent shared state. -func configureMCPClient(cfg BacktestConfig, base mcp.AIClient) (mcp.AIClient, error) { - providerName := strings.ToLower(strings.TrimSpace(cfg.AICfg.Provider)) - - // Inherit base client - if providerName == "" || providerName == "inherit" || providerName == "default" { - client := cloneBaseClient(base) - if cfg.AICfg.APIKey != "" || cfg.AICfg.BaseURL != "" || cfg.AICfg.Model != "" { - client.SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model) - } - return client, nil - } - - // Custom provider uses cloned base - if providerName == "custom" { - if cfg.AICfg.BaseURL == "" || cfg.AICfg.APIKey == "" || cfg.AICfg.Model == "" { - return nil, fmt.Errorf("custom provider requires base_url, api key and model") - } - client := cloneBaseClient(base) - client.SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model) - return client, nil - } - - // Create client via registry - client := mcp.NewAIClientByProvider(providerName) - if client == nil { - return nil, fmt.Errorf("unsupported ai provider %s", cfg.AICfg.Provider) - } - - if cfg.AICfg.APIKey == "" { - return nil, fmt.Errorf("%s provider requires api key", providerName) - } - - // Payment providers ignore custom URL - switch providerName { - case "blockrun-base", "blockrun-sol", "claw402": - client.SetAPIKey(cfg.AICfg.APIKey, "", cfg.AICfg.Model) - default: - client.SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model) - } - return client, nil -} - -// cloneBaseClient copies the base client to avoid shared mutable state. -// Uses the ClientEmbedder interface to extract the underlying *mcp.Client -// from any provider type that embeds it. -func cloneBaseClient(base mcp.AIClient) *mcp.Client { - if embedder, ok := base.(mcp.ClientEmbedder); ok { - if inner := embedder.BaseClient(); inner != nil { - cp := *inner - return &cp - } - } - if c, ok := base.(*mcp.Client); ok { - cp := *c - return &cp - } - // Fall back to a new default client - return mcp.NewClient().(*mcp.Client) -} diff --git a/backtest/aicache.go b/backtest/aicache.go deleted file mode 100644 index f5627e92..00000000 --- a/backtest/aicache.go +++ /dev/null @@ -1,168 +0,0 @@ -package backtest - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sync" - - "nofx/kernel" - "nofx/market" -) - -type cachedDecision struct { - Key string `json:"key"` - PromptVariant string `json:"prompt_variant"` - Timestamp int64 `json:"ts"` - Decision *kernel.FullDecision `json:"decision"` -} - -// AICache persists AI decisions for repeated backtesting or replay. -type AICache struct { - mu sync.RWMutex - path string - Entries map[string]cachedDecision `json:"entries"` -} - -func LoadAICache(path string) (*AICache, error) { - if path == "" { - return nil, fmt.Errorf("ai cache path is empty") - } - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, err - } - - cache := &AICache{ - path: path, - Entries: make(map[string]cachedDecision), - } - - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return cache, nil - } - return nil, err - } - if len(data) == 0 { - return cache, nil - } - if err := json.Unmarshal(data, cache); err != nil { - return nil, err - } - if cache.Entries == nil { - cache.Entries = make(map[string]cachedDecision) - } - return cache, nil -} - -func (c *AICache) Path() string { - if c == nil { - return "" - } - return c.path -} - -func (c *AICache) Get(key string) (*kernel.FullDecision, bool) { - if c == nil || key == "" { - return nil, false - } - c.mu.RLock() - entry, ok := c.Entries[key] - c.mu.RUnlock() - if !ok || entry.Decision == nil { - return nil, false - } - return cloneDecision(entry.Decision), true -} - -func (c *AICache) Put(key string, variant string, ts int64, decision *kernel.FullDecision) error { - if c == nil || key == "" || decision == nil { - return nil - } - entry := cachedDecision{ - Key: key, - PromptVariant: variant, - Timestamp: ts, - Decision: cloneDecision(decision), - } - c.mu.Lock() - c.Entries[key] = entry - c.mu.Unlock() - return c.save() -} - -func (c *AICache) save() error { - if c == nil || c.path == "" { - return nil - } - c.mu.RLock() - data, err := json.MarshalIndent(c, "", " ") - c.mu.RUnlock() - if err != nil { - return err - } - return writeFileAtomic(c.path, data, 0o644) -} - -func cloneDecision(src *kernel.FullDecision) *kernel.FullDecision { - if src == nil { - return nil - } - data, err := json.Marshal(src) - if err != nil { - return nil - } - var dst kernel.FullDecision - if err := json.Unmarshal(data, &dst); err != nil { - return nil - } - return &dst -} - -func computeCacheKey(ctx *kernel.Context, variant string, ts int64) (string, error) { - if ctx == nil { - return "", fmt.Errorf("context is nil") - } - payload := struct { - Variant string `json:"variant"` - Timestamp int64 `json:"ts"` - CurrentTime string `json:"current_time"` - Account kernel.AccountInfo `json:"account"` - Positions []kernel.PositionInfo `json:"positions"` - CandidateCoins []kernel.CandidateCoin `json:"candidate_coins"` - MarketData map[string]market.Data `json:"market"` - MarginUsedPct float64 `json:"margin_used_pct"` - Runtime int `json:"runtime_minutes"` - CallCount int `json:"call_count"` - }{ - Variant: variant, - Timestamp: ts, - CurrentTime: ctx.CurrentTime, - Account: ctx.Account, - Positions: ctx.Positions, - CandidateCoins: ctx.CandidateCoins, - MarginUsedPct: ctx.Account.MarginUsedPct, - Runtime: ctx.RuntimeMinutes, - CallCount: ctx.CallCount, - MarketData: make(map[string]market.Data, len(ctx.MarketDataMap)), - } - - for symbol, data := range ctx.MarketDataMap { - if data == nil { - continue - } - payload.MarketData[symbol] = *data - } - - bytes, err := json.Marshal(payload) - if err != nil { - return "", err - } - sum := sha256.Sum256(bytes) - return hex.EncodeToString(sum[:]), nil -} diff --git a/backtest/config.go b/backtest/config.go deleted file mode 100644 index 629fac97..00000000 --- a/backtest/config.go +++ /dev/null @@ -1,285 +0,0 @@ -package backtest - -import ( - "fmt" - "strings" - "time" - - "nofx/market" - "nofx/store" -) - -// AIConfig defines the AI client configuration used in backtesting. -type AIConfig struct { - Provider string `json:"provider"` - Model string `json:"model"` - APIKey string `json:"key"` - SecretKey string `json:"secret_key,omitempty"` - BaseURL string `json:"base_url,omitempty"` - Temperature float64 `json:"temperature,omitempty"` -} - -type LeverageConfig struct { - BTCETHLeverage int `json:"btc_eth_leverage"` - AltcoinLeverage int `json:"altcoin_leverage"` -} - -// BacktestConfig describes the input configuration for a backtest run. -type BacktestConfig struct { - RunID string `json:"run_id"` - UserID string `json:"user_id,omitempty"` - AIModelID string `json:"ai_model_id,omitempty"` - StrategyID string `json:"strategy_id,omitempty"` // Optional: use saved strategy from Strategy Studio - Symbols []string `json:"symbols"` - Timeframes []string `json:"timeframes"` - DecisionTimeframe string `json:"decision_timeframe"` - DecisionCadenceNBars int `json:"decision_cadence_nbars"` - StartTS int64 `json:"start_ts"` - EndTS int64 `json:"end_ts"` - InitialBalance float64 `json:"initial_balance"` - FeeBps float64 `json:"fee_bps"` - SlippageBps float64 `json:"slippage_bps"` - FillPolicy string `json:"fill_policy"` - PromptVariant string `json:"prompt_variant"` - PromptTemplate string `json:"prompt_template"` - CustomPrompt string `json:"custom_prompt"` - OverrideBasePrompt bool `json:"override_prompt"` - CacheAI bool `json:"cache_ai"` - ReplayOnly bool `json:"replay_only"` - - AICfg AIConfig `json:"ai"` - Leverage LeverageConfig `json:"leverage"` - - SharedAICachePath string `json:"ai_cache_path,omitempty"` - CheckpointIntervalBars int `json:"checkpoint_interval_bars,omitempty"` - CheckpointIntervalSeconds int `json:"checkpoint_interval_seconds,omitempty"` - ReplayDecisionDir string `json:"replay_decision_dir,omitempty"` - - // Internal: loaded strategy config (set by Manager when StrategyID is provided) - loadedStrategy *store.StrategyConfig `json:"-"` -} - -// Validate performs validity checks on the configuration and fills in default values. -func (cfg *BacktestConfig) Validate() error { - if cfg == nil { - return fmt.Errorf("config is nil") - } - cfg.RunID = strings.TrimSpace(cfg.RunID) - if cfg.RunID == "" { - return fmt.Errorf("run_id cannot be empty") - } - cfg.UserID = strings.TrimSpace(cfg.UserID) - if cfg.UserID == "" { - cfg.UserID = "default" - } - cfg.AIModelID = strings.TrimSpace(cfg.AIModelID) - - if len(cfg.Symbols) == 0 { - return fmt.Errorf("at least one symbol is required") - } - for i, sym := range cfg.Symbols { - cfg.Symbols[i] = market.Normalize(sym) - } - - if len(cfg.Timeframes) == 0 { - cfg.Timeframes = []string{"3m", "15m", "4h"} - } - normTF := make([]string, 0, len(cfg.Timeframes)) - for _, tf := range cfg.Timeframes { - normalized, err := market.NormalizeTimeframe(tf) - if err != nil { - return fmt.Errorf("invalid timeframe '%s': %w", tf, err) - } - normTF = append(normTF, normalized) - } - cfg.Timeframes = normTF - - if cfg.DecisionTimeframe == "" { - cfg.DecisionTimeframe = cfg.Timeframes[0] - } - normalizedDecision, err := market.NormalizeTimeframe(cfg.DecisionTimeframe) - if err != nil { - return fmt.Errorf("invalid decision_timeframe: %w", err) - } - cfg.DecisionTimeframe = normalizedDecision - - if cfg.DecisionCadenceNBars <= 0 { - cfg.DecisionCadenceNBars = 20 - } - - if cfg.StartTS <= 0 || cfg.EndTS <= 0 || cfg.EndTS <= cfg.StartTS { - return fmt.Errorf("invalid start_ts/end_ts") - } - - if cfg.InitialBalance <= 0 { - cfg.InitialBalance = 1000 - } - - if cfg.FillPolicy == "" { - cfg.FillPolicy = FillPolicyNextOpen - } - if err := validateFillPolicy(cfg.FillPolicy); err != nil { - return err - } - - if cfg.CheckpointIntervalBars <= 0 { - cfg.CheckpointIntervalBars = 20 - } - if cfg.CheckpointIntervalSeconds <= 0 { - cfg.CheckpointIntervalSeconds = 2 - } - - cfg.PromptVariant = strings.TrimSpace(cfg.PromptVariant) - if cfg.PromptVariant == "" { - cfg.PromptVariant = "baseline" - } - cfg.PromptTemplate = strings.TrimSpace(cfg.PromptTemplate) - if cfg.PromptTemplate == "" { - cfg.PromptTemplate = "default" - } - cfg.CustomPrompt = strings.TrimSpace(cfg.CustomPrompt) - - if cfg.AICfg.Provider == "" { - cfg.AICfg.Provider = "inherit" - } - if cfg.AICfg.Temperature == 0 { - cfg.AICfg.Temperature = 0.4 - } - - if cfg.Leverage.BTCETHLeverage <= 0 { - cfg.Leverage.BTCETHLeverage = 5 - } - if cfg.Leverage.AltcoinLeverage <= 0 { - cfg.Leverage.AltcoinLeverage = 5 - } - - return nil -} - -// Duration returns the backtest interval duration. -func (cfg *BacktestConfig) Duration() time.Duration { - if cfg == nil { - return 0 - } - return time.Unix(cfg.EndTS, 0).Sub(time.Unix(cfg.StartTS, 0)) -} - -const ( - // FillPolicyNextOpen uses the open price of the next bar for execution. - FillPolicyNextOpen = "next_open" - // FillPolicyBarVWAP uses the approximate VWAP of the current bar for execution. - FillPolicyBarVWAP = "bar_vwap" - // FillPolicyMidPrice uses the mid-price (high+low)/2 for execution. - FillPolicyMidPrice = "mid" -) - -func validateFillPolicy(policy string) error { - switch policy { - case FillPolicyNextOpen, FillPolicyBarVWAP, FillPolicyMidPrice: - return nil - default: - return fmt.Errorf("unsupported fill_policy '%s'", policy) - } -} - -// SetLoadedStrategy sets the loaded strategy config from database. -func (cfg *BacktestConfig) SetLoadedStrategy(strategy *store.StrategyConfig) { - cfg.loadedStrategy = strategy -} - -// ToStrategyConfig converts BacktestConfig to StrategyConfig for unified prompt generation. -// This ensures backtest uses the same StrategyEngine logic as live trading. -// If a strategy was loaded from database (via StrategyID), it will be used with overrides. -func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig { - // If a strategy was loaded from database, use it with some overrides - if cfg.loadedStrategy != nil { - result := *cfg.loadedStrategy // Make a copy - - // Override coin source with backtest symbols (backtest-specified pairs take priority) - if len(cfg.Symbols) > 0 { - result.CoinSource.SourceType = "static" - result.CoinSource.StaticCoins = cfg.Symbols - result.CoinSource.UseAI500 = false - result.CoinSource.UseOITop = false - } - - // Override timeframes with backtest config - if len(cfg.Timeframes) > 0 { - result.Indicators.Klines.SelectedTimeframes = cfg.Timeframes - result.Indicators.Klines.PrimaryTimeframe = cfg.Timeframes[0] - if len(cfg.Timeframes) > 1 { - result.Indicators.Klines.LongerTimeframe = cfg.Timeframes[len(cfg.Timeframes)-1] - } - result.Indicators.Klines.EnableMultiTimeframe = len(cfg.Timeframes) > 1 - } - - // Override leverage with backtest config - if cfg.Leverage.BTCETHLeverage > 0 { - result.RiskControl.BTCETHMaxLeverage = cfg.Leverage.BTCETHLeverage - } - if cfg.Leverage.AltcoinLeverage > 0 { - result.RiskControl.AltcoinMaxLeverage = cfg.Leverage.AltcoinLeverage - } - - // Override custom prompt if provided in backtest config - if cfg.CustomPrompt != "" { - result.CustomPrompt = cfg.CustomPrompt - } - - return &result - } - - // Fallback: build strategy config from backtest config (original logic) - primaryTF := "5m" - longerTF := "4h" - if len(cfg.Timeframes) > 0 { - primaryTF = cfg.Timeframes[0] - } - if len(cfg.Timeframes) > 1 { - longerTF = cfg.Timeframes[len(cfg.Timeframes)-1] - } - - return &store.StrategyConfig{ - CoinSource: store.CoinSourceConfig{ - SourceType: "static", - StaticCoins: cfg.Symbols, - UseAI500: false, - AI500Limit: len(cfg.Symbols), - UseOITop: false, - OITopLimit: 0, - }, - Indicators: store.IndicatorConfig{ - Klines: store.KlineConfig{ - PrimaryTimeframe: primaryTF, - PrimaryCount: 30, - LongerTimeframe: longerTF, - LongerCount: 10, - EnableMultiTimeframe: len(cfg.Timeframes) > 1, - SelectedTimeframes: cfg.Timeframes, - }, - EnableRawKlines: true, - EnableEMA: true, - EnableMACD: true, - EnableRSI: true, - EnableATR: true, - EnableVolume: true, - EnableOI: true, - EnableFundingRate: true, - EMAPeriods: []int{20, 50}, - RSIPeriods: []int{7, 14}, - ATRPeriods: []int{14}, - }, - CustomPrompt: cfg.CustomPrompt, - RiskControl: store.RiskControlConfig{ - MaxPositions: 3, - BTCETHMaxLeverage: cfg.Leverage.BTCETHLeverage, - AltcoinMaxLeverage: cfg.Leverage.AltcoinLeverage, - BTCETHMaxPositionValueRatio: 5.0, - AltcoinMaxPositionValueRatio: 1.0, - MaxMarginUsage: 0.9, - MinPositionSize: 12, - MinRiskRewardRatio: 3.0, - MinConfidence: 75, - }, - } -} diff --git a/backtest/datafeed.go b/backtest/datafeed.go deleted file mode 100644 index 0e06ed82..00000000 --- a/backtest/datafeed.go +++ /dev/null @@ -1,206 +0,0 @@ -package backtest - -import ( - "fmt" - "sort" - "time" - - "nofx/market" -) - -type timeframeSeries struct { - klines []market.Kline - closeTimes []int64 -} - -type symbolSeries struct { - byTF map[string]*timeframeSeries -} - -// DataFeed manages historical kline data and provides time-progressive snapshots for backtesting. -type DataFeed struct { - cfg BacktestConfig - symbols []string - timeframes []string - symbolSeries map[string]*symbolSeries - decisionTimes []int64 - primaryTF string - longerTF string -} - -func NewDataFeed(cfg BacktestConfig) (*DataFeed, error) { - df := &DataFeed{ - cfg: cfg, - symbols: make([]string, len(cfg.Symbols)), - timeframes: append([]string(nil), cfg.Timeframes...), - symbolSeries: make(map[string]*symbolSeries), - primaryTF: cfg.DecisionTimeframe, - } - copy(df.symbols, cfg.Symbols) - - if err := df.loadAll(); err != nil { - return nil, err - } - - return df, nil -} - -func (df *DataFeed) loadAll() error { - start := time.Unix(df.cfg.StartTS, 0) - end := time.Unix(df.cfg.EndTS, 0) - - // longest timeframe used for auxiliary indicators - var longestDur time.Duration - for _, tf := range df.timeframes { - dur, err := market.TFDuration(tf) - if err != nil { - return err - } - if dur > longestDur { - longestDur = dur - df.longerTF = tf - } - } - - for _, symbol := range df.symbols { - ss := &symbolSeries{byTF: make(map[string]*timeframeSeries)} - for _, tf := range df.timeframes { - dur, _ := market.TFDuration(tf) - buffer := dur * 200 - fetchStart := start.Add(-buffer) - if fetchStart.Before(time.Unix(0, 0)) { - fetchStart = time.Unix(0, 0) - } - fetchEnd := end.Add(dur) - - klines, err := market.GetKlinesRange(symbol, tf, fetchStart, fetchEnd) - if err != nil { - return fmt.Errorf("fetch klines for %s %s: %w", symbol, tf, err) - } - if len(klines) == 0 { - return fmt.Errorf("no klines for %s %s", symbol, tf) - } - - series := &timeframeSeries{ - klines: klines, - closeTimes: make([]int64, len(klines)), - } - for i, k := range klines { - series.closeTimes[i] = k.CloseTime - } - ss.byTF[tf] = series - } - df.symbolSeries[symbol] = ss - } - - // Generate backtest progress timeline using the primary timeframe of the first symbol - firstSymbol := df.symbols[0] - primarySeries := df.symbolSeries[firstSymbol].byTF[df.primaryTF] - startMs := start.UnixMilli() - endMs := end.UnixMilli() - for _, ts := range primarySeries.closeTimes { - if ts < startMs { - continue - } - if ts > endMs { - break - } - df.decisionTimes = append(df.decisionTimes, ts) - // Align other symbols; report error early if data is missing - for _, symbol := range df.symbols[1:] { - if _, ok := df.symbolSeries[symbol].byTF[df.primaryTF]; !ok { - return fmt.Errorf("symbol %s missing timeframe %s", symbol, df.primaryTF) - } - } - } - if len(df.decisionTimes) == 0 { - return fmt.Errorf("no decision bars in range") - } - return nil -} - -func (df *DataFeed) DecisionBarCount() int { - return len(df.decisionTimes) -} - -func (df *DataFeed) DecisionTimestamp(index int) int64 { - // Bounds check to prevent panic - if index < 0 || index >= len(df.decisionTimes) { - return 0 - } - return df.decisionTimes[index] -} - -func (df *DataFeed) sliceUpTo(symbol, tf string, ts int64) []market.Kline { - // Nil checks to prevent panic - ss, ok := df.symbolSeries[symbol] - if !ok || ss == nil { - return nil - } - series, ok := ss.byTF[tf] - if !ok || series == nil { - return nil - } - idx := sort.Search(len(series.closeTimes), func(i int) bool { - return series.closeTimes[i] > ts - }) - if idx <= 0 { - return nil - } - return series.klines[:idx] -} - -func (df *DataFeed) BuildMarketData(ts int64) (map[string]*market.Data, map[string]map[string]*market.Data, error) { - result := make(map[string]*market.Data, len(df.symbols)) - multi := make(map[string]map[string]*market.Data, len(df.symbols)) - - for _, symbol := range df.symbols { - perTF := make(map[string]*market.Data, len(df.timeframes)) - for _, tf := range df.timeframes { - series := df.sliceUpTo(symbol, tf, ts) - if len(series) == 0 { - continue - } - var longer []market.Kline - if df.longerTF != "" && df.longerTF != tf { - longer = df.sliceUpTo(symbol, df.longerTF, ts) - } - data, err := market.BuildDataFromKlines(symbol, series, longer) - if err != nil { - return nil, nil, err - } - perTF[tf] = data - if tf == df.primaryTF { - result[symbol] = data - } - } - if _, ok := perTF[df.primaryTF]; !ok { - return nil, nil, fmt.Errorf("no primary data for %s at %d", symbol, ts) - } - multi[symbol] = perTF - } - return result, multi, nil -} - -func (df *DataFeed) decisionBarSnapshot(symbol string, ts int64) (*market.Kline, *market.Kline) { - ss, ok := df.symbolSeries[symbol] - if !ok { - return nil, nil - } - series, ok := ss.byTF[df.primaryTF] - if !ok { - return nil, nil - } - idx := sort.Search(len(series.closeTimes), func(i int) bool { - return series.closeTimes[i] >= ts - }) - if idx >= len(series.closeTimes) || series.closeTimes[idx] != ts { - return nil, nil - } - curr := &series.klines[idx] - var next *market.Kline - if idx+1 < len(series.klines) { - next = &series.klines[idx+1] - } - return curr, next -} diff --git a/backtest/equity.go b/backtest/equity.go deleted file mode 100644 index 8f153268..00000000 --- a/backtest/equity.go +++ /dev/null @@ -1,95 +0,0 @@ -package backtest - -import ( - "math" - "sort" - - "nofx/market" -) - -// ResampleEquity resamples equity curve based on timeframe. -func ResampleEquity(points []EquityPoint, timeframe string) ([]EquityPoint, error) { - if timeframe == "" { - return points, nil - } - dur, err := market.TFDuration(timeframe) - if err != nil { - return nil, err - } - if len(points) == 0 { - return points, nil - } - - durMs := dur.Milliseconds() - if durMs <= 0 { - return points, nil - } - - bucketMap := make(map[int64]EquityPoint) - bucketKeys := make([]int64, 0) - for _, pt := range points { - bucket := (pt.Timestamp / durMs) * durMs - if _, exists := bucketMap[bucket]; !exists { - bucketKeys = append(bucketKeys, bucket) - } - bucketPoint := pt - bucketPoint.Timestamp = bucket - bucketMap[bucket] = bucketPoint - } - - sort.Slice(bucketKeys, func(i, j int) bool { - return bucketKeys[i] < bucketKeys[j] - }) - - resampled := make([]EquityPoint, 0, len(bucketKeys)) - for _, key := range bucketKeys { - resampled = append(resampled, bucketMap[key]) - } - - return resampled, nil -} - -// LimitEquityPoints limits the number of data points within a given range (uniform sampling). -func LimitEquityPoints(points []EquityPoint, limit int) []EquityPoint { - if limit <= 0 || len(points) <= limit { - return points - } - - step := float64(len(points)) / float64(limit) - result := make([]EquityPoint, 0, limit) - for i := 0; i < limit; i++ { - idx := int(math.Round(step * float64(i))) - if idx >= len(points) { - idx = len(points) - 1 - } - result = append(result, points[idx]) - } - - return result -} - -// LimitTradeEvents applies uniform sampling to trade events. -func LimitTradeEvents(events []TradeEvent, limit int) []TradeEvent { - if limit <= 0 || len(events) <= limit { - return events - } - - step := float64(len(events)) / float64(limit) - result := make([]TradeEvent, 0, limit) - for i := 0; i < limit; i++ { - idx := int(math.Round(step * float64(i))) - if idx >= len(events) { - idx = len(events) - 1 - } - result = append(result, events[idx]) - } - return result -} - -// AlignEquityTimestamps ensures timestamps are sorted in ascending order. -func AlignEquityTimestamps(points []EquityPoint) []EquityPoint { - sort.Slice(points, func(i, j int) bool { - return points[i].Timestamp < points[j].Timestamp - }) - return points -} diff --git a/backtest/lock.go b/backtest/lock.go deleted file mode 100644 index 6f26ed08..00000000 --- a/backtest/lock.go +++ /dev/null @@ -1,100 +0,0 @@ -package backtest - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "time" -) - -const ( - lockFileName = "lock" - lockHeartbeatInterval = 2 * time.Second - lockStaleAfter = 10 * time.Second -) - -// RunLockInfo represents the lock file structure for a backtest run. -type RunLockInfo struct { - RunID string `json:"run_id"` - PID int `json:"pid"` - Host string `json:"host"` - StartedAt time.Time `json:"started_at"` - LastHeartbeat time.Time `json:"last_heartbeat"` -} - -func lockFilePath(runID string) string { - return filepath.Join(runDir(runID), lockFileName) -} - -func loadRunLock(runID string) (*RunLockInfo, error) { - path := lockFilePath(runID) - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var info RunLockInfo - if err := json.Unmarshal(data, &info); err != nil { - return nil, err - } - return &info, nil -} - -func saveRunLock(info *RunLockInfo) error { - if info == nil { - return fmt.Errorf("lock info nil") - } - return writeJSONAtomic(lockFilePath(info.RunID), info) -} - -func deleteRunLock(runID string) error { - err := os.Remove(lockFilePath(runID)) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - return nil -} - -func lockIsStale(info *RunLockInfo) bool { - if info == nil { - return true - } - return time.Since(info.LastHeartbeat) > lockStaleAfter -} - -func acquireRunLock(runID string) (*RunLockInfo, error) { - if err := ensureRunDir(runID); err != nil { - return nil, err - } - - if existing, err := loadRunLock(runID); err == nil { - if !lockIsStale(existing) { - return nil, fmt.Errorf("run %s is locked by pid %d", runID, existing.PID) - } - } else if err != nil && !errors.Is(err, os.ErrNotExist) { - return nil, err - } - - host, _ := os.Hostname() - info := &RunLockInfo{ - RunID: runID, - PID: os.Getpid(), - Host: host, - StartedAt: time.Now().UTC(), - LastHeartbeat: time.Now().UTC(), - } - - if err := saveRunLock(info); err != nil { - return nil, err - } - return info, nil -} - -func updateRunLockHeartbeat(info *RunLockInfo) error { - if info == nil { - return fmt.Errorf("lock info nil") - } - info.LastHeartbeat = time.Now().UTC() - return saveRunLock(info) -} diff --git a/backtest/manager.go b/backtest/manager.go deleted file mode 100644 index 34ec9ec2..00000000 --- a/backtest/manager.go +++ /dev/null @@ -1,493 +0,0 @@ -package backtest - -import ( - "context" - "errors" - "fmt" - "nofx/logger" - "os" - "sort" - "strings" - "sync" - - "nofx/mcp" - "nofx/store" -) - -type Manager struct { - mu sync.RWMutex - runners map[string]*Runner - metadata map[string]*RunMetadata - cancels map[string]context.CancelFunc - mcpClient mcp.AIClient - aiResolver AIConfigResolver -} - -type AIConfigResolver func(*BacktestConfig) error - -func NewManager(defaultClient mcp.AIClient) *Manager { - return &Manager{ - runners: make(map[string]*Runner), - metadata: make(map[string]*RunMetadata), - cancels: make(map[string]context.CancelFunc), - mcpClient: defaultClient, - } -} - -func (m *Manager) SetAIResolver(resolver AIConfigResolver) { - m.mu.Lock() - defer m.mu.Unlock() - m.aiResolver = resolver -} - -func (m *Manager) Start(ctx context.Context, cfg BacktestConfig) (*Runner, error) { - if err := cfg.Validate(); err != nil { - return nil, err - } - if err := m.resolveAIConfig(&cfg); err != nil { - return nil, err - } - if ctx == nil { - ctx = context.Background() - } - - m.mu.Lock() - if existing, ok := m.runners[cfg.RunID]; ok { - state := existing.Status() - if state == RunStateRunning || state == RunStatePaused { - m.mu.Unlock() - return nil, fmt.Errorf("run %s is already active", cfg.RunID) - } - } - m.mu.Unlock() - - persistCfg := cfg - persistCfg.AICfg.APIKey = "" - if err := SaveConfig(cfg.RunID, &persistCfg); err != nil { - return nil, err - } - - runner, err := NewRunner(cfg, m.client()) - if err != nil { - return nil, err - } - - runCtx, cancel := context.WithCancel(ctx) - - m.mu.Lock() - if _, exists := m.runners[cfg.RunID]; exists { - m.mu.Unlock() - cancel() - return nil, fmt.Errorf("run %s is already active", cfg.RunID) - } - m.runners[cfg.RunID] = runner - m.cancels[cfg.RunID] = cancel - meta := runner.CurrentMetadata() - m.metadata[cfg.RunID] = meta - m.mu.Unlock() - - if err := runner.Start(runCtx); err != nil { - cancel() - m.mu.Lock() - delete(m.runners, cfg.RunID) - delete(m.cancels, cfg.RunID) - delete(m.metadata, cfg.RunID) - m.mu.Unlock() - runner.releaseLock() - return nil, err - } - - m.storeMetadata(cfg.RunID, meta) - m.launchWatcher(cfg.RunID, runner) - return runner, nil -} - -func (m *Manager) client() mcp.AIClient { - if m.mcpClient != nil { - return m.mcpClient - } - return mcp.New() -} - -func (m *Manager) GetRunner(runID string) (*Runner, bool) { - m.mu.RLock() - runner, ok := m.runners[runID] - m.mu.RUnlock() - return runner, ok -} - -func (m *Manager) ListRuns() ([]*RunMetadata, error) { - m.mu.RLock() - localCopy := make(map[string]*RunMetadata, len(m.metadata)) - for k, v := range m.metadata { - localCopy[k] = v - } - m.mu.RUnlock() - - runIDs, err := LoadRunIDs() - if err != nil { - return nil, err - } - - ordered := make([]string, 0, len(runIDs)) - if entries, err := listIndexEntries(); err == nil { - seen := make(map[string]bool, len(runIDs)) - for _, entry := range entries { - if contains(runIDs, entry.RunID) { - ordered = append(ordered, entry.RunID) - seen[entry.RunID] = true - } - } - for _, id := range runIDs { - if !seen[id] { - ordered = append(ordered, id) - } - } - } else { - ordered = append(ordered, runIDs...) - } - - metas := make([]*RunMetadata, 0, len(runIDs)) - for _, runID := range ordered { - if meta, ok := localCopy[runID]; ok { - metas = append(metas, meta) - continue - } - meta, err := LoadRunMetadata(runID) - if err == nil { - metas = append(metas, meta) - } - } - - sort.Slice(metas, func(i, j int) bool { - return metas[i].UpdatedAt.After(metas[j].UpdatedAt) - }) - - return metas, nil -} - -func contains(list []string, target string) bool { - for _, item := range list { - if item == target { - return true - } - } - return false -} - -func (m *Manager) Pause(runID string) error { - runner, ok := m.GetRunner(runID) - if !ok { - return fmt.Errorf("run %s not found", runID) - } - runner.Pause() - m.refreshMetadata(runID) - return nil -} - -func (m *Manager) Resume(runID string) error { - if runID == "" { - return fmt.Errorf("run_id is required") - } - - runner, ok := m.GetRunner(runID) - if ok { - runner.Resume() - m.refreshMetadata(runID) - return nil - } - - cfg, err := LoadConfig(runID) - if err != nil { - return err - } - cfgCopy := *cfg - if err := cfgCopy.Validate(); err != nil { - return err - } - if err := m.resolveAIConfig(&cfgCopy); err != nil { - return err - } - - restored, err := NewRunner(cfgCopy, m.client()) - if err != nil { - return err - } - if err := restored.RestoreFromCheckpoint(); err != nil { - return err - } - - ctx, cancel := context.WithCancel(context.Background()) - - m.mu.Lock() - if _, exists := m.runners[runID]; exists { - m.mu.Unlock() - cancel() - return fmt.Errorf("run %s is already active", runID) - } - m.runners[runID] = restored - m.cancels[runID] = cancel - m.metadata[runID] = restored.CurrentMetadata() - m.mu.Unlock() - - if err := restored.Start(ctx); err != nil { - cancel() - m.mu.Lock() - delete(m.runners, runID) - delete(m.cancels, runID) - delete(m.metadata, runID) - m.mu.Unlock() - restored.releaseLock() - return err - } - - m.storeMetadata(runID, restored.CurrentMetadata()) - m.launchWatcher(runID, restored) - return nil -} - -func (m *Manager) Stop(runID string) error { - runner, ok := m.GetRunner(runID) - if ok { - runner.Stop() - err := runner.Wait() - m.refreshMetadata(runID) - return err - } - meta, err := m.LoadMetadata(runID) - if err != nil { - return err - } - if meta.State == RunStateStopped || meta.State == RunStateCompleted { - return nil - } - meta.State = RunStateStopped - m.storeMetadata(runID, meta) - return nil -} - -func (m *Manager) Wait(runID string) error { - runner, ok := m.GetRunner(runID) - if !ok { - return fmt.Errorf("run %s not found", runID) - } - err := runner.Wait() - m.refreshMetadata(runID) - return err -} - -func (m *Manager) UpdateLabel(runID, label string) (*RunMetadata, error) { - meta, err := m.LoadMetadata(runID) - if err != nil { - return nil, err - } - clean := strings.TrimSpace(label) - metaCopy := *meta - metaCopy.Label = clean - m.storeMetadata(runID, &metaCopy) - return &metaCopy, nil -} - -func (m *Manager) Delete(runID string) error { - runner, ok := m.GetRunner(runID) - if ok { - runner.Stop() - _ = runner.Wait() - } - m.mu.Lock() - if cancel, ok := m.cancels[runID]; ok { - cancel() - delete(m.cancels, runID) - } - delete(m.runners, runID) - delete(m.metadata, runID) - m.mu.Unlock() - if err := removeFromRunIndex(runID); err != nil { - return err - } - if err := deleteRunLock(runID); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - return nil -} - -func (m *Manager) LoadMetadata(runID string) (*RunMetadata, error) { - runner, ok := m.GetRunner(runID) - if ok { - meta := runner.CurrentMetadata() - m.storeMetadata(runID, meta) - return meta, nil - } - meta, err := LoadRunMetadata(runID) - if err != nil { - return nil, err - } - m.storeMetadata(runID, meta) - return meta, nil -} - -func (m *Manager) LoadEquity(runID string, timeframe string, limit int) ([]EquityPoint, error) { - points, err := LoadEquityPoints(runID) - if err != nil { - return nil, err - } - if timeframe != "" { - points, err = ResampleEquity(points, timeframe) - if err != nil { - return nil, err - } - } - points = AlignEquityTimestamps(points) - points = LimitEquityPoints(points, limit) - return points, nil -} - -func (m *Manager) LoadTrades(runID string, limit int) ([]TradeEvent, error) { - events, err := LoadTradeEvents(runID) - if err != nil { - return nil, err - } - return LimitTradeEvents(events, limit), nil -} - -func (m *Manager) GetMetrics(runID string) (*Metrics, error) { - return LoadMetrics(runID) -} - -func (m *Manager) Cleanup(runID string) { - m.mu.Lock() - defer m.mu.Unlock() - delete(m.runners, runID) - if cancel, ok := m.cancels[runID]; ok { - cancel() - delete(m.cancels, runID) - } -} - -func (m *Manager) Status(runID string) *StatusPayload { - runner, ok := m.GetRunner(runID) - if !ok { - return nil - } - payload := runner.StatusPayload() - m.storeMetadata(runID, runner.CurrentMetadata()) - return &payload -} - -func (m *Manager) launchWatcher(runID string, runner *Runner) { - go func() { - if err := runner.Wait(); err != nil { - logger.Infof("backtest run %s finished with error: %v", runID, err) - } - runner.PersistMetadata() - meta := runner.CurrentMetadata() - m.storeMetadata(runID, meta) - - m.mu.Lock() - if cancel, ok := m.cancels[runID]; ok { - cancel() - delete(m.cancels, runID) - } - delete(m.runners, runID) - m.mu.Unlock() - }() -} - -func (m *Manager) refreshMetadata(runID string) { - runner, ok := m.GetRunner(runID) - if !ok { - return - } - meta := runner.CurrentMetadata() - m.storeMetadata(runID, meta) -} - -func (m *Manager) storeMetadata(runID string, meta *RunMetadata) { - if meta == nil { - return - } - m.mu.Lock() - if existing, ok := m.metadata[runID]; ok { - if meta.Label == "" && existing.Label != "" { - meta.Label = existing.Label - } - if meta.LastError == "" && existing.LastError != "" { - meta.LastError = existing.LastError - } - } - m.metadata[runID] = meta - m.mu.Unlock() - _ = SaveRunMetadata(meta) - if err := updateRunIndex(meta, nil); err != nil { - logger.Infof("failed to update run index for %s: %v", runID, err) - } -} - -func (m *Manager) resolveAIConfig(cfg *BacktestConfig) error { - if cfg == nil { - return fmt.Errorf("ai config missing") - } - provider := strings.TrimSpace(cfg.AICfg.Provider) - apiKey := strings.TrimSpace(cfg.AICfg.APIKey) - if provider != "" && !strings.EqualFold(provider, "inherit") && apiKey != "" { - return nil - } - - m.mu.RLock() - resolver := m.aiResolver - m.mu.RUnlock() - if resolver == nil { - if apiKey == "" { - return fmt.Errorf("AI configuration missing key and no resolver configured") - } - return nil - } - return resolver(cfg) -} - -func (m *Manager) GetTrace(runID string, cycle int) (*store.DecisionRecord, error) { - return LoadDecisionTrace(runID, cycle) -} - -func (m *Manager) ExportRun(runID string) (string, error) { - return CreateRunExport(runID) -} - -// RestoreRuns scans the backtests directory and restores metadata for existing runs (service restart scenario). -func (m *Manager) RestoreRuns() error { - runIDs, err := LoadRunIDs() - if err != nil { - return err - } - for _, runID := range runIDs { - meta, err := LoadRunMetadata(runID) - if err != nil { - logger.Infof("skip run %s: %v", runID, err) - continue - } - if meta.State == RunStateRunning { - lock, err := loadRunLock(runID) - if err != nil || lockIsStale(lock) { - if err := deleteRunLock(runID); err != nil { - logger.Infof("failed to cleanup lock for %s: %v", runID, err) - } - meta.State = RunStatePaused - if err := SaveRunMetadata(meta); err != nil { - logger.Infof("failed to mark %s paused: %v", runID, err) - } - } - } - m.mu.Lock() - m.metadata[runID] = meta - m.mu.Unlock() - if err := updateRunIndex(meta, nil); err != nil { - logger.Infof("failed to sync index for %s: %v", runID, err) - } - } - return nil -} - -// RestoreRunsFromDisk retains the old method name for backward compatibility. -func (m *Manager) RestoreRunsFromDisk() error { - return m.RestoreRuns() -} diff --git a/backtest/metrics.go b/backtest/metrics.go deleted file mode 100644 index a7aac519..00000000 --- a/backtest/metrics.go +++ /dev/null @@ -1,239 +0,0 @@ -package backtest - -import ( - "fmt" - "math" - "strings" -) - -// CalculateMetrics reads existing logs and calculates summary metrics. state is optional, used to supplement information not yet persisted. -func CalculateMetrics(runID string, cfg *BacktestConfig, state *BacktestState) (*Metrics, error) { - if cfg == nil { - return nil, fmt.Errorf("config is nil") - } - - points, err := LoadEquityPoints(runID) - if err != nil { - return nil, fmt.Errorf("load equity points: %w", err) - } - - events, err := LoadTradeEvents(runID) - if err != nil { - return nil, fmt.Errorf("load trade events: %w", err) - } - - metrics := &Metrics{ - SymbolStats: make(map[string]SymbolMetrics), - } - - metrics.Liquidated = determineLiquidation(events, state) - - initialBalance := cfg.InitialBalance - if initialBalance <= 0 { - initialBalance = 1 - } - - lastEquity := initialBalance - if len(points) > 0 && points[len(points)-1].Equity > 0 { - lastEquity = points[len(points)-1].Equity - } else if state != nil && state.Equity > 0 { - lastEquity = state.Equity - } - metrics.TotalReturnPct = ((lastEquity - initialBalance) / initialBalance) * 100 - - metrics.MaxDrawdownPct = maxDrawdown(points, state) - metrics.SharpeRatio = sharpeRatio(points) - - fillTradeMetrics(metrics, events) - - return metrics, nil -} - -func determineLiquidation(events []TradeEvent, state *BacktestState) bool { - if state != nil && state.Liquidated { - return true - } - for i := len(events) - 1; i >= 0; i-- { - if events[i].LiquidationFlag { - return true - } - } - return false -} - -func maxDrawdown(points []EquityPoint, state *BacktestState) float64 { - if len(points) == 0 { - if state != nil { - return state.MaxDrawdownPct - } - return 0 - } - peak := points[0].Equity - if peak <= 0 { - peak = 1 - } - maxDD := 0.0 - for _, pt := range points { - if pt.Equity > peak { - peak = pt.Equity - } - if peak <= 0 { - continue - } - dd := (peak - pt.Equity) / peak * 100 - if dd > maxDD { - maxDD = dd - } - } - if state != nil && state.MaxDrawdownPct > maxDD { - maxDD = state.MaxDrawdownPct - } - return maxDD -} - -// sharpeRatio calculates the Sharpe ratio from equity points. -// Uses sample standard deviation (n-1) and annualizes assuming ~252 trading days. -// Returns math.NaN() for edge cases (insufficient data, zero variance). -func sharpeRatio(points []EquityPoint) float64 { - // Need at least 10 data points for meaningful Sharpe calculation - const minDataPoints = 10 - if len(points) < minDataPoints { - return 0 - } - - returns := make([]float64, 0, len(points)-1) - prev := points[0].Equity - for i := 1; i < len(points); i++ { - curr := points[i].Equity - if prev <= 0 { - prev = curr - continue - } - ret := (curr - prev) / prev - returns = append(returns, ret) - prev = curr - } - if len(returns) < minDataPoints-1 { - return 0 - } - - // Calculate mean return - mean := 0.0 - for _, r := range returns { - mean += r - } - mean /= float64(len(returns)) - - // Calculate sample variance (using n-1 for unbiased estimator) - variance := 0.0 - for _, r := range returns { - diff := r - mean - variance += diff * diff - } - if len(returns) > 1 { - variance /= float64(len(returns) - 1) - } - - std := math.Sqrt(variance) - if std < 1e-10 { - // Zero or near-zero volatility - return 0 instead of infinity/NaN - return 0 - } - - // Calculate Sharpe ratio (assuming risk-free rate = 0 for crypto) - // Annualize by multiplying by sqrt(periods per year) - // Assuming each equity point represents ~1 hour, we have ~8760 periods/year - // For conservative estimate, use sqrt(252) as if daily returns - periodsPerYear := 252.0 - annualizationFactor := math.Sqrt(periodsPerYear) - - sharpe := (mean / std) * annualizationFactor - return sharpe -} - -func fillTradeMetrics(metrics *Metrics, events []TradeEvent) { - if metrics == nil { - return - } - - totalTrades := 0 - winTrades := 0 - lossTrades := 0 - totalWinAmount := 0.0 - totalLossAmount := 0.0 - - for _, evt := range events { - include := evt.LiquidationFlag || strings.HasPrefix(evt.Action, "close") - if evt.RealizedPnL != 0 { - include = true - } - if !include { - continue - } - totalTrades++ - - stats := metrics.SymbolStats[evt.Symbol] - stats.TotalTrades++ - stats.TotalPnL += evt.RealizedPnL - - if evt.RealizedPnL > 0 { - winTrades++ - totalWinAmount += evt.RealizedPnL - stats.WinningTrades++ - } else if evt.RealizedPnL < 0 { - lossTrades++ - totalLossAmount += -evt.RealizedPnL - stats.LosingTrades++ - } - - metrics.SymbolStats[evt.Symbol] = stats - } - - metrics.Trades = totalTrades - if totalTrades > 0 { - metrics.WinRate = (float64(winTrades) / float64(totalTrades)) * 100 - } - if winTrades > 0 { - metrics.AvgWin = totalWinAmount / float64(winTrades) - } - if lossTrades > 0 { - metrics.AvgLoss = -(totalLossAmount / float64(lossTrades)) - } - if totalLossAmount > 0 { - metrics.ProfitFactor = totalWinAmount / totalLossAmount - } else if totalWinAmount > 0 { - // No losses but have wins - use a high but reasonable cap - metrics.ProfitFactor = 100.0 - } - - bestSymbol := "" - bestPnL := math.Inf(-1) - worstSymbol := "" - worstPnL := math.Inf(1) - - for symbol, stats := range metrics.SymbolStats { - if stats.TotalTrades > 0 { - if stats.TotalPnL > bestPnL { - bestPnL = stats.TotalPnL - bestSymbol = symbol - } - if stats.TotalPnL < worstPnL { - worstPnL = stats.TotalPnL - worstSymbol = symbol - } - - stats.AvgPnL = stats.TotalPnL / float64(stats.TotalTrades) - stats.WinRate = (float64(stats.WinningTrades) / float64(stats.TotalTrades)) * 100 - } - metrics.SymbolStats[symbol] = stats - } - - metrics.BestSymbol = bestSymbol - if math.IsInf(bestPnL, -1) { - metrics.BestSymbol = "" - } - metrics.WorstSymbol = worstSymbol - if math.IsInf(worstPnL, 1) { - metrics.WorstSymbol = "" - } -} diff --git a/backtest/persistence_db.go b/backtest/persistence_db.go deleted file mode 100644 index d494ed65..00000000 --- a/backtest/persistence_db.go +++ /dev/null @@ -1,40 +0,0 @@ -package backtest - -import ( - "database/sql" - "fmt" - "strings" -) - -var persistenceDB *sql.DB -var dbIsPostgres bool - -// UseDatabase enables database-backed persistence for all backtest storage operations. -// If isPostgres is true, queries will use $1, $2... placeholders instead of ? -func UseDatabase(db *sql.DB) { - persistenceDB = db -} - -// UseDatabaseWithType enables database-backed persistence with explicit type. -func UseDatabaseWithType(db *sql.DB, isPostgres bool) { - persistenceDB = db - dbIsPostgres = isPostgres -} - -func usingDB() bool { - return persistenceDB != nil -} - -// convertQuery converts ? placeholders to $1, $2, etc. for PostgreSQL -func convertQuery(query string) string { - if !dbIsPostgres { - return query - } - result := query - index := 1 - for strings.Contains(result, "?") { - result = strings.Replace(result, "?", fmt.Sprintf("$%d", index), 1) - index++ - } - return result -} diff --git a/backtest/registry.go b/backtest/registry.go deleted file mode 100644 index 9c0330e6..00000000 --- a/backtest/registry.go +++ /dev/null @@ -1,160 +0,0 @@ -package backtest - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "sort" - "time" -) - -const runIndexFile = "index.json" - -type RunIndexEntry struct { - RunID string `json:"run_id"` - State RunState `json:"state"` - Symbols []string `json:"symbols"` - DecisionTF string `json:"decision_tf"` - StartTS int64 `json:"start_ts"` - EndTS int64 `json:"end_ts"` - EquityLast float64 `json:"equity_last"` - MaxDrawdownPct float64 `json:"max_dd_pct"` - CreatedAtISO string `json:"created_at"` - UpdatedAtISO string `json:"updated_at"` -} - -type RunIndex struct { - Runs map[string]RunIndexEntry `json:"runs"` - UpdatedAt string `json:"updated_at"` -} - -func runIndexPath() string { - return filepath.Join(backtestsRootDir, runIndexFile) -} - -func loadRunIndex() (*RunIndex, error) { - if usingDB() { - entries, err := listIndexEntriesDB() - if err != nil { - return nil, err - } - idx := &RunIndex{ - Runs: make(map[string]RunIndexEntry), - UpdatedAt: time.Now().UTC().Format(time.RFC3339), - } - for _, entry := range entries { - idx.Runs[entry.RunID] = entry - } - return idx, nil - } - path := runIndexPath() - data, err := os.ReadFile(path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return &RunIndex{Runs: make(map[string]RunIndexEntry)}, nil - } - return nil, err - } - var idx RunIndex - if err := json.Unmarshal(data, &idx); err != nil { - return nil, err - } - if idx.Runs == nil { - idx.Runs = make(map[string]RunIndexEntry) - } - return &idx, nil -} - -func saveRunIndex(idx *RunIndex) error { - if usingDB() { - return nil - } - if idx == nil { - return fmt.Errorf("index is nil") - } - idx.UpdatedAt = time.Now().UTC().Format(time.RFC3339) - return writeJSONAtomic(runIndexPath(), idx) -} - -func updateRunIndex(meta *RunMetadata, cfg *BacktestConfig) error { - if usingDB() { - enforceRetention(maxCompletedRuns) - return nil - } - if meta == nil { - return fmt.Errorf("meta nil") - } - if cfg == nil { - var err error - cfg, err = LoadConfig(meta.RunID) - if err != nil { - return err - } - } - - idx, err := loadRunIndex() - if err != nil { - return err - } - - entry := RunIndexEntry{ - RunID: meta.RunID, - State: meta.State, - Symbols: append([]string(nil), cfg.Symbols...), - DecisionTF: meta.Summary.DecisionTF, - StartTS: cfg.StartTS, - EndTS: cfg.EndTS, - EquityLast: meta.Summary.EquityLast, - MaxDrawdownPct: meta.Summary.MaxDrawdownPct, - CreatedAtISO: meta.CreatedAt.Format(time.RFC3339), - UpdatedAtISO: meta.UpdatedAt.Format(time.RFC3339), - } - - if idx.Runs == nil { - idx.Runs = make(map[string]RunIndexEntry) - } - idx.Runs[meta.RunID] = entry - if err := saveRunIndex(idx); err != nil { - return err - } - enforceRetention(maxCompletedRuns) - return nil -} - -func removeFromRunIndex(runID string) error { - if usingDB() { - if err := deleteRunDB(runID); err != nil { - return err - } - return os.RemoveAll(runDir(runID)) - } - idx, err := loadRunIndex() - if err != nil { - return err - } - if idx.Runs == nil { - return nil - } - delete(idx.Runs, runID) - return saveRunIndex(idx) -} - -func listIndexEntries() ([]RunIndexEntry, error) { - if usingDB() { - return listIndexEntriesDB() - } - idx, err := loadRunIndex() - if err != nil { - return nil, err - } - entries := make([]RunIndexEntry, 0, len(idx.Runs)) - for _, entry := range idx.Runs { - entries = append(entries, entry) - } - sort.Slice(entries, func(i, j int) bool { - return entries[i].UpdatedAtISO > entries[j].UpdatedAtISO - }) - return entries, nil -} diff --git a/backtest/retention.go b/backtest/retention.go deleted file mode 100644 index 49ec3542..00000000 --- a/backtest/retention.go +++ /dev/null @@ -1,101 +0,0 @@ -package backtest - -import ( - "nofx/logger" - "os" - "sort" - "time" -) - -const maxCompletedRuns = 100 - -func enforceRetention(maxRuns int) { - if maxRuns <= 0 { - return - } - if usingDB() { - enforceRetentionDB(maxRuns) - return - } - idx, err := loadRunIndex() - if err != nil { - return - } - - type wrapped struct { - entry RunIndexEntry - updated time.Time - } - finalStates := map[RunState]bool{ - RunStateCompleted: true, - RunStateStopped: true, - RunStateFailed: true, - RunStateLiquidated: true, - } - - candidates := make([]wrapped, 0) - for _, entry := range idx.Runs { - if !finalStates[entry.State] { - continue - } - ts, err := time.Parse(time.RFC3339, entry.UpdatedAtISO) - if err != nil { - ts = time.Now() - } - candidates = append(candidates, wrapped{entry: entry, updated: ts}) - } - if len(candidates) <= maxRuns { - return - } - - sort.Slice(candidates, func(i, j int) bool { - return candidates[i].updated.Before(candidates[j].updated) - }) - - toRemove := len(candidates) - maxRuns - for i := 0; i < toRemove; i++ { - runID := candidates[i].entry.RunID - if err := os.RemoveAll(runDir(runID)); err != nil { - logger.Infof("failed to prune run %s: %v", runID, err) - continue - } - delete(idx.Runs, runID) - } - if err := saveRunIndex(idx); err != nil { - logger.Infof("failed to save index after pruning: %v", err) - } -} - -func enforceRetentionDB(maxRuns int) { - finalStates := []RunState{ - RunStateCompleted, - RunStateStopped, - RunStateFailed, - RunStateLiquidated, - } - query := convertQuery(` - SELECT run_id FROM backtest_runs - WHERE state IN (?, ?, ?, ?) - ORDER BY updated_at DESC - OFFSET ? - `) - rows, err := persistenceDB.Query(query, - finalStates[0], finalStates[1], finalStates[2], finalStates[3], maxRuns) - if err != nil { - return - } - defer rows.Close() - for rows.Next() { - var runID string - if err := rows.Scan(&runID); err != nil { - continue - } - if err := deleteRunDB(runID); err != nil { - logger.Infof("failed to remove run %s: %v", runID, err) - continue - } - if err := os.RemoveAll(runDir(runID)); err != nil { - logger.Infof("failed to remove run dir %s: %v", runID, err) - } - } -} diff --git a/backtest/runner.go b/backtest/runner.go deleted file mode 100644 index 7b31c97f..00000000 --- a/backtest/runner.go +++ /dev/null @@ -1,341 +0,0 @@ -package backtest - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "time" - - "nofx/kernel" - "nofx/logger" - "nofx/mcp" -) - -var ( - errBacktestCompleted = errors.New("backtest completed") - errLiquidated = errors.New("account liquidated") -) - -const ( - metricsWriteInterval = 5 * time.Second - aiDecisionMaxRetries = 3 -) - -// Runner encapsulates the lifecycle of a single backtest run. -type Runner struct { - cfg BacktestConfig - feed *DataFeed - account *BacktestAccount - strategyEngine *kernel.StrategyEngine - - decisionLogDir string - mcpClient mcp.AIClient - - statusMu sync.RWMutex - status RunState - - stateMu sync.RWMutex - state *BacktestState - - pauseCh chan struct{} - resumeCh chan struct{} - stopCh chan struct{} - doneCh chan struct{} - - err error - errMu sync.RWMutex - lastError string - lastCheckpoint time.Time - createdAt time.Time - lastMetricsWrite time.Time - - aiCache *AICache - cachePath string - - lockInfo *RunLockInfo - lockStop chan struct{} - lockStopOnce sync.Once // Ensures lockStop is closed only once -} - -// NewRunner constructs a backtest runner. -func NewRunner(cfg BacktestConfig, mcpClient mcp.AIClient) (*Runner, error) { - if err := ensureRunDir(cfg.RunID); err != nil { - return nil, err - } - - client, err := configureMCPClient(cfg, mcpClient) - if err != nil { - return nil, err - } - - feed, err := NewDataFeed(cfg) - if err != nil { - return nil, err - } - - if err := os.MkdirAll(decisionLogDir(cfg.RunID), 0o755); err != nil { - return nil, err - } - - dLogDir := decisionLogDir(cfg.RunID) - account := NewBacktestAccount(cfg.InitialBalance, cfg.FeeBps, cfg.SlippageBps) - - createdAt := time.Now().UTC() - state := &BacktestState{ - Positions: make(map[string]PositionSnapshot), - Cash: account.Cash(), - Equity: cfg.InitialBalance, - UnrealizedPnL: 0, - RealizedPnL: 0, - MaxEquity: cfg.InitialBalance, - MinEquity: cfg.InitialBalance, - MaxDrawdownPct: 0, - LastUpdate: createdAt, - } - - var ( - aiCache *AICache - cachePath string - ) - if cfg.CacheAI || cfg.ReplayOnly || cfg.SharedAICachePath != "" { - cachePath = cfg.SharedAICachePath - if cachePath == "" { - cachePath = filepath.Join(runDir(cfg.RunID), "ai_cache.json") - } - cache, err := LoadAICache(cachePath) - if err != nil { - return nil, fmt.Errorf("load ai cache: %w", err) - } - aiCache = cache - } - - // Create strategy engine from backtest config for unified prompt generation - strategyConfig := cfg.ToStrategyConfig() - strategyEngine := kernel.NewStrategyEngine(strategyConfig) - - r := &Runner{ - cfg: cfg, - feed: feed, - account: account, - strategyEngine: strategyEngine, - decisionLogDir: dLogDir, - mcpClient: client, - status: RunStateCreated, - state: state, - pauseCh: make(chan struct{}, 1), - resumeCh: make(chan struct{}, 1), - stopCh: make(chan struct{}, 1), - doneCh: make(chan struct{}), - createdAt: createdAt, - aiCache: aiCache, - cachePath: cachePath, - } - - if err := r.initLock(); err != nil { - return nil, err - } - - return r, nil -} - -func (r *Runner) initLock() error { - if r.cfg.RunID == "" { - return fmt.Errorf("run_id required for lock") - } - info, err := acquireRunLock(r.cfg.RunID) - if err != nil { - return err - } - r.lockInfo = info - r.lockStop = make(chan struct{}) - go r.lockHeartbeatLoop() - return nil -} - -func (r *Runner) lockHeartbeatLoop() { - ticker := time.NewTicker(lockHeartbeatInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - if err := updateRunLockHeartbeat(r.lockInfo); err != nil { - logger.Infof("failed to update lock heartbeat for %s: %v", r.cfg.RunID, err) - } - case <-r.lockStop: - return - } - } -} - -func (r *Runner) releaseLock() { - // Use sync.Once to ensure channel is closed exactly once, preventing panic on double-close - r.lockStopOnce.Do(func() { - if r.lockStop != nil { - close(r.lockStop) - } - }) - if err := deleteRunLock(r.cfg.RunID); err != nil { - logger.Infof("failed to release lock for %s: %v", r.cfg.RunID, err) - } - r.lockInfo = nil -} - -// Start launches the backtest loop. -func (r *Runner) Start(ctx context.Context) error { - r.statusMu.Lock() - if r.status != RunStateCreated && r.status != RunStatePaused { - r.statusMu.Unlock() - return fmt.Errorf("cannot start runner in state %s", r.status) - } - r.status = RunStateRunning - r.statusMu.Unlock() - - go r.loop(ctx) - return nil -} - -// PersistMetadata writes the current snapshot to run.json. -func (r *Runner) PersistMetadata() { - r.persistMetadata() -} - -func (r *Runner) setLastError(err error) { - r.errMu.Lock() - defer r.errMu.Unlock() - if err == nil { - r.lastError = "" - return - } - r.lastError = err.Error() -} - -func (r *Runner) lastErrorString() string { - r.errMu.RLock() - defer r.errMu.RUnlock() - return r.lastError -} - -// CurrentMetadata returns the metadata corresponding to the current in-memory state. -func (r *Runner) CurrentMetadata() *RunMetadata { - state := r.snapshotState() - meta := r.buildMetadata(state, r.Status()) - meta.CreatedAt = r.createdAt - meta.UpdatedAt = state.LastUpdate - return meta -} - -func (r *Runner) Pause() { - select { - case r.pauseCh <- struct{}{}: - default: - } -} - -func (r *Runner) Resume() { - select { - case r.resumeCh <- struct{}{}: - default: - } -} - -func (r *Runner) Stop() { - select { - case r.stopCh <- struct{}{}: - default: - } -} - -func (r *Runner) Wait() error { - <-r.doneCh - r.statusMu.RLock() - defer r.statusMu.RUnlock() - return r.err -} - -// Status returns the current run state. -func (r *Runner) Status() RunState { - r.statusMu.RLock() - defer r.statusMu.RUnlock() - return r.status -} - -// StatusPayload builds the status response for the API. -func (r *Runner) StatusPayload() StatusPayload { - snapshot := r.snapshotState() - progress := progressPercent(snapshot, r.cfg) - - // Build position statuses with unrealized P&L - positions := make([]PositionStatus, 0, len(snapshot.Positions)) - for _, pos := range snapshot.Positions { - if pos.Quantity <= 0 { - continue - } - // Get mark price from feed if available - markPrice := pos.AvgPrice // fallback to entry price - if r.feed != nil && snapshot.BarTimestamp > 0 { - if md, _, err := r.feed.BuildMarketData(snapshot.BarTimestamp); err == nil { - if data, ok := md[pos.Symbol]; ok { - markPrice = data.CurrentPrice - } - } - } - - // Calculate unrealized P&L - var unrealizedPnL float64 - if pos.Side == "long" { - unrealizedPnL = (markPrice - pos.AvgPrice) * pos.Quantity - } else { - unrealizedPnL = (pos.AvgPrice - markPrice) * pos.Quantity - } - - // Calculate P&L percentage based on margin - pnlPct := 0.0 - if pos.MarginUsed > 0 { - pnlPct = (unrealizedPnL / pos.MarginUsed) * 100 - } - - positions = append(positions, PositionStatus{ - Symbol: pos.Symbol, - Side: pos.Side, - Quantity: pos.Quantity, - EntryPrice: pos.AvgPrice, - MarkPrice: markPrice, - Leverage: pos.Leverage, - UnrealizedPnL: unrealizedPnL, - UnrealizedPnLPct: pnlPct, - MarginUsed: pos.MarginUsed, - }) - } - - payload := StatusPayload{ - RunID: r.cfg.RunID, - State: r.Status(), - ProgressPct: progress, - ProcessedBars: snapshot.BarIndex, - CurrentTime: snapshot.BarTimestamp, - DecisionCycle: snapshot.DecisionCycle, - Equity: snapshot.Equity, - UnrealizedPnL: snapshot.UnrealizedPnL, - RealizedPnL: snapshot.RealizedPnL, - Positions: positions, - Note: snapshot.LiquidationNote, - LastError: r.lastErrorString(), - LastUpdatedIso: snapshot.LastUpdate.UTC().Format(time.RFC3339), - } - return payload -} - -func (r *Runner) snapshotState() BacktestState { - r.stateMu.RLock() - defer r.stateMu.RUnlock() - - copyState := *r.state - copyState.Positions = make(map[string]PositionSnapshot, len(r.state.Positions)) - for k, v := range r.state.Positions { - copyState.Positions[k] = v - } - return copyState -} diff --git a/backtest/runner_loop.go b/backtest/runner_loop.go deleted file mode 100644 index 81703bc1..00000000 --- a/backtest/runner_loop.go +++ /dev/null @@ -1,563 +0,0 @@ -package backtest - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "sort" - "time" - - "nofx/kernel" - "nofx/logger" - "nofx/market" - "nofx/store" -) - -func (r *Runner) loop(ctx context.Context) { - defer close(r.doneCh) - - for { - select { - case <-ctx.Done(): - r.handleStop(fmt.Errorf("context canceled: %w", ctx.Err())) - return - case <-r.stopCh: - r.handleStop(nil) - return - case <-r.pauseCh: - r.handlePause() - <-r.resumeCh - r.resumeFromPause() - default: - } - - err := r.stepOnce() - if errors.Is(err, errBacktestCompleted) { - r.handleCompletion() - return - } - if errors.Is(err, errLiquidated) { - r.handleLiquidation() - return - } - if err != nil { - r.handleFailure(err) - return - } - } -} - -func (r *Runner) stepOnce() error { - state := r.snapshotState() - if state.BarIndex >= r.feed.DecisionBarCount() { - return errBacktestCompleted - } - - ts := r.feed.DecisionTimestamp(state.BarIndex) - - marketData, multiTF, err := r.feed.BuildMarketData(ts) - if err != nil { - return err - } - - priceMap := make(map[string]float64, len(marketData)) - for symbol, data := range marketData { - priceMap[symbol] = data.CurrentPrice - } - - callCount := state.DecisionCycle + 1 - shouldDecide := r.shouldTriggerDecision(state.BarIndex) - - var ( - record *store.DecisionRecord - decisionActions []store.DecisionAction - tradeEvents = make([]TradeEvent, 0) - execLog []string - hadError bool - ) - - decisionAttempted := shouldDecide - - if shouldDecide { - ctx, rec, err := r.buildDecisionContext(ts, marketData, multiTF, priceMap, callCount) - if err != nil { - // Defensive nil check to prevent panic if buildDecisionContext returns error with nil record - if rec != nil { - rec.Success = false - rec.ErrorMessage = fmt.Sprintf("failed to build trading context: %v", err) - _ = r.logDecision(rec) - } - return err - } - record = rec - - var ( - fullDecision *kernel.FullDecision - fromCache bool - cacheKey string - ) - if r.aiCache != nil { - if key, err := computeCacheKey(ctx, r.cfg.PromptVariant, ts); err == nil { - cacheKey = key - if cached, ok := r.aiCache.Get(cacheKey); ok { - fullDecision = cached - fromCache = true - } else if r.cfg.ReplayOnly { - decisionErr := fmt.Errorf("replay_only enabled but cache miss at %d", ts) - record.Success = false - record.ErrorMessage = fmt.Sprintf("cached decision not found for ts=%d", ts) - _ = r.logDecision(record) - return decisionErr - } - } else { - logger.Infof("failed to compute ai cache key: %v", err) - } - } - - if !fromCache { - fd, err := r.invokeAIWithRetry(ctx) - if err != nil { - decisionAttempted = true - hadError = true - record.Success = false - record.ErrorMessage = fmt.Sprintf("AI decision failed: %v", err) - execLog = append(execLog, fmt.Sprintf("⚠️ AI decision failed: %v", err)) - r.setLastError(err) - } else { - fullDecision = fd - if r.cfg.CacheAI && r.aiCache != nil && cacheKey != "" { - if err := r.aiCache.Put(cacheKey, r.cfg.PromptVariant, ts, fullDecision); err != nil { - logger.Infof("failed to persist ai cache for %s: %v", r.cfg.RunID, err) - } - } - } - } - - if fullDecision != nil { - r.fillDecisionRecord(record, fullDecision) - - sorted := sortDecisionsByPriority(fullDecision.Decisions) - - prevLogs := execLog - decisionActions = make([]store.DecisionAction, 0, len(sorted)) - execLog = make([]string, 0, len(sorted)+len(prevLogs)) - if len(prevLogs) > 0 { - execLog = append(execLog, prevLogs...) - } - - for _, dec := range sorted { - actionRecord, trades, logEntry, execErr := r.executeDecision(dec, priceMap, ts, callCount) - if execErr != nil { - actionRecord.Success = false - actionRecord.Error = execErr.Error() - hadError = true - execLog = append(execLog, fmt.Sprintf("❌ %s %s: %v", dec.Symbol, dec.Action, execErr)) - } else { - actionRecord.Success = true - execLog = append(execLog, fmt.Sprintf("✓ %s %s", dec.Symbol, dec.Action)) - } - if len(trades) > 0 { - tradeEvents = append(tradeEvents, trades...) - } - if logEntry != "" { - execLog = append(execLog, logEntry) - } - decisionActions = append(decisionActions, actionRecord) - } - } - } - - cycleForLog := state.DecisionCycle - if decisionAttempted { - cycleForLog = callCount - } - - liquidationEvents, liquidationNote, err := r.checkLiquidation(ts, priceMap, cycleForLog) - if err != nil { - if record != nil { - record.Success = false - record.ErrorMessage = err.Error() - _ = r.logDecision(record) - } - return err - } - if len(liquidationEvents) > 0 { - hadError = true - tradeEvents = append(tradeEvents, liquidationEvents...) - if record != nil { - execLog = append(execLog, fmt.Sprintf("⚠️ Forced liquidation: %s", liquidationNote)) - } - } - - if record != nil { - record.Decisions = decisionActions - record.ExecutionLog = execLog - record.Success = !hadError && liquidationNote == "" - if liquidationNote != "" { - record.ErrorMessage = liquidationNote - } - } - - equity, unrealized, _ := r.account.TotalEquity(priceMap) - marginUsed := r.totalMarginUsed() - - r.updateState(ts, equity, unrealized, marginUsed, priceMap, decisionAttempted) - - snapshot := r.snapshotState() - drawdownPct := 0.0 - if snapshot.MaxEquity > 0 { - drawdownPct = ((snapshot.MaxEquity - snapshot.Equity) / snapshot.MaxEquity) * 100 - } - - equityPoint := EquityPoint{ - Timestamp: ts, - Equity: snapshot.Equity, - Available: snapshot.Cash, - PnL: snapshot.Equity - r.account.InitialBalance(), - PnLPct: ((snapshot.Equity - r.account.InitialBalance()) / r.account.InitialBalance()) * 100, - DrawdownPct: drawdownPct, - Cycle: snapshot.DecisionCycle, - } - - if err := appendEquityPoint(r.cfg.RunID, equityPoint); err != nil { - return err - } - - for _, evt := range tradeEvents { - if err := appendTradeEvent(r.cfg.RunID, evt); err != nil { - return err - } - } - - if record != nil { - if err := r.logDecision(record); err != nil { - return err - } - } - - if err := saveProgress(r.cfg.RunID, &snapshot, &r.cfg); err != nil { - return err - } - - if err := r.maybeCheckpoint(); err != nil { - return err - } - - r.persistMetadata() - r.persistMetrics(false) - - if !hadError && liquidationNote == "" { - r.setLastError(nil) - } - - if snapshot.Liquidated { - return errLiquidated - } - - return nil -} - -func (r *Runner) buildDecisionContext(ts int64, marketData map[string]*market.Data, multiTF map[string]map[string]*market.Data, priceMap map[string]float64, callCount int) (*kernel.Context, *store.DecisionRecord, error) { - equity, unrealized, _ := r.account.TotalEquity(priceMap) - available := r.account.Cash() - marginUsed := r.totalMarginUsed() - marginPct := 0.0 - if equity > 0 { - marginPct = (marginUsed / equity) * 100 - } - - accountInfo := kernel.AccountInfo{ - TotalEquity: equity, - AvailableBalance: available, - TotalPnL: equity - r.account.InitialBalance(), - TotalPnLPct: ((equity - r.account.InitialBalance()) / r.account.InitialBalance()) * 100, - MarginUsed: marginUsed, - MarginUsedPct: marginPct, - PositionCount: len(r.account.Positions()), - } - - positions := r.convertPositions(priceMap) - - // Get candidate coins from strategy engine (includes source info) - candidateCoins, err := r.strategyEngine.GetCandidateCoins() - if err != nil { - // Fallback to simple list if strategy engine fails - candidateCoins = make([]kernel.CandidateCoin, 0, len(r.cfg.Symbols)) - for _, sym := range r.cfg.Symbols { - candidateCoins = append(candidateCoins, kernel.CandidateCoin{Symbol: sym, Sources: []string{"backtest"}}) - } - } - - runtime := int((ts - int64(r.cfg.StartTS*1000)) / 60000) - ctx := &kernel.Context{ - CurrentTime: time.UnixMilli(ts).UTC().Format("2006-01-02 15:04:05 UTC"), - RuntimeMinutes: runtime, - CallCount: callCount, - Account: accountInfo, - Positions: positions, - CandidateCoins: candidateCoins, - PromptVariant: r.cfg.PromptVariant, - MarketDataMap: marketData, - MultiTFMarket: multiTF, - BTCETHLeverage: r.cfg.Leverage.BTCETHLeverage, - AltcoinLeverage: r.cfg.Leverage.AltcoinLeverage, - Timeframes: r.cfg.Timeframes, - } - - // Fetch quantitative data if enabled in strategy (uses current data as approximation) - strategyConfig := r.strategyEngine.GetConfig() - if strategyConfig.Indicators.EnableQuantData { - // Collect symbols to query (candidate coins + position coins) - symbolSet := make(map[string]bool) - for _, sym := range r.cfg.Symbols { - symbolSet[sym] = true - } - for _, pos := range positions { - symbolSet[pos.Symbol] = true - } - symbols := make([]string, 0, len(symbolSet)) - for sym := range symbolSet { - symbols = append(symbols, sym) - } - ctx.QuantDataMap = r.strategyEngine.FetchQuantDataBatch(symbols) - if len(ctx.QuantDataMap) > 0 { - logger.Infof("📊 Backtest: fetched quant data for %d symbols", len(ctx.QuantDataMap)) - } - } - - // Fetch OI ranking data if enabled in strategy (uses current data as approximation) - if strategyConfig.Indicators.EnableOIRanking { - ctx.OIRankingData = r.strategyEngine.FetchOIRankingData() - if ctx.OIRankingData != nil { - logger.Infof("📊 Backtest: OI ranking data ready: %d top, %d low positions", - len(ctx.OIRankingData.TopPositions), len(ctx.OIRankingData.LowPositions)) - } - } - - // Fetch NetFlow ranking data if enabled in strategy - if strategyConfig.Indicators.EnableNetFlowRanking { - ctx.NetFlowRankingData = r.strategyEngine.FetchNetFlowRankingData() - if ctx.NetFlowRankingData != nil { - logger.Infof("💰 Backtest: NetFlow ranking data ready: inst_in=%d, inst_out=%d", - len(ctx.NetFlowRankingData.InstitutionFutureTop), len(ctx.NetFlowRankingData.InstitutionFutureLow)) - } - } - - // Fetch Price ranking data if enabled in strategy - if strategyConfig.Indicators.EnablePriceRanking { - ctx.PriceRankingData = r.strategyEngine.FetchPriceRankingData() - if ctx.PriceRankingData != nil { - logger.Infof("📈 Backtest: Price ranking data ready for %d durations", - len(ctx.PriceRankingData.Durations)) - } - } - - record := &store.DecisionRecord{ - AccountState: store.AccountSnapshot{ - TotalBalance: accountInfo.TotalEquity, - AvailableBalance: accountInfo.AvailableBalance, - TotalUnrealizedProfit: unrealized, - PositionCount: accountInfo.PositionCount, - MarginUsedPct: accountInfo.MarginUsedPct, - }, - CandidateCoins: make([]string, 0, len(candidateCoins)), - Positions: r.snapshotPositions(priceMap), - } - for _, coin := range candidateCoins { - record.CandidateCoins = append(record.CandidateCoins, coin.Symbol) - } - record.Timestamp = time.UnixMilli(ts).UTC() - - return ctx, record, nil -} - -func (r *Runner) fillDecisionRecord(record *store.DecisionRecord, full *kernel.FullDecision) { - record.InputPrompt = full.UserPrompt - record.CoTTrace = full.CoTTrace - if len(full.Decisions) > 0 { - if data, err := json.MarshalIndent(full.Decisions, "", " "); err == nil { - record.DecisionJSON = string(data) - } - } -} - -func (r *Runner) invokeAIWithRetry(ctx *kernel.Context) (*kernel.FullDecision, error) { - var lastErr error - for attempt := 0; attempt < aiDecisionMaxRetries; attempt++ { - // Use GetFullDecisionWithStrategy with the pre-configured strategy engine - // This ensures backtest uses the same unified prompt generation as live trading - fd, err := kernel.GetFullDecisionWithStrategy( - ctx, - r.mcpClient, - r.strategyEngine, - r.cfg.PromptVariant, - ) - if err == nil { - return fd, nil - } - lastErr = err - delay := time.Duration(attempt+1) * 500 * time.Millisecond - time.Sleep(delay) - } - return nil, lastErr -} - -func (r *Runner) shouldTriggerDecision(barIndex int) bool { - if r.cfg.DecisionCadenceNBars <= 1 { - return true - } - if barIndex < 0 { - return true - } - return barIndex%r.cfg.DecisionCadenceNBars == 0 -} - -func (r *Runner) updateState(ts int64, equity, unrealized, marginUsed float64, priceMap map[string]float64, advancedDecision bool) { - r.stateMu.Lock() - defer r.stateMu.Unlock() - - if r.state.MaxEquity == 0 || equity > r.state.MaxEquity { - r.state.MaxEquity = equity - } - if r.state.MinEquity == 0 || equity < r.state.MinEquity { - r.state.MinEquity = equity - } - if r.state.MaxEquity > 0 { - drawdown := ((r.state.MaxEquity - equity) / r.state.MaxEquity) * 100 - if drawdown > r.state.MaxDrawdownPct { - r.state.MaxDrawdownPct = drawdown - } - } - - positions := make(map[string]PositionSnapshot) - for _, pos := range r.account.Positions() { - key := fmt.Sprintf("%s:%s", pos.Symbol, pos.Side) - positions[key] = PositionSnapshot{ - Symbol: pos.Symbol, - Side: pos.Side, - Quantity: pos.Quantity, - AvgPrice: pos.EntryPrice, - Leverage: pos.Leverage, - LiquidationPrice: pos.LiquidationPrice, - MarginUsed: pos.Margin, - OpenTime: pos.OpenTime, - AccumulatedFee: pos.AccumulatedFee, - } - } - - r.state.BarTimestamp = ts - r.state.BarIndex++ - if advancedDecision { - r.state.DecisionCycle++ - } - r.state.Cash = r.account.Cash() - r.state.Equity = equity - r.state.UnrealizedPnL = unrealized - r.state.RealizedPnL = r.account.RealizedPnL() - r.state.Positions = positions - r.state.LastUpdate = time.Now().UTC() -} - -func (r *Runner) handleStop(reason error) { - r.forceCheckpoint() - if reason != nil { - r.setLastError(reason) - } else { - r.setLastError(nil) - } - r.statusMu.Lock() - r.err = reason - r.status = RunStateStopped - r.statusMu.Unlock() - r.persistMetadata() - r.persistMetrics(true) - r.releaseLock() -} - -func (r *Runner) handlePause() { - r.forceCheckpoint() - r.setLastError(nil) - r.statusMu.Lock() - r.status = RunStatePaused - r.statusMu.Unlock() - r.persistMetadata() - r.persistMetrics(true) -} - -func (r *Runner) resumeFromPause() { - r.setLastError(nil) - r.statusMu.Lock() - r.status = RunStateRunning - r.statusMu.Unlock() - r.persistMetadata() -} - -func (r *Runner) handleCompletion() { - r.setLastError(nil) - r.statusMu.Lock() - r.status = RunStateCompleted - r.statusMu.Unlock() - r.persistMetadata() - r.persistMetrics(true) - r.releaseLock() -} - -func (r *Runner) handleFailure(err error) { - r.forceCheckpoint() - if err != nil { - r.setLastError(err) - } - r.statusMu.Lock() - r.err = err - r.status = RunStateFailed - r.statusMu.Unlock() - r.persistMetadata() - r.persistMetrics(true) - r.releaseLock() -} - -func (r *Runner) handleLiquidation() { - r.forceCheckpoint() - r.setLastError(errLiquidated) - r.statusMu.Lock() - r.err = errLiquidated - r.status = RunStateLiquidated - r.statusMu.Unlock() - r.persistMetadata() - r.persistMetrics(true) - r.releaseLock() -} - -func sortDecisionsByPriority(decisions []kernel.Decision) []kernel.Decision { - if len(decisions) <= 1 { - return decisions - } - - priority := func(action string) int { - switch action { - case "close_long", "close_short": - return 1 - case "open_long", "open_short": - return 2 - case "hold", "wait": - return 3 - default: - return 99 - } - } - - result := make([]kernel.Decision, len(decisions)) - copy(result, decisions) - - sort.Slice(result, func(i, j int) bool { - pi := priority(result[i].Action) - pj := priority(result[j].Action) - if pi != pj { - return pi < pj - } - return i < j - }) - - return result -} diff --git a/backtest/runner_metrics.go b/backtest/runner_metrics.go deleted file mode 100644 index e3dea04e..00000000 --- a/backtest/runner_metrics.go +++ /dev/null @@ -1,239 +0,0 @@ -package backtest - -import ( - "fmt" - "sort" - "time" - - "nofx/logger" - "nofx/store" -) - -func (r *Runner) persistMetadata() { - state := r.snapshotState() - meta := r.buildMetadata(state, r.Status()) - meta.CreatedAt = r.createdAt - if err := SaveRunMetadata(meta); err != nil { - logger.Infof("failed to save run metadata for %s: %v", r.cfg.RunID, err) - } else { - if err := updateRunIndex(meta, &r.cfg); err != nil { - logger.Infof("failed to update index for %s: %v", r.cfg.RunID, err) - } - } -} - -func (r *Runner) logDecision(record *store.DecisionRecord) error { - if record == nil { - return nil - } - persistDecisionRecord(r.cfg.RunID, record) - return nil -} - -func (r *Runner) persistMetrics(force bool) { - if r.cfg.RunID == "" { - return - } - - if !force && !r.lastMetricsWrite.IsZero() { - if time.Since(r.lastMetricsWrite) < metricsWriteInterval { - return - } - } - - state := r.snapshotState() - metrics, err := CalculateMetrics(r.cfg.RunID, &r.cfg, &state) - if err != nil { - logger.Infof("failed to compute metrics for %s: %v", r.cfg.RunID, err) - return - } - if metrics == nil { - return - } - if err := PersistMetrics(r.cfg.RunID, metrics); err != nil { - logger.Infof("failed to persist metrics for %s: %v", r.cfg.RunID, err) - return - } - r.lastMetricsWrite = time.Now() -} - -func (r *Runner) buildMetadata(state BacktestState, runState RunState) *RunMetadata { - if state.Liquidated && runState != RunStateLiquidated { - runState = RunStateLiquidated - } - - progress := progressPercent(state, r.cfg) - - summary := RunSummary{ - SymbolCount: len(r.cfg.Symbols), - DecisionTF: r.cfg.DecisionTimeframe, - ProcessedBars: state.BarIndex, - ProgressPct: progress, - EquityLast: state.Equity, - MaxDrawdownPct: state.MaxDrawdownPct, - Liquidated: state.Liquidated, - LiquidationNote: state.LiquidationNote, - } - - meta := &RunMetadata{ - RunID: r.cfg.RunID, - UserID: r.cfg.UserID, - State: runState, - LastError: r.lastErrorString(), - Summary: summary, - } - - return meta -} - -func progressPercent(state BacktestState, cfg BacktestConfig) float64 { - duration := cfg.Duration() - if duration <= 0 { - return 0 - } - if state.BarTimestamp == 0 { - return 0 - } - - start := time.Unix(cfg.StartTS, 0) - end := time.Unix(cfg.EndTS, 0) - current := time.UnixMilli(state.BarTimestamp) - - if !current.After(start) { - return 0 - } - if current.After(end) { - return 100 - } - - elapsed := current.Sub(start) - pct := float64(elapsed) / float64(duration) * 100 - if pct > 100 { - pct = 100 - } - if pct < 0 { - pct = 0 - } - return pct -} - -func (r *Runner) maybeCheckpoint() error { - state := r.snapshotState() - shouldCheckpoint := false - - if r.cfg.CheckpointIntervalBars > 0 && state.BarIndex > 0 && state.BarIndex%r.cfg.CheckpointIntervalBars == 0 { - shouldCheckpoint = true - } - - interval := time.Duration(r.cfg.CheckpointIntervalSeconds) * time.Second - if interval <= 0 { - interval = 2 * time.Second - } - if time.Since(r.lastCheckpoint) >= interval { - shouldCheckpoint = true - } - - if !shouldCheckpoint { - return nil - } - - if err := r.saveCheckpoint(state); err != nil { - return err - } - - return nil -} - -func (r *Runner) snapshotForCheckpoint(state BacktestState) []PositionSnapshot { - res := make([]PositionSnapshot, 0, len(state.Positions)) - for _, pos := range state.Positions { - res = append(res, pos) - } - sort.Slice(res, func(i, j int) bool { - if res[i].Symbol == res[j].Symbol { - return res[i].Side < res[j].Side - } - return res[i].Symbol < res[j].Symbol - }) - return res -} - -func (r *Runner) buildCheckpointFromState(state BacktestState) *Checkpoint { - return &Checkpoint{ - BarIndex: state.BarIndex, - BarTimestamp: state.BarTimestamp, - Cash: state.Cash, - Equity: state.Equity, - UnrealizedPnL: state.UnrealizedPnL, - RealizedPnL: state.RealizedPnL, - Positions: r.snapshotForCheckpoint(state), - DecisionCycle: state.DecisionCycle, - Liquidated: state.Liquidated, - LiquidationNote: state.LiquidationNote, - MaxEquity: state.MaxEquity, - MinEquity: state.MinEquity, - MaxDrawdownPct: state.MaxDrawdownPct, - AICacheRef: r.cachePath, - } -} - -func (r *Runner) saveCheckpoint(state BacktestState) error { - ckpt := r.buildCheckpointFromState(state) - if ckpt == nil { - return nil - } - if err := SaveCheckpoint(r.cfg.RunID, ckpt); err != nil { - return err - } - r.lastCheckpoint = time.Now() - return nil -} - -func (r *Runner) forceCheckpoint() { - state := r.snapshotState() - if err := r.saveCheckpoint(state); err != nil { - logger.Infof("failed to save checkpoint for %s: %v", r.cfg.RunID, err) - } -} - -func (r *Runner) RestoreFromCheckpoint() error { - ckpt, err := LoadCheckpoint(r.cfg.RunID) - if err != nil { - return err - } - return r.applyCheckpoint(ckpt) -} - -func (r *Runner) applyCheckpoint(ckpt *Checkpoint) error { - if ckpt == nil { - return fmt.Errorf("checkpoint is nil") - } - r.account.RestoreFromSnapshots(ckpt.Cash, ckpt.RealizedPnL, ckpt.Positions) - r.stateMu.Lock() - defer r.stateMu.Unlock() - r.state.BarIndex = ckpt.BarIndex - r.state.BarTimestamp = ckpt.BarTimestamp - r.state.Cash = ckpt.Cash - r.state.Equity = ckpt.Equity - r.state.UnrealizedPnL = ckpt.UnrealizedPnL - r.state.RealizedPnL = ckpt.RealizedPnL - r.state.DecisionCycle = ckpt.DecisionCycle - r.state.Liquidated = ckpt.Liquidated - r.state.LiquidationNote = ckpt.LiquidationNote - r.state.MaxEquity = ckpt.MaxEquity - r.state.MinEquity = ckpt.MinEquity - r.state.MaxDrawdownPct = ckpt.MaxDrawdownPct - r.state.Positions = snapshotsToMap(ckpt.Positions) - r.state.LastUpdate = time.Now().UTC() - r.lastCheckpoint = time.Now() - return nil -} - -func snapshotsToMap(snaps []PositionSnapshot) map[string]PositionSnapshot { - positions := make(map[string]PositionSnapshot, len(snaps)) - for _, snap := range snaps { - key := fmt.Sprintf("%s:%s", snap.Symbol, snap.Side) - positions[key] = snap - } - return positions -} diff --git a/backtest/runner_orders.go b/backtest/runner_orders.go deleted file mode 100644 index 8e6bdfa0..00000000 --- a/backtest/runner_orders.go +++ /dev/null @@ -1,420 +0,0 @@ -package backtest - -import ( - "fmt" - "strings" - "time" - - "nofx/kernel" - "nofx/logger" - "nofx/market" - "nofx/store" -) - -func (r *Runner) executeDecision(dec kernel.Decision, priceMap map[string]float64, ts int64, cycle int) (store.DecisionAction, []TradeEvent, string, error) { - symbol := dec.Symbol - if symbol == "" { - return store.DecisionAction{}, nil, "", fmt.Errorf("empty symbol in decision") - } - - usedLeverage := r.resolveLeverage(dec.Leverage, symbol) - actionRecord := store.DecisionAction{ - Action: dec.Action, - Symbol: symbol, - Leverage: usedLeverage, - Timestamp: time.UnixMilli(ts).UTC(), - } - - if priceMap == nil { - return actionRecord, nil, "", fmt.Errorf("priceMap is nil") - } - - basePrice, ok := priceMap[symbol] - if !ok || basePrice <= 0 { - return actionRecord, nil, "", fmt.Errorf("price unavailable for %s (found=%v, price=%.4f)", symbol, ok, basePrice) - } - fillPrice := r.executionPrice(symbol, basePrice, ts) - - switch dec.Action { - case "open_long": - qty := r.determineQuantity(dec, basePrice) - if qty <= 0 { - return actionRecord, nil, "", fmt.Errorf("invalid qty") - } - pos, fee, execPrice, err := r.account.Open(symbol, "long", qty, usedLeverage, fillPrice, ts) - if err != nil { - return actionRecord, nil, "", err - } - actionRecord.Quantity = qty - actionRecord.Price = execPrice - actionRecord.Leverage = pos.Leverage - trade := TradeEvent{ - Timestamp: ts, - Symbol: symbol, - Action: dec.Action, - Side: "long", - Quantity: qty, - Price: execPrice, - Fee: fee, - Slippage: execPrice - basePrice, - OrderValue: execPrice * qty, - RealizedPnL: 0, - Leverage: pos.Leverage, - Cycle: cycle, - PositionAfter: pos.Quantity, - } - return actionRecord, []TradeEvent{trade}, "", nil - - case "open_short": - qty := r.determineQuantity(dec, basePrice) - if qty <= 0 { - return actionRecord, nil, "", fmt.Errorf("invalid qty") - } - pos, fee, execPrice, err := r.account.Open(symbol, "short", qty, usedLeverage, fillPrice, ts) - if err != nil { - return actionRecord, nil, "", err - } - actionRecord.Quantity = qty - actionRecord.Price = execPrice - actionRecord.Leverage = pos.Leverage - trade := TradeEvent{ - Timestamp: ts, - Symbol: symbol, - Action: dec.Action, - Side: "short", - Quantity: qty, - Price: execPrice, - Fee: fee, - Slippage: basePrice - execPrice, - OrderValue: execPrice * qty, - RealizedPnL: 0, - Leverage: pos.Leverage, - Cycle: cycle, - PositionAfter: pos.Quantity, - } - return actionRecord, []TradeEvent{trade}, "", nil - - case "close_long": - qty := r.determineCloseQuantity(symbol, "long", dec) - if qty <= 0 { - return actionRecord, nil, "", fmt.Errorf("invalid close qty") - } - posLev := r.account.positionLeverage(symbol, "long") - realized, fee, execPrice, err := r.account.Close(symbol, "long", qty, fillPrice) - if err != nil { - return actionRecord, nil, "", err - } - actionRecord.Quantity = qty - actionRecord.Price = execPrice - actionRecord.Leverage = posLev - trade := TradeEvent{ - Timestamp: ts, - Symbol: symbol, - Action: dec.Action, - Side: "long", - Quantity: qty, - Price: execPrice, - Fee: fee, - Slippage: basePrice - execPrice, - OrderValue: execPrice * qty, - RealizedPnL: realized - fee, - Leverage: posLev, - Cycle: cycle, - PositionAfter: r.remainingPosition(symbol, "long"), - } - return actionRecord, []TradeEvent{trade}, "", nil - - case "close_short": - qty := r.determineCloseQuantity(symbol, "short", dec) - if qty <= 0 { - return actionRecord, nil, "", fmt.Errorf("invalid close qty") - } - posLev := r.account.positionLeverage(symbol, "short") - realized, fee, execPrice, err := r.account.Close(symbol, "short", qty, fillPrice) - if err != nil { - return actionRecord, nil, "", err - } - actionRecord.Quantity = qty - actionRecord.Price = execPrice - actionRecord.Leverage = posLev - trade := TradeEvent{ - Timestamp: ts, - Symbol: symbol, - Action: dec.Action, - Side: "short", - Quantity: qty, - Price: execPrice, - Fee: fee, - Slippage: execPrice - basePrice, - OrderValue: execPrice * qty, - RealizedPnL: realized - fee, - Leverage: posLev, - Cycle: cycle, - PositionAfter: r.remainingPosition(symbol, "short"), - } - return actionRecord, []TradeEvent{trade}, "", nil - - case "hold", "wait": - return actionRecord, nil, fmt.Sprintf("hold position: %s", dec.Action), nil - default: - return actionRecord, nil, "", fmt.Errorf("unsupported action %s", dec.Action) - } -} - -// MinPositionSizeUSD is the minimum position size in USD to avoid dust positions -const MinPositionSizeUSD = 10.0 - -func (r *Runner) determineQuantity(dec kernel.Decision, price float64) float64 { - snapshot := r.snapshotState() - equity := snapshot.Equity - if equity <= 0 { - equity = r.account.InitialBalance() - } - - // Get leverage for this symbol - leverage := r.resolveLeverage(dec.Leverage, dec.Symbol) - if leverage <= 0 { - leverage = 5 - } - - // Calculate available margin (leave some buffer for fees) - availableCash := r.account.Cash() - maxMarginToUse := availableCash * 0.9 // Use max 90% of available cash - maxPositionValue := maxMarginToUse * float64(leverage) - - sizeUSD := dec.PositionSizeUSD - if sizeUSD <= 0 { - // Default to 5% of equity, but cap to available margin - sizeUSD = 0.05 * equity - } - - // Cap position size to what we can actually afford - if sizeUSD > maxPositionValue { - logger.Infof("📊 Backtest: capping position from %.2f to %.2f (available margin: %.2f, leverage: %dx)", - sizeUSD, maxPositionValue, maxMarginToUse, leverage) - sizeUSD = maxPositionValue - } - - // Reject positions below minimum size to avoid dust positions - if sizeUSD < MinPositionSizeUSD { - logger.Infof("📊 Backtest: rejecting position size %.2f USD (below minimum %.2f USD)", - sizeUSD, MinPositionSizeUSD) - return 0 - } - - qty := sizeUSD / price - if qty < 0 { - qty = 0 - } - return qty -} - -func (r *Runner) determineCloseQuantity(symbol, side string, dec kernel.Decision) float64 { - for _, pos := range r.account.Positions() { - if pos.Symbol == strings.ToUpper(symbol) && pos.Side == side { - return pos.Quantity - } - } - return 0 -} - -func (r *Runner) resolveLeverage(requested int, symbol string) int { - sym := strings.ToUpper(symbol) - isBTCETH := sym == "BTCUSDT" || sym == "ETHUSDT" - - // Determine configured max leverage for this symbol type - var maxLeverage int - if isBTCETH { - maxLeverage = r.cfg.Leverage.BTCETHLeverage - if maxLeverage <= 0 { - maxLeverage = 10 // Default max for BTC/ETH - } - } else { - maxLeverage = r.cfg.Leverage.AltcoinLeverage - if maxLeverage <= 0 { - maxLeverage = 5 // Default max for altcoins - } - } - - // Use requested leverage if provided, otherwise use max as default - leverage := requested - if leverage <= 0 { - leverage = maxLeverage - } - - // Enforce max leverage limit - if leverage > maxLeverage { - logger.Infof("📊 Backtest: capping leverage from %dx to %dx for %s", - leverage, maxLeverage, symbol) - leverage = maxLeverage - } - - return leverage -} - -func (r *Runner) remainingPosition(symbol, side string) float64 { - for _, pos := range r.account.Positions() { - if pos.Symbol == strings.ToUpper(symbol) && pos.Side == side { - return pos.Quantity - } - } - return 0 -} - -func (r *Runner) snapshotPositions(priceMap map[string]float64) []store.PositionSnapshot { - positions := r.account.Positions() - list := make([]store.PositionSnapshot, 0, len(positions)) - for _, pos := range positions { - price := priceMap[pos.Symbol] - list = append(list, store.PositionSnapshot{ - Symbol: pos.Symbol, - Side: pos.Side, - PositionAmt: pos.Quantity, - EntryPrice: pos.EntryPrice, - MarkPrice: price, - UnrealizedProfit: unrealizedPnL(pos, price), - Leverage: float64(pos.Leverage), - LiquidationPrice: pos.LiquidationPrice, - }) - } - return list -} - -func (r *Runner) convertPositions(priceMap map[string]float64) []kernel.PositionInfo { - positions := r.account.Positions() - list := make([]kernel.PositionInfo, 0, len(positions)) - for _, pos := range positions { - price := priceMap[pos.Symbol] - pnl := unrealizedPnL(pos, price) - // Calculate P&L percentage based on entry notional (position cost) - pnlPct := 0.0 - if pos.Notional > 0 { - pnlPct = (pnl / pos.Notional) * 100 - } - list = append(list, kernel.PositionInfo{ - Symbol: pos.Symbol, - Side: pos.Side, - EntryPrice: pos.EntryPrice, - MarkPrice: price, - Quantity: pos.Quantity, - Leverage: pos.Leverage, - UnrealizedPnL: pnl, - UnrealizedPnLPct: pnlPct, - LiquidationPrice: pos.LiquidationPrice, - MarginUsed: pos.Margin, - UpdateTime: time.Now().UnixMilli(), - }) - } - return list -} - -func (r *Runner) executionPrice(symbol string, markPrice float64, ts int64) float64 { - curr, next := r.feed.decisionBarSnapshot(symbol, ts) - switch r.cfg.FillPolicy { - case FillPolicyNextOpen: - if next != nil && next.Open > 0 { - return next.Open - } - case FillPolicyBarVWAP: - if curr != nil { - if vwap := barVWAP(*curr); vwap > 0 { - return vwap - } - } - case FillPolicyMidPrice: - if curr != nil && curr.High > 0 && curr.Low > 0 { - return (curr.High + curr.Low) / 2 - } - } - return markPrice -} - -func (r *Runner) totalMarginUsed() float64 { - sum := 0.0 - for _, pos := range r.account.Positions() { - sum += pos.Margin - } - return sum -} - -func (r *Runner) checkLiquidation(ts int64, priceMap map[string]float64, cycle int) ([]TradeEvent, string, error) { - positions := append([]*position(nil), r.account.Positions()...) - events := make([]TradeEvent, 0) - var noteBuilder strings.Builder - - for _, pos := range positions { - price := priceMap[pos.Symbol] - liqPrice := pos.LiquidationPrice - trigger := false - execPrice := price - if pos.Side == "long" { - if price <= liqPrice && liqPrice > 0 { - trigger = true - execPrice = liqPrice - } - } else { - if price >= liqPrice && liqPrice > 0 { - trigger = true - execPrice = liqPrice - } - } - if !trigger { - continue - } - - realized, fee, finalPrice, err := r.account.Close(pos.Symbol, pos.Side, pos.Quantity, execPrice) - if err != nil { - return nil, "", err - } - - noteBuilder.WriteString(fmt.Sprintf("%s %s @ %.4f; ", pos.Symbol, pos.Side, finalPrice)) - - evt := TradeEvent{ - Timestamp: ts, - Symbol: pos.Symbol, - Action: "liquidated", - Side: pos.Side, - Quantity: pos.Quantity, - Price: finalPrice, - Fee: fee, - Slippage: 0, - OrderValue: finalPrice * pos.Quantity, - RealizedPnL: realized - fee, - Leverage: pos.Leverage, - Cycle: cycle, - PositionAfter: 0, - LiquidationFlag: true, - Note: fmt.Sprintf("forced liquidation at %.4f", finalPrice), - } - events = append(events, evt) - } - - if len(events) == 0 { - return events, "", nil - } - - note := strings.TrimSuffix(noteBuilder.String(), "; ") - - r.stateMu.Lock() - r.state.Liquidated = true - r.state.LiquidationNote = note - r.stateMu.Unlock() - - return events, note, nil -} - -func barVWAP(k market.Kline) float64 { - values := []float64{k.Open, k.High, k.Low, k.Close} - sum := 0.0 - count := 0.0 - for _, v := range values { - if v > 0 { - sum += v - count++ - } - } - if count == 0 { - return 0 - } - return sum / count -} diff --git a/backtest/storage.go b/backtest/storage.go deleted file mode 100644 index 84cbcaa6..00000000 --- a/backtest/storage.go +++ /dev/null @@ -1,561 +0,0 @@ -package backtest - -import ( - "archive/zip" - "bufio" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "nofx/store" -) - -const ( - backtestsRootDir = "backtests" -) - -type progressPayload struct { - BarIndex int `json:"bar_index"` - Equity float64 `json:"equity"` - ProgressPct float64 `json:"progress_pct"` - Liquidated bool `json:"liquidated"` - UpdatedAtISO string `json:"updated_at_iso"` -} - -func runDir(runID string) string { - return filepath.Join(backtestsRootDir, runID) -} - -func ensureRunDir(runID string) error { - dir := runDir(runID) - return os.MkdirAll(dir, 0o755) -} - -func checkpointPath(runID string) string { - return filepath.Join(runDir(runID), "checkpoint.json") -} - -func runMetadataPath(runID string) string { - return filepath.Join(runDir(runID), "run.json") -} - -func equityLogPath(runID string) string { - return filepath.Join(runDir(runID), "equity.jsonl") -} - -func tradesLogPath(runID string) string { - return filepath.Join(runDir(runID), "trades.jsonl") -} - -func metricsPath(runID string) string { - return filepath.Join(runDir(runID), "metrics.json") -} - -func progressPath(runID string) string { - return filepath.Join(runDir(runID), "progress.json") -} - -func decisionLogDir(runID string) string { - return filepath.Join(runDir(runID), "decision_logs") -} - -func writeJSONAtomic(path string, v any) error { - data, err := json.MarshalIndent(v, "", " ") - if err != nil { - return err - } - return writeFileAtomic(path, data, 0o644) -} - -func writeFileAtomic(path string, data []byte, perm os.FileMode) error { - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return err - } - tmpFile, err := os.CreateTemp(dir, ".tmp-*") - if err != nil { - return err - } - tmpPath := tmpFile.Name() - if _, err := tmpFile.Write(data); err != nil { - tmpFile.Close() - os.Remove(tmpPath) - return err - } - if err := tmpFile.Sync(); err != nil { - tmpFile.Close() - os.Remove(tmpPath) - return err - } - if err := tmpFile.Close(); err != nil { - os.Remove(tmpPath) - return err - } - if err := os.Chmod(tmpPath, perm); err != nil { - os.Remove(tmpPath) - return err - } - return os.Rename(tmpPath, path) -} - -func appendJSONLine(path string, payload any) error { - data, err := json.Marshal(payload) - if err != nil { - return err - } - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return err - } - f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - - writer := bufio.NewWriter(f) - if _, err := writer.Write(data); err != nil { - return err - } - if err := writer.WriteByte('\n'); err != nil { - return err - } - if err := writer.Flush(); err != nil { - return err - } - return f.Sync() -} - -// SaveCheckpoint writes the checkpoint to disk. -func SaveCheckpoint(runID string, ckpt *Checkpoint) error { - if ckpt == nil { - return fmt.Errorf("checkpoint is nil") - } - if usingDB() { - return saveCheckpointDB(runID, ckpt) - } - return writeJSONAtomic(checkpointPath(runID), ckpt) -} - -// LoadCheckpoint reads the most recent checkpoint. -func LoadCheckpoint(runID string) (*Checkpoint, error) { - if usingDB() { - return loadCheckpointDB(runID) - } - path := checkpointPath(runID) - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var ckpt Checkpoint - if err := json.Unmarshal(data, &ckpt); err != nil { - return nil, err - } - return &ckpt, nil -} - -// SaveRunMetadata writes to run.json. -func SaveRunMetadata(meta *RunMetadata) error { - if meta == nil { - return fmt.Errorf("run metadata is nil") - } - if meta.Version == 0 { - meta.Version = 1 - } - if meta.CreatedAt.IsZero() { - meta.CreatedAt = time.Now().UTC() - } - meta.UpdatedAt = time.Now().UTC() - if usingDB() { - return saveRunMetadataDB(meta) - } - return writeJSONAtomic(runMetadataPath(meta.RunID), meta) -} - -// LoadRunMetadata reads run.json. -func LoadRunMetadata(runID string) (*RunMetadata, error) { - if usingDB() { - return loadRunMetadataDB(runID) - } - path := runMetadataPath(runID) - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var meta RunMetadata - if err := json.Unmarshal(data, &meta); err != nil { - return nil, err - } - return &meta, nil -} - -func appendEquityPoint(runID string, point EquityPoint) error { - if usingDB() { - return appendEquityPointDB(runID, point) - } - return appendJSONLine(equityLogPath(runID), point) -} - -func appendTradeEvent(runID string, event TradeEvent) error { - if usingDB() { - return appendTradeEventDB(runID, event) - } - return appendJSONLine(tradesLogPath(runID), event) -} - -func saveMetrics(runID string, metrics *Metrics) error { - if metrics == nil { - return fmt.Errorf("metrics is nil") - } - if usingDB() { - return saveMetricsDB(runID, metrics) - } - return writeJSONAtomic(metricsPath(runID), metrics) -} - -func saveProgress(runID string, state *BacktestState, cfg *BacktestConfig) error { - if state == nil || cfg == nil { - return fmt.Errorf("state or config nil") - } - dur := cfg.Duration() - progress := 0.0 - if dur > 0 { - current := time.UnixMilli(state.BarTimestamp) - start := time.Unix(cfg.StartTS, 0) - if current.After(start) { - elapsed := current.Sub(start) - progress = float64(elapsed) / float64(dur) - } - } - payload := progressPayload{ - BarIndex: state.BarIndex, - Equity: state.Equity, - ProgressPct: progress * 100, - Liquidated: state.Liquidated, - - UpdatedAtISO: time.Now().UTC().Format(time.RFC3339), - } - if usingDB() { - return saveProgressDB(runID, payload) - } - return writeJSONAtomic(progressPath(runID), payload) -} - -func SaveConfig(runID string, cfg *BacktestConfig) error { - if cfg == nil { - return fmt.Errorf("config is nil") - } - persist := *cfg - persist.AICfg.APIKey = "" - if usingDB() { - return saveConfigDB(runID, &persist) - } - if err := ensureRunDir(runID); err != nil { - return err - } - return writeJSONAtomic(filepath.Join(runDir(runID), "config.json"), &persist) -} - -func LoadConfig(runID string) (*BacktestConfig, error) { - if usingDB() { - return loadConfigDB(runID) - } - data, err := os.ReadFile(filepath.Join(runDir(runID), "config.json")) - if err != nil { - return nil, err - } - var cfg BacktestConfig - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, err - } - return &cfg, nil -} - -func LoadEquityPoints(runID string) ([]EquityPoint, error) { - if usingDB() { - return loadEquityPointsDB(runID) - } - points, err := loadJSONLines[EquityPoint](equityLogPath(runID)) - if err != nil { - return nil, err - } - sort.Slice(points, func(i, j int) bool { - return points[i].Timestamp < points[j].Timestamp - }) - return points, nil -} - -func LoadTradeEvents(runID string) ([]TradeEvent, error) { - if usingDB() { - return loadTradeEventsDB(runID) - } - events, err := loadJSONLines[TradeEvent](tradesLogPath(runID)) - if err != nil { - return nil, err - } - sort.Slice(events, func(i, j int) bool { - if events[i].Timestamp == events[j].Timestamp { - return events[i].Symbol < events[j].Symbol - } - return events[i].Timestamp < events[j].Timestamp - }) - return events, nil -} - -func LoadMetrics(runID string) (*Metrics, error) { - if usingDB() { - return loadMetricsDB(runID) - } - data, err := os.ReadFile(metricsPath(runID)) - if err != nil { - return nil, err - } - var metrics Metrics - if err := json.Unmarshal(data, &metrics); err != nil { - return nil, err - } - return &metrics, nil -} - -func LoadRunIDs() ([]string, error) { - if usingDB() { - return loadRunIDsDB() - } - entries, err := os.ReadDir(backtestsRootDir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return []string{}, nil - } - return nil, err - } - runIDs := make([]string, 0, len(entries)) - for _, entry := range entries { - if entry.IsDir() { - runIDs = append(runIDs, entry.Name()) - } - } - sort.Strings(runIDs) - return runIDs, nil -} - -func loadJSONLines[T any](path string) ([]T, error) { - file, err := os.Open(path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return []T{}, nil - } - return nil, err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) - - var result []T - for scanner.Scan() { - line := scanner.Bytes() - if len(line) == 0 { - continue - } - var item T - if err := json.Unmarshal(line, &item); err != nil { - return nil, err - } - result = append(result, item) - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return result, nil -} -func PersistMetrics(runID string, metrics *Metrics) error { - return saveMetrics(runID, metrics) -} - -func LoadDecisionTrace(runID string, cycle int) (*store.DecisionRecord, error) { - if usingDB() { - return loadDecisionTraceDB(runID, cycle) - } - dir := decisionLogDir(runID) - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - type candidate struct { - path string - info os.DirEntry - } - cands := make([]candidate, 0, len(entries)) - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - if !strings.HasPrefix(name, "decision_") || !strings.HasSuffix(name, ".json") { - continue - } - cands = append(cands, candidate{path: filepath.Join(dir, name), info: entry}) - } - sort.Slice(cands, func(i, j int) bool { - infoI, _ := cands[i].info.Info() - infoJ, _ := cands[j].info.Info() - if infoI == nil || infoJ == nil { - return cands[i].path > cands[j].path - } - return infoI.ModTime().After(infoJ.ModTime()) - }) - - for _, cand := range cands { - data, err := os.ReadFile(cand.path) - if err != nil { - continue - } - var record store.DecisionRecord - if err := json.Unmarshal(data, &record); err != nil { - continue - } - if cycle <= 0 || record.CycleNumber == cycle { - return &record, nil - } - } - return nil, fmt.Errorf("decision trace not found for run %s cycle %d", runID, cycle) -} - -func LoadDecisionRecords(runID string, limit, offset int) ([]*store.DecisionRecord, error) { - if limit <= 0 { - limit = 20 - } - if offset < 0 { - offset = 0 - } - if usingDB() { - return loadDecisionRecordsDB(runID, limit, offset) - } - dir := decisionLogDir(runID) - entries, err := os.ReadDir(dir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return []*store.DecisionRecord{}, nil - } - return nil, err - } - type fileEntry struct { - path string - info os.DirEntry - } - files := make([]fileEntry, 0, len(entries)) - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - if !strings.HasPrefix(name, "decision_") || !strings.HasSuffix(name, ".json") { - continue - } - files = append(files, fileEntry{path: filepath.Join(dir, name), info: entry}) - } - sort.Slice(files, func(i, j int) bool { - infoI, _ := files[i].info.Info() - infoJ, _ := files[j].info.Info() - if infoI == nil || infoJ == nil { - return files[i].path > files[j].path - } - return infoI.ModTime().After(infoJ.ModTime()) - }) - if offset >= len(files) { - return []*store.DecisionRecord{}, nil - } - end := offset + limit - if end > len(files) { - end = len(files) - } - records := make([]*store.DecisionRecord, 0, end-offset) - for _, file := range files[offset:end] { - data, err := os.ReadFile(file.path) - if err != nil { - continue - } - var record store.DecisionRecord - if err := json.Unmarshal(data, &record); err != nil { - continue - } - records = append(records, &record) - } - return records, nil -} - -func CreateRunExport(runID string) (string, error) { - if usingDB() { - return createRunExportDB(runID) - } - root := runDir(runID) - if _, err := os.Stat(root); err != nil { - return "", err - } - tmpFile, err := os.CreateTemp("", fmt.Sprintf("%s-*.zip", runID)) - if err != nil { - return "", err - } - defer tmpFile.Close() - - zipWriter := zip.NewWriter(tmpFile) - err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - rel, err := filepath.Rel(root, path) - if err != nil { - return err - } - info, err := d.Info() - if err != nil { - return err - } - header, err := zip.FileInfoHeader(info) - if err != nil { - return err - } - header.Name = rel - header.Method = zip.Deflate - writer, err := zipWriter.CreateHeader(header) - if err != nil { - return err - } - src, err := os.Open(path) - if err != nil { - return err - } - if _, err := io.Copy(writer, src); err != nil { - src.Close() - return err - } - src.Close() - return nil - }) - if err != nil { - zipWriter.Close() - return "", err - } - if err := zipWriter.Close(); err != nil { - return "", err - } - return tmpFile.Name(), nil -} - -func persistDecisionRecord(runID string, record *store.DecisionRecord) { - if !usingDB() || record == nil { - return - } - _ = saveDecisionRecordDB(runID, record) -} diff --git a/backtest/storage_db_impl.go b/backtest/storage_db_impl.go deleted file mode 100644 index 2eb2b407..00000000 --- a/backtest/storage_db_impl.go +++ /dev/null @@ -1,498 +0,0 @@ -package backtest - -import ( - "archive/zip" - "database/sql" - "encoding/json" - "errors" - "fmt" - "os" - "time" - - "nofx/store" -) - -func saveCheckpointDB(runID string, ckpt *Checkpoint) error { - data, err := json.Marshal(ckpt) - if err != nil { - return err - } - _, err = persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_checkpoints (run_id, payload, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(run_id) DO UPDATE SET payload=excluded.payload, updated_at=CURRENT_TIMESTAMP - `), runID, data) - return err -} - -func loadCheckpointDB(runID string) (*Checkpoint, error) { - var payload []byte - err := persistenceDB.QueryRow(convertQuery(`SELECT payload FROM backtest_checkpoints WHERE run_id = ?`), runID).Scan(&payload) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, os.ErrNotExist - } - return nil, err - } - var ckpt Checkpoint - if err := json.Unmarshal(payload, &ckpt); err != nil { - return nil, err - } - return &ckpt, nil -} - -func saveConfigDB(runID string, cfg *BacktestConfig) error { - persist := *cfg - persist.AICfg.APIKey = "" - data, err := json.Marshal(&persist) - if err != nil { - return err - } - template := cfg.PromptTemplate - if template == "" { - template = "default" - } - now := time.Now().UTC().Format(time.RFC3339) - userID := cfg.UserID - if userID == "" { - userID = "default" - } - _, err = persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_runs (run_id, user_id, config_json, prompt_template, custom_prompt, override_prompt, ai_provider, ai_model, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(run_id) DO NOTHING - `), runID, userID, data, template, cfg.CustomPrompt, cfg.OverrideBasePrompt, cfg.AICfg.Provider, cfg.AICfg.Model, now, now) - if err != nil { - return err - } - _, err = persistenceDB.Exec(convertQuery(` - UPDATE backtest_runs - SET user_id = ?, config_json = ?, prompt_template = ?, custom_prompt = ?, override_prompt = ?, ai_provider = ?, ai_model = ?, updated_at = CURRENT_TIMESTAMP - WHERE run_id = ? - `), userID, data, template, cfg.CustomPrompt, cfg.OverrideBasePrompt, cfg.AICfg.Provider, cfg.AICfg.Model, runID) - return err -} - -func loadConfigDB(runID string) (*BacktestConfig, error) { - var payload []byte - err := persistenceDB.QueryRow(convertQuery(`SELECT config_json FROM backtest_runs WHERE run_id = ?`), runID).Scan(&payload) - if err != nil { - return nil, err - } - if len(payload) == 0 { - return nil, fmt.Errorf("config missing for %s", runID) - } - var cfg BacktestConfig - if err := json.Unmarshal(payload, &cfg); err != nil { - return nil, err - } - return &cfg, nil -} - -func saveRunMetadataDB(meta *RunMetadata) error { - created := meta.CreatedAt.UTC().Format(time.RFC3339) - updated := meta.UpdatedAt.UTC().Format(time.RFC3339) - userID := meta.UserID - if userID == "" { - userID = "default" - } - if _, err := persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_runs (run_id, user_id, label, last_error, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(run_id) DO NOTHING - `), meta.RunID, userID, meta.Label, meta.LastError, created, updated); err != nil { - return err - } - _, err := persistenceDB.Exec(convertQuery(` - UPDATE backtest_runs - SET user_id = ?, state = ?, symbol_count = ?, decision_tf = ?, processed_bars = ?, progress_pct = ?, equity_last = ?, max_drawdown_pct = ?, liquidated = ?, liquidation_note = ?, label = ?, last_error = ?, updated_at = ? - WHERE run_id = ? - `), userID, string(meta.State), meta.Summary.SymbolCount, meta.Summary.DecisionTF, meta.Summary.ProcessedBars, meta.Summary.ProgressPct, meta.Summary.EquityLast, meta.Summary.MaxDrawdownPct, meta.Summary.Liquidated, meta.Summary.LiquidationNote, meta.Label, meta.LastError, updated, meta.RunID) - return err -} - -func loadRunMetadataDB(runID string) (*RunMetadata, error) { - var ( - userID string - state string - label string - lastErr string - symbolCount int - decisionTF string - processedBars int - progressPct float64 - equityLast float64 - maxDD float64 - liquidated bool - liquidationNote string - createdISO string - updatedISO string - ) - err := persistenceDB.QueryRow(convertQuery(` - SELECT user_id, state, label, last_error, symbol_count, decision_tf, processed_bars, progress_pct, equity_last, max_drawdown_pct, liquidated, liquidation_note, created_at, updated_at - FROM backtest_runs WHERE run_id = ? - `), runID).Scan(&userID, &state, &label, &lastErr, &symbolCount, &decisionTF, &processedBars, &progressPct, &equityLast, &maxDD, &liquidated, &liquidationNote, &createdISO, &updatedISO) - if err != nil { - return nil, err - } - meta := &RunMetadata{ - RunID: runID, - UserID: userID, - Version: 1, - State: RunState(state), - Label: label, - LastError: lastErr, - Summary: RunSummary{ - SymbolCount: symbolCount, - DecisionTF: decisionTF, - ProcessedBars: processedBars, - ProgressPct: progressPct, - EquityLast: equityLast, - MaxDrawdownPct: maxDD, - Liquidated: liquidated, - LiquidationNote: liquidationNote, - }, - } - if meta.UserID == "" { - meta.UserID = "default" - } - if t, err := time.Parse(time.RFC3339, createdISO); err == nil { - meta.CreatedAt = t - } - if t, err := time.Parse(time.RFC3339, updatedISO); err == nil { - meta.UpdatedAt = t - } - return meta, nil -} - -func loadRunIDsDB() ([]string, error) { - rows, err := persistenceDB.Query(`SELECT run_id FROM backtest_runs ORDER BY updated_at DESC`) - if err != nil { - return nil, err - } - defer rows.Close() - var ids []string - for rows.Next() { - var runID string - if err := rows.Scan(&runID); err != nil { - return nil, err - } - ids = append(ids, runID) - } - return ids, rows.Err() -} - -func appendEquityPointDB(runID string, point EquityPoint) error { - _, err := persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_equity (run_id, ts, equity, available, pnl, pnl_pct, dd_pct, cycle) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `), runID, point.Timestamp, point.Equity, point.Available, point.PnL, point.PnLPct, point.DrawdownPct, point.Cycle) - return err -} - -func loadEquityPointsDB(runID string) ([]EquityPoint, error) { - rows, err := persistenceDB.Query(convertQuery(` - SELECT ts, equity, available, pnl, pnl_pct, dd_pct, cycle - FROM backtest_equity WHERE run_id = ? ORDER BY ts ASC - `), runID) - if err != nil { - return nil, err - } - defer rows.Close() - points := make([]EquityPoint, 0) - for rows.Next() { - var point EquityPoint - if err := rows.Scan(&point.Timestamp, &point.Equity, &point.Available, &point.PnL, &point.PnLPct, &point.DrawdownPct, &point.Cycle); err != nil { - return nil, err - } - points = append(points, point) - } - return points, rows.Err() -} - -func appendTradeEventDB(runID string, event TradeEvent) error { - _, err := persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_trades (run_id, ts, symbol, action, side, qty, price, fee, slippage, order_value, realized_pnl, leverage, cycle, position_after, liquidation, note) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), runID, event.Timestamp, event.Symbol, event.Action, event.Side, event.Quantity, event.Price, event.Fee, event.Slippage, event.OrderValue, event.RealizedPnL, event.Leverage, event.Cycle, event.PositionAfter, event.LiquidationFlag, event.Note) - return err -} - -func loadTradeEventsDB(runID string) ([]TradeEvent, error) { - rows, err := persistenceDB.Query(convertQuery(` - SELECT ts, symbol, action, side, qty, price, fee, slippage, order_value, realized_pnl, leverage, cycle, position_after, liquidation, note - FROM backtest_trades WHERE run_id = ? ORDER BY ts ASC - `), runID) - if err != nil { - return nil, err - } - defer rows.Close() - events := make([]TradeEvent, 0) - for rows.Next() { - var event TradeEvent - if err := rows.Scan(&event.Timestamp, &event.Symbol, &event.Action, &event.Side, &event.Quantity, &event.Price, &event.Fee, &event.Slippage, &event.OrderValue, &event.RealizedPnL, &event.Leverage, &event.Cycle, &event.PositionAfter, &event.LiquidationFlag, &event.Note); err != nil { - return nil, err - } - events = append(events, event) - } - return events, rows.Err() -} - -func saveMetricsDB(runID string, metrics *Metrics) error { - data, err := json.Marshal(metrics) - if err != nil { - return err - } - _, err = persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_metrics (run_id, payload, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(run_id) DO UPDATE SET payload=excluded.payload, updated_at=CURRENT_TIMESTAMP - `), runID, data) - return err -} - -func loadMetricsDB(runID string) (*Metrics, error) { - var payload []byte - err := persistenceDB.QueryRow(convertQuery(`SELECT payload FROM backtest_metrics WHERE run_id = ?`), runID).Scan(&payload) - if err != nil { - return nil, err - } - var metrics Metrics - if err := json.Unmarshal(payload, &metrics); err != nil { - return nil, err - } - return &metrics, nil -} - -func saveProgressDB(runID string, payload progressPayload) error { - _, err := persistenceDB.Exec(convertQuery(` - UPDATE backtest_runs - SET progress_pct = ?, equity_last = ?, processed_bars = ?, liquidated = ?, updated_at = ? - WHERE run_id = ? - `), payload.ProgressPct, payload.Equity, payload.BarIndex, payload.Liquidated, payload.UpdatedAtISO, runID) - return err -} - -func loadDecisionTraceDB(runID string, cycle int) (*store.DecisionRecord, error) { - var rows *sql.Rows - var err error - if cycle > 0 { - rows, err = persistenceDB.Query(convertQuery(`SELECT payload FROM backtest_decisions WHERE run_id = ? AND cycle = ? ORDER BY created_at DESC LIMIT 1`), runID, cycle) - } else { - rows, err = persistenceDB.Query(convertQuery(`SELECT payload FROM backtest_decisions WHERE run_id = ? ORDER BY created_at DESC LIMIT 1`), runID) - } - if err != nil { - return nil, err - } - defer rows.Close() - if !rows.Next() { - return nil, fmt.Errorf("decision trace not found for %s", runID) - } - var payload []byte - if err := rows.Scan(&payload); err != nil { - return nil, err - } - var record store.DecisionRecord - if err := json.Unmarshal(payload, &record); err != nil { - return nil, err - } - return &record, nil -} - -func saveDecisionRecordDB(runID string, record *store.DecisionRecord) error { - if record == nil { - return nil - } - data, err := json.Marshal(record) - if err != nil { - return err - } - _, err = persistenceDB.Exec(convertQuery(` - INSERT INTO backtest_decisions (run_id, cycle, payload) - VALUES (?, ?, ?) - `), runID, record.CycleNumber, data) - return err -} - -func loadDecisionRecordsDB(runID string, limit, offset int) ([]*store.DecisionRecord, error) { - rows, err := persistenceDB.Query(convertQuery(` - SELECT payload FROM backtest_decisions - WHERE run_id = ? - ORDER BY id DESC - LIMIT ? OFFSET ? - `), runID, limit, offset) - if err != nil { - return nil, err - } - defer rows.Close() - records := make([]*store.DecisionRecord, 0, limit) - for rows.Next() { - var payload []byte - if err := rows.Scan(&payload); err != nil { - return nil, err - } - var record store.DecisionRecord - if err := json.Unmarshal(payload, &record); err != nil { - return nil, err - } - records = append(records, &record) - } - return records, rows.Err() -} - -func createRunExportDB(runID string) (string, error) { - tmpFile, err := os.CreateTemp("", fmt.Sprintf("%s-*.zip", runID)) - if err != nil { - return "", err - } - defer tmpFile.Close() - - zipWriter := zip.NewWriter(tmpFile) - defer zipWriter.Close() - - if meta, err := loadRunMetadataDB(runID); err == nil { - if err := writeJSONToZip(zipWriter, "run.json", meta); err != nil { - return "", err - } - } - if cfg, err := loadConfigDB(runID); err == nil { - if err := writeJSONToZip(zipWriter, "config.json", cfg); err != nil { - return "", err - } - } - if ckpt, err := loadCheckpointDB(runID); err == nil { - if err := writeJSONToZip(zipWriter, "checkpoint.json", ckpt); err != nil { - return "", err - } - } - if metrics, err := loadMetricsDB(runID); err == nil { - if err := writeJSONToZip(zipWriter, "metrics.json", metrics); err != nil { - return "", err - } - } - if points, err := loadEquityPointsDB(runID); err == nil && len(points) > 0 { - if err := writeJSONLinesToZip(zipWriter, "equity.jsonl", points); err != nil { - return "", err - } - } - if trades, err := loadTradeEventsDB(runID); err == nil && len(trades) > 0 { - if err := writeJSONLinesToZip(zipWriter, "trades.jsonl", trades); err != nil { - return "", err - } - } - if err := writeDecisionLogsToZip(zipWriter, runID); err != nil { - return "", err - } - - if err := zipWriter.Close(); err != nil { - return "", err - } - if err := tmpFile.Sync(); err != nil { - return "", err - } - return tmpFile.Name(), nil -} - -func writeJSONToZip(z *zip.Writer, name string, value any) error { - data, err := json.MarshalIndent(value, "", " ") - if err != nil { - return err - } - w, err := z.Create(name) - if err != nil { - return err - } - _, err = w.Write(data) - return err -} - -func writeJSONLinesToZip[T any](z *zip.Writer, name string, items []T) error { - w, err := z.Create(name) - if err != nil { - return err - } - for _, item := range items { - data, err := json.Marshal(item) - if err != nil { - return err - } - if _, err := w.Write(data); err != nil { - return err - } - if _, err := w.Write([]byte("\n")); err != nil { - return err - } - } - return nil -} - -func writeDecisionLogsToZip(z *zip.Writer, runID string) error { - rows, err := persistenceDB.Query(convertQuery(` - SELECT id, cycle, payload FROM backtest_decisions - WHERE run_id = ? ORDER BY id ASC - `), runID) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - id int64 - cycle int - payload []byte - ) - if err := rows.Scan(&id, &cycle, &payload); err != nil { - return err - } - name := fmt.Sprintf("decision_logs/decision_%d_cycle%d.json", id, cycle) - w, err := z.Create(name) - if err != nil { - return err - } - if _, err := w.Write(payload); err != nil { - return err - } - } - return rows.Err() -} - -func listIndexEntriesDB() ([]RunIndexEntry, error) { - rows, err := persistenceDB.Query(` - SELECT run_id, state, symbol_count, decision_tf, equity_last, max_drawdown_pct, created_at, updated_at, config_json - FROM backtest_runs - ORDER BY updated_at DESC - `) - if err != nil { - return nil, err - } - defer rows.Close() - var entries []RunIndexEntry - for rows.Next() { - var ( - entry RunIndexEntry - createdISO string - updatedISO string - cfgJSON []byte - symbolCnt int - ) - if err := rows.Scan(&entry.RunID, &entry.State, &symbolCnt, &entry.DecisionTF, &entry.EquityLast, &entry.MaxDrawdownPct, &createdISO, &updatedISO, &cfgJSON); err != nil { - return nil, err - } - entry.CreatedAtISO = createdISO - entry.UpdatedAtISO = updatedISO - entry.Symbols = make([]string, 0, symbolCnt) - var cfg BacktestConfig - if len(cfgJSON) > 0 && json.Unmarshal(cfgJSON, &cfg) == nil { - entry.Symbols = append([]string(nil), cfg.Symbols...) - entry.StartTS = cfg.StartTS - entry.EndTS = cfg.EndTS - } - entries = append(entries, entry) - } - return entries, rows.Err() -} - -func deleteRunDB(runID string) error { - _, err := persistenceDB.Exec(convertQuery(`DELETE FROM backtest_runs WHERE run_id = ?`), runID) - return err -} diff --git a/backtest/types.go b/backtest/types.go deleted file mode 100644 index f9c1295c..00000000 --- a/backtest/types.go +++ /dev/null @@ -1,179 +0,0 @@ -package backtest - -import "time" - -// RunState represents the current state of a backtest run. -type RunState string - -const ( - RunStateCreated RunState = "created" - RunStateRunning RunState = "running" - RunStatePaused RunState = "paused" - RunStateStopped RunState = "stopped" - RunStateCompleted RunState = "completed" - RunStateFailed RunState = "failed" - RunStateLiquidated RunState = "liquidated" -) - -// PositionSnapshot represents core position data for backtest state and persistence. -type PositionSnapshot struct { - Symbol string `json:"symbol"` - Side string `json:"side"` - Quantity float64 `json:"quantity"` - AvgPrice float64 `json:"avg_price"` - Leverage int `json:"leverage"` - LiquidationPrice float64 `json:"liquidation_price"` - MarginUsed float64 `json:"margin_used"` - OpenTime int64 `json:"open_time"` - AccumulatedFee float64 `json:"accumulated_fee,omitempty"` // Opening fees accumulated -} - -// BacktestState represents the real-time state during execution (in-memory state). -type BacktestState struct { - BarIndex int - BarTimestamp int64 - DecisionCycle int - - Cash float64 - Equity float64 - UnrealizedPnL float64 - RealizedPnL float64 - MaxEquity float64 - MinEquity float64 - MaxDrawdownPct float64 - Positions map[string]PositionSnapshot - LastUpdate time.Time - Liquidated bool - LiquidationNote string -} - -// EquityPoint represents a single point on the equity curve. -type EquityPoint struct { - Timestamp int64 `json:"ts"` - Equity float64 `json:"equity"` - Available float64 `json:"available"` - PnL float64 `json:"pnl"` - PnLPct float64 `json:"pnl_pct"` - DrawdownPct float64 `json:"dd_pct"` - Cycle int `json:"cycle"` -} - -// TradeEvent records a trade execution result or special event (such as liquidation). -type TradeEvent struct { - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - Action string `json:"action"` - Side string `json:"side,omitempty"` - Quantity float64 `json:"qty"` - Price float64 `json:"price"` - Fee float64 `json:"fee"` - Slippage float64 `json:"slippage"` - OrderValue float64 `json:"order_value"` - RealizedPnL float64 `json:"realized_pnl"` - Leverage int `json:"leverage,omitempty"` - Cycle int `json:"cycle"` - PositionAfter float64 `json:"position_after"` - LiquidationFlag bool `json:"liquidation"` - Note string `json:"note,omitempty"` -} - -// Metrics summarizes backtest performance metrics. -type Metrics struct { - TotalReturnPct float64 `json:"total_return_pct"` - MaxDrawdownPct float64 `json:"max_drawdown_pct"` - SharpeRatio float64 `json:"sharpe_ratio"` - ProfitFactor float64 `json:"profit_factor"` - WinRate float64 `json:"win_rate"` - Trades int `json:"trades"` - AvgWin float64 `json:"avg_win"` - AvgLoss float64 `json:"avg_loss"` - BestSymbol string `json:"best_symbol"` - WorstSymbol string `json:"worst_symbol"` - SymbolStats map[string]SymbolMetrics `json:"symbol_stats"` - Liquidated bool `json:"liquidated"` -} - -// SymbolMetrics records performance for a single symbol. -type SymbolMetrics struct { - TotalTrades int `json:"total_trades"` - WinningTrades int `json:"winning_trades"` - LosingTrades int `json:"losing_trades"` - TotalPnL float64 `json:"total_pnl"` - AvgPnL float64 `json:"avg_pnl"` - WinRate float64 `json:"win_rate"` -} - -// Checkpoint represents checkpoint information saved to disk for pause, resume, and crash recovery. -type Checkpoint struct { - BarIndex int `json:"bar_index"` - BarTimestamp int64 `json:"bar_ts"` - Cash float64 `json:"cash"` - Equity float64 `json:"equity"` - MaxEquity float64 `json:"max_equity"` - MinEquity float64 `json:"min_equity"` - MaxDrawdownPct float64 `json:"max_drawdown_pct"` - UnrealizedPnL float64 `json:"unrealized_pnl"` - RealizedPnL float64 `json:"realized_pnl"` - Positions []PositionSnapshot `json:"positions"` - DecisionCycle int `json:"decision_cycle"` - IndicatorsState map[string]map[string]any `json:"indicators_state,omitempty"` - RNGSeed int64 `json:"rng_seed,omitempty"` - AICacheRef string `json:"ai_cache_ref,omitempty"` - Liquidated bool `json:"liquidated"` - LiquidationNote string `json:"liquidation_note,omitempty"` -} - -// RunMetadata records the summary required for run.json. -type RunMetadata struct { - RunID string `json:"run_id"` - Label string `json:"label,omitempty"` - UserID string `json:"user_id,omitempty"` - LastError string `json:"last_error,omitempty"` - Version int `json:"version"` - State RunState `json:"state"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Summary RunSummary `json:"summary"` -} - -// RunSummary represents the summary field in run.json. -type RunSummary struct { - SymbolCount int `json:"symbol_count"` - DecisionTF string `json:"decision_tf"` - ProcessedBars int `json:"processed_bars"` - ProgressPct float64 `json:"progress_pct"` - EquityLast float64 `json:"equity_last"` - MaxDrawdownPct float64 `json:"max_drawdown_pct"` - Liquidated bool `json:"liquidated"` - LiquidationNote string `json:"liquidation_note,omitempty"` -} - -// StatusPayload is used for /status API responses. -type StatusPayload struct { - RunID string `json:"run_id"` - State RunState `json:"state"` - ProgressPct float64 `json:"progress_pct"` - ProcessedBars int `json:"processed_bars"` - CurrentTime int64 `json:"current_time"` - DecisionCycle int `json:"decision_cycle"` - Equity float64 `json:"equity"` - UnrealizedPnL float64 `json:"unrealized_pnl"` - RealizedPnL float64 `json:"realized_pnl"` - Positions []PositionStatus `json:"positions,omitempty"` - Note string `json:"note,omitempty"` - LastError string `json:"last_error,omitempty"` - LastUpdatedIso string `json:"last_updated_iso"` -} - -// PositionStatus represents a position with unrealized P&L for status display. -type PositionStatus struct { - Symbol string `json:"symbol"` - Side string `json:"side"` - Quantity float64 `json:"quantity"` - EntryPrice float64 `json:"entry_price"` - MarkPrice float64 `json:"mark_price"` - Leverage int `json:"leverage"` - UnrealizedPnL float64 `json:"unrealized_pnl"` - UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"` - MarginUsed float64 `json:"margin_used"` -} diff --git a/docs/architecture/BACKTEST_MODULE.md b/docs/architecture/BACKTEST_MODULE.md deleted file mode 100644 index 820c1e2b..00000000 --- a/docs/architecture/BACKTEST_MODULE.md +++ /dev/null @@ -1,624 +0,0 @@ -# NOFX Backtest Module - Technical Documentation - -**Language:** [English](BACKTEST_MODULE.md) | [中文](BACKTEST_MODULE.zh-CN.md) - -## Overview - -This document describes the complete technical implementation of the NOFX backtest module, including configuration, historical data loading, simulation engine, AI decision making, performance metrics calculation, and result storage. - ---- - -## Complete Backtest Flow - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Backtest Execution Flow │ -└─────────────────────────────────────────────────────────────────┘ - -1. API Request: /backtest/start - ↓ -2. Manager.Start() - ├─ Validate config - ├─ Parse AI model - ├─ Create Runner instance - └─ Start runner.Start() (goroutine) - ↓ -3. Runner.Start() → Runner.loop() - └─ Iterate each decision time point: - ├─ DataFeed.BuildMarketData() [Build market data] - ├─ Check decision trigger [Every N bars] - ├─ buildDecisionContext() [Build decision context] - ├─ invokeAIWithRetry() [Call AI + cache] - ├─ executeDecision() [Execute trades] - ├─ checkLiquidation() [Check liquidation] - ├─ updateState() [Update state] - ├─ appendEquityPoint() [Record equity] - ├─ appendTradeEvent() [Record trades] - ├─ maybeCheckpoint() [Save checkpoint] - └─ persistMetrics() [Persist metrics] - ↓ -4. Complete/Failed - ├─ Calculate final metrics - ├─ Persist all results - └─ Release lock - ↓ -5. API Query: /backtest/metrics, /backtest/equity, /backtest/trades - └─ Load and return results -``` - ---- - -## 1. Configuration - -**Core File:** `backtest/config.go` - -### 1.1 Config Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `RunID` | string | (required) | Unique backtest run ID | -| `UserID` | string | "default" | User ID | -| `Symbols` | []string | (required) | Trading symbols list | -| `Timeframes` | []string | ["3m", "15m", "4h"] | K-line timeframes | -| `DecisionTimeframe` | string | Symbols[0] | Primary decision timeframe | -| `DecisionCadenceNBars` | int | 20 | Trigger decision every N bars | -| `StartTS`, `EndTS` | int64 | (required) | Backtest time range (Unix timestamp) | -| `InitialBalance` | float64 | 1000 | Initial balance (USD) | -| `FeeBps` | float64 | 5 | Trading fee (basis points) | -| `SlippageBps` | float64 | 2 | Slippage (basis points) | -| `FillPolicy` | string | "next_open" | Fill policy | -| `PromptVariant` | string | "baseline" | AI prompt variant | -| `CacheAI` | bool | false | Cache AI decisions | -| `Leverage` | LeverageConfig | BTC/ETH:5, Altcoin:5 | Leverage settings | - -### 1.2 Fill Policy - -```go -// backtest/config.go:163-179 -switch fillPolicy { -case "next_open": // Next bar open price -case "bar_vwap": // Current bar VWAP -case "mid": // Current bar (High+Low)/2 -default: // Mark Price -} -``` - -### 1.3 Config Example - -```go -cfg := backtest.BacktestConfig{ - RunID: "bt_20231215_150405", - Symbols: []string{"BTCUSDT", "ETHUSDT"}, - Timeframes: []string{"3m", "15m", "4h"}, - DecisionTimeframe: "3m", - DecisionCadenceNBars: 20, - StartTS: 1702566000, - EndTS: 1702652400, - InitialBalance: 10000, - FeeBps: 5, - SlippageBps: 2, - FillPolicy: "next_open", -} -``` - ---- - -## 2. Data Loading - -**Core File:** `backtest/datafeed.go` - -### 2.1 Data Loading Flow - -``` -1. NewDataFeed() - Initialize - ↓ -2. loadAll() - Load all historical data - ├─ Calculate buffer (200 bars before StartTS) - ├─ Call market.GetKlinesRange() to fetch data - ├─ Store in symbolSeries map - └─ Build decision timeline from primary timeframe - ↓ -3. BuildMarketData() - Build market data snapshot - ├─ Slice K-line data to current timestamp - ├─ Calculate technical indicators (EMA, MACD, RSI, ATR) - └─ Return market.Data structure -``` - -### 2.2 Data Structure - -```go -// DataFeed core structure -type DataFeed struct { - decisionTimes []int64 // Decision time points list - symbolSeries map[string]*symbolSeries // Data stored by symbol -} - -// Single symbol time series -type symbolSeries struct { - timeframes map[string]*timeframeSeries // Stored by timeframe -} - -// Single timeframe data -type timeframeSeries struct { - klines []market.Kline // K-line data - closeTimes []int64 // Close time index -} -``` - -### 2.3 Key Code References - -- Data fetching: `backtest/datafeed.go:48-93` -- Timeline generation: `backtest/datafeed.go:96-115` -- Market data assembly: `backtest/datafeed.go:141-171` - ---- - -## 3. Simulation Engine - -**Core File:** `backtest/runner.go` - -### 3.1 Main Loop - -```go -// backtest/runner.go:232-264 -func (r *Runner) loop() { - for _, ts := range r.feed.DecisionTimes() { - if r.isPaused() { - break - } - r.stepOnce(ts) - } -} -``` - -### 3.2 Single Step Execution - -```go -// backtest/runner.go:266-471 -func (r *Runner) stepOnce(ts int64) { - // 1. Get current bar timestamp - // 2. Build market data - // 3. Check decision trigger (every N bars) - // 4. Execute decision cycle (if triggered) - // 5. Check liquidation - // 6. Update state and record -} -``` - -### 3.3 State Management - -```go -// backtest/types.go:31-47 -type BacktestState struct { - BarIndex int // Current bar index - Cash float64 // Available balance - Equity float64 // Total equity - UnrealizedPnL float64 // Unrealized PnL - RealizedPnL float64 // Realized PnL - MaxEquity float64 // Peak equity - MinEquity float64 // Trough equity - MaxDrawdownPct float64 // Max drawdown - Positions map[string]*position // Positions -} -``` - ---- - -## 4. AI Decision Making - -**Core File:** `backtest/runner.go` - -### 4.1 Decision Context Building - -```go -// backtest/runner.go:473-532 -func (r *Runner) buildDecisionContext() *decision.Context { - return &decision.Context{ - CurrentTime: "2023-12-15 10:30:00 UTC", - RuntimeMinutes: elapsed, - CallCount: cycleNumber, - Account: { - TotalEquity, AvailableBalance, TotalPnL, MarginUsedPct - }, - Positions: []PositionInfo{...}, - CandidateCoins: []string{symbols...}, - MarketDataMap: map[symbol]*market.Data{...}, - MultiTFMarket: map[symbol]map[timeframe]*market.Data{...}, - } -} -``` - -### 4.2 AI Invocation - -```go -// backtest/runner.go:544-563 -func (r *Runner) invokeAIWithRetry() (*decision.FullDecision, error) { - // Max 3 retries - // Exponential backoff: 500ms, 1000ms, 1500ms - // Uses decision.GetFullDecisionWithStrategy() for unified prompt generation -} -``` - -### 4.3 AI Cache - -```go -// backtest/aicache.go:127-168 -// Cache key: SHA256(context payload) -// Contains: variant, timestamp, account, positions, market data -``` - -### 4.4 Supported AI Models - -| Model | Client File | -|-------|-------------| -| DeepSeek | `mcp/deepseek_client.go` | -| Qwen | `mcp/qwen_client.go` | -| Claude | `mcp/claude_client.go` | -| Gemini | `mcp/gemini_client.go` | -| Grok | `mcp/grok_client.go` | -| OpenAI | `mcp/openai_client.go` | -| Kimi | `mcp/kimi_client.go` | - ---- - -## 5. Performance Metrics - -**Core File:** `backtest/metrics.go` - -### 5.1 Metrics Calculation - -| Metric | Formula | Code Location | -|--------|---------|---------------| -| **Total Return** | (Final Equity - Initial) / Initial × 100 | metrics.go:36-42 | -| **Max Drawdown** | max((Peak - Current) / Peak × 100) | metrics.go:64-91 | -| **Sharpe Ratio** | Avg Return / Return StdDev | metrics.go:94-138 | -| **Win Rate** | Winning Trades / Total Trades × 100 | metrics.go:180-181 | -| **Profit Factor** | Total Profit / Total Loss | metrics.go:189-193 | - -### 5.2 Trade Statistics - -```go -// backtest/metrics.go:141-225 -type TradeMetrics struct { - TotalTrades int - WinningTrades int - LosingTrades int - AvgWin float64 - AvgLoss float64 - BestSymbol string - WorstSymbol string - SymbolStats map[string]*SymbolStat -} -``` - ---- - -## 6. Equity Curve - -**Core File:** `backtest/equity.go` - -### 6.1 Equity Point Structure - -```json -{ - "ts": 1702566000000, - "equity": 10500.50, - "available": 8000.00, - "pnl": 500.50, - "pnl_pct": 5.005, - "dd_pct": 2.34, - "cycle": 42 -} -``` - -### 6.2 Equity Update - -```go -// backtest/runner.go:829-872 -func (r *Runner) updateState() { - // 1. Calculate total equity: cash + margin + unrealized PnL - // 2. Track peak (MaxEquity) - // 3. Track trough (MinEquity) - // 4. Recalculate drawdown: (MaxEquity - Equity) / MaxEquity × 100 -} -``` - -### 6.3 Data Resampling - -```go -// backtest/equity.go:10-50 -func ResampleEquity(points []EquityPoint, timeframe string) []EquityPoint { - // Bucket by timeframe - // Keep last point in each bucket -} -``` - ---- - -## 7. Result Storage - -**Core Files:** `backtest/storage.go`, `store/backtest.go` - -### 7.1 File Storage Structure - -``` -backtests/ -├── / -│ ├── run.json # Run metadata -│ ├── checkpoint.json # Checkpoint (for resume) -│ ├── equity.jsonl # Equity curve (line-delimited JSON) -│ ├── trades.jsonl # Trade records (line-delimited JSON) -│ ├── metrics.json # Performance metrics -│ ├── progress.json # Progress info -│ ├── ai_cache.json # AI decision cache -│ └── decision_logs/ # Decision logs -│ ├── 0.json -│ ├── 1.json -│ └── ... -``` - -### 7.2 Database Schema - -```sql --- Backtest run metadata -CREATE TABLE backtest_runs ( - run_id TEXT PRIMARY KEY, - user_id TEXT, - config_json TEXT, - state TEXT, -- pending, running, completed, failed - processed_bars INTEGER, - progress_pct REAL, - equity_last REAL, - max_drawdown_pct REAL, - liquidated BOOLEAN, - ai_provider TEXT, - ai_model TEXT, - created_at DATETIME, - updated_at DATETIME -); - --- Equity curve -CREATE TABLE backtest_equity ( - id INTEGER PRIMARY KEY, - run_id TEXT, - ts INTEGER, - equity REAL, - available REAL, - pnl REAL, - pnl_pct REAL, - dd_pct REAL, - cycle INTEGER -); - --- Trade records -CREATE TABLE backtest_trades ( - id INTEGER PRIMARY KEY, - run_id TEXT, - ts INTEGER, - symbol TEXT, - action TEXT, - side TEXT, - qty REAL, - price REAL, - fee REAL, - slippage REAL, - realized_pnl REAL, - leverage INTEGER, - liquidation BOOLEAN -); - --- Performance metrics -CREATE TABLE backtest_metrics ( - run_id TEXT PRIMARY KEY, - payload BLOB, - updated_at DATETIME -); - --- Checkpoints (pause/resume) -CREATE TABLE backtest_checkpoints ( - run_id TEXT PRIMARY KEY, - payload BLOB, - updated_at DATETIME -); -``` - ---- - -## 8. API Endpoints - -**Core File:** `api/backtest.go` - -### 8.1 Endpoint List - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/backtest/start` | POST | Start backtest | -| `/backtest/pause` | POST | Pause backtest | -| `/backtest/resume` | POST | Resume backtest | -| `/backtest/stop` | POST | Stop backtest | -| `/backtest/status` | GET | Get status | -| `/backtest/runs` | GET | List all backtests | -| `/backtest/equity` | GET | Get equity curve | -| `/backtest/trades` | GET | Get trade records | -| `/backtest/metrics` | GET | Get performance metrics | -| `/backtest/trace` | GET | Get decision logs | -| `/backtest/export` | GET | Export ZIP | -| `/backtest/delete` | POST | Delete backtest | - -### 8.2 Request Examples - -```bash -# Start backtest -POST /backtest/start -{ - "config": { - "run_id": "bt_20231215", - "symbols": ["BTCUSDT", "ETHUSDT"], - "timeframes": ["3m", "15m", "4h"], - "start_ts": 1702566000, - "end_ts": 1702652400, - "initial_balance": 10000, - "ai_model_id": "model_001" - } -} - -# Get equity curve -GET /backtest/equity?run_id=bt_20231215&tf=1h&limit=1000 - -# Get metrics -GET /backtest/metrics?run_id=bt_20231215 -``` - -### 8.3 Response Examples - -```json -// Status response -{ - "run_id": "bt_20231215", - "state": "running", - "progress_pct": 45.5, - "processed_bars": 1234, - "equity": 10234.50, - "unrealized_pnl": 234.50 -} - -// Metrics response -{ - "total_return_pct": 12.34, - "max_drawdown_pct": 5.67, - "sharpe_ratio": 1.89, - "profit_factor": 2.34, - "win_rate": 65.5, - "trades": 123 -} -``` - ---- - -## 9. Account & Position Management - -**Core File:** `backtest/account.go` - -### 9.1 Position Structure - -```go -type position struct { - Symbol string - Side string // "long" or "short" - Quantity float64 - EntryPrice float64 - Leverage int - Margin float64 // Margin - Notional float64 // Notional value - LiquidationPrice float64 // Liquidation price - OpenTime int64 -} -``` - -### 9.2 Open Position Logic - -```go -// backtest/account.go:61-104 -func (a *BacktestAccount) Open(symbol, side string, qty, price float64, leverage int) { - // 1. Apply slippage - // 2. Calculate notional value (qty × price) - // 3. Calculate margin (notional / leverage) - // 4. Deduct margin + fees - // 5. Create/add to position - // 6. Calculate liquidation price -} -``` - -### 9.3 Close Position Logic - -```go -// backtest/account.go:106-140 -func (a *BacktestAccount) Close(symbol, side string, qty, price float64) { - // 1. Verify position exists - // 2. Apply slippage (reverse direction) - // 3. Calculate realized PnL - // long: (exit - entry) × qty - // short: (entry - exit) × qty - // 4. Return margin + PnL - fees - // 5. Update/delete position -} -``` - -### 9.4 Liquidation Price Calculation - -```go -// backtest/account.go:177-186 -func computeLiquidation(entry float64, leverage int, side string) float64 { - if side == "long" { - return entry * (1 - 1.0/float64(leverage)) // Long: liquidate on drop - } - return entry * (1 + 1.0/float64(leverage)) // Short: liquidate on rise -} -``` - ---- - -## 10. Checkpoint & Resume - -**Core File:** `backtest/runner.go` - -### 10.1 Checkpoint Structure - -```json -{ - "bar_index": 1234, - "bar_ts": 1702609200000, - "cash": 8000.00, - "equity": 10234.50, - "max_equity": 10500.00, - "max_drawdown_pct": 5.67, - "positions": [...], - "decision_cycle": 62, - "liquidated": false -} -``` - -### 10.2 Checkpoint Trigger - -```go -// backtest/runner.go:874-898 -func (r *Runner) maybeCheckpoint() { - // Save every N bars - // Or save every N seconds -} -``` - -### 10.3 Resume Flow - -```go -func (r *Runner) RestoreFromCheckpoint() { - // 1. Load checkpoint - // 2. Restore account state - // 3. Restore bar index (continue from next bar) - // 4. Restore equity curve, trade records -} -``` - ---- - -## Core File Index - -| Module | File | Key Methods | -|--------|------|-------------| -| **Config** | `backtest/config.go` | `BacktestConfig`, `Validate()` | -| **Data Loading** | `backtest/datafeed.go` | `NewDataFeed()`, `loadAll()`, `BuildMarketData()` | -| **Sim Engine** | `backtest/runner.go` | `Start()`, `loop()`, `stepOnce()` | -| **Decision** | `backtest/runner.go` | `buildDecisionContext()`, `invokeAIWithRetry()` | -| **Execution** | `backtest/runner.go` | `executeDecision()` | -| **Account** | `backtest/account.go` | `Open()`, `Close()`, `TotalEquity()` | -| **Metrics** | `backtest/metrics.go` | `CalculateMetrics()` | -| **Equity** | `backtest/equity.go` | `ResampleEquity()`, `LimitEquityPoints()` | -| **Storage** | `backtest/storage.go` | `SaveCheckpoint()`, `appendEquityPoint()` | -| **Database** | `store/backtest.go` | Schema and CRUD operations | -| **API** | `api/backtest.go` | HTTP handlers | -| **AI Cache** | `backtest/aicache.go` | `Get()`, `Put()`, `save()` | - ---- - -**Document Version:** 1.0.0 -**Last Updated:** 2025-01-15 diff --git a/docs/architecture/BACKTEST_MODULE.zh-CN.md b/docs/architecture/BACKTEST_MODULE.zh-CN.md deleted file mode 100644 index 8ec1938c..00000000 --- a/docs/architecture/BACKTEST_MODULE.zh-CN.md +++ /dev/null @@ -1,624 +0,0 @@ -# NOFX 回测模块技术文档 - -**语言:** [English](BACKTEST_MODULE.md) | [中文](BACKTEST_MODULE.zh-CN.md) - -## 概述 - -本文档详细描述 NOFX 回测模块的完整技术实现,包括配置、历史数据加载、模拟引擎、AI 决策、性能指标计算和结果存储。 - ---- - -## 完整回测流程图 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 回测执行流程 │ -└─────────────────────────────────────────────────────────────────┘ - -1. API 请求: /backtest/start - ↓ -2. Manager.Start() - ├─ 验证配置 - ├─ 解析 AI 模型 - ├─ 创建 Runner 实例 - └─ 启动 runner.Start() (goroutine) - ↓ -3. Runner.Start() → Runner.loop() - └─ 遍历每个决策时间点: - ├─ DataFeed.BuildMarketData() [构建市场数据] - ├─ 检查决策触发条件 [每 N 根 K 线] - ├─ buildDecisionContext() [构建决策上下文] - ├─ invokeAIWithRetry() [调用 AI + 缓存] - ├─ executeDecision() [执行交易] - ├─ checkLiquidation() [检查爆仓] - ├─ updateState() [更新状态] - ├─ appendEquityPoint() [记录权益] - ├─ appendTradeEvent() [记录交易] - ├─ maybeCheckpoint() [保存检查点] - └─ persistMetrics() [持久化指标] - ↓ -4. 完成/失败 - ├─ 计算最终指标 - ├─ 持久化所有结果 - └─ 释放锁 - ↓ -5. API 查询: /backtest/metrics, /backtest/equity, /backtest/trades - └─ 加载并返回结果 -``` - ---- - -## 1. 回测配置 (Configuration) - -**核心文件:** `backtest/config.go` - -### 1.1 配置参数 - -| 参数 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `RunID` | string | (必填) | 回测运行唯一标识 | -| `UserID` | string | "default" | 用户 ID | -| `Symbols` | []string | (必填) | 交易币种列表 | -| `Timeframes` | []string | ["3m", "15m", "4h"] | K 线周期 | -| `DecisionTimeframe` | string | Symbols[0] | 主决策周期 | -| `DecisionCadenceNBars` | int | 20 | 每 N 根 K 线触发一次决策 | -| `StartTS`, `EndTS` | int64 | (必填) | 回测时间范围 (Unix 时间戳) | -| `InitialBalance` | float64 | 1000 | 初始资金 (USD) | -| `FeeBps` | float64 | 5 | 手续费 (基点) | -| `SlippageBps` | float64 | 2 | 滑点 (基点) | -| `FillPolicy` | string | "next_open" | 成交策略 | -| `PromptVariant` | string | "baseline" | AI 提示词变体 | -| `CacheAI` | bool | false | 是否缓存 AI 决策 | -| `Leverage` | LeverageConfig | BTC/ETH:5, Altcoin:5 | 杠杆设置 | - -### 1.2 成交策略 (Fill Policy) - -```go -// backtest/config.go:163-179 -switch fillPolicy { -case "next_open": // 下一根 K 线开盘价 -case "bar_vwap": // 当前 K 线 VWAP -case "mid": // 当前 K 线 (High+Low)/2 -default: // Mark Price -} -``` - -### 1.3 配置示例 - -```go -cfg := backtest.BacktestConfig{ - RunID: "bt_20231215_150405", - Symbols: []string{"BTCUSDT", "ETHUSDT"}, - Timeframes: []string{"3m", "15m", "4h"}, - DecisionTimeframe: "3m", - DecisionCadenceNBars: 20, - StartTS: 1702566000, - EndTS: 1702652400, - InitialBalance: 10000, - FeeBps: 5, - SlippageBps: 2, - FillPolicy: "next_open", -} -``` - ---- - -## 2. 历史数据加载 (Data Loading) - -**核心文件:** `backtest/datafeed.go` - -### 2.1 数据加载流程 - -``` -1. NewDataFeed() - 初始化 - ↓ -2. loadAll() - 加载所有历史数据 - ├─ 计算缓冲区 (StartTS 前 200 根 K 线) - ├─ 调用 market.GetKlinesRange() 获取数据 - ├─ 存储到 symbolSeries map - └─ 从主周期构建决策时间线 - ↓ -3. BuildMarketData() - 构建市场数据快照 - ├─ 切片 K 线数据到当前时间戳 - ├─ 计算技术指标 (EMA, MACD, RSI, ATR) - └─ 返回 market.Data 结构 -``` - -### 2.2 数据结构 - -```go -// DataFeed 核心结构 -type DataFeed struct { - decisionTimes []int64 // 决策时间点列表 - symbolSeries map[string]*symbolSeries // 按币种存储的数据 -} - -// 单币种时间序列 -type symbolSeries struct { - timeframes map[string]*timeframeSeries // 按周期存储 -} - -// 单周期数据 -type timeframeSeries struct { - klines []market.Kline // K 线数据 - closeTimes []int64 // 收盘时间索引 -} -``` - -### 2.3 关键代码引用 - -- 数据获取: `backtest/datafeed.go:48-93` -- 时间线生成: `backtest/datafeed.go:96-115` -- 市场数据组装: `backtest/datafeed.go:141-171` - ---- - -## 3. 模拟引擎 (Simulation Engine) - -**核心文件:** `backtest/runner.go` - -### 3.1 主循环 - -```go -// backtest/runner.go:232-264 -func (r *Runner) loop() { - for _, ts := range r.feed.DecisionTimes() { - if r.isPaused() { - break - } - r.stepOnce(ts) - } -} -``` - -### 3.2 单步执行 - -```go -// backtest/runner.go:266-471 -func (r *Runner) stepOnce(ts int64) { - // 1. 获取当前 K 线时间戳 - // 2. 构建市场数据 - // 3. 检查决策触发条件 (每 N 根 K 线) - // 4. 执行决策周期 (如果触发) - // 5. 检查爆仓 - // 6. 更新状态并记录 -} -``` - -### 3.3 状态管理 - -```go -// backtest/types.go:31-47 -type BacktestState struct { - BarIndex int // 当前 K 线索引 - Cash float64 // 可用余额 - Equity float64 // 总权益 - UnrealizedPnL float64 // 未实现盈亏 - RealizedPnL float64 // 已实现盈亏 - MaxEquity float64 // 最高权益 - MinEquity float64 // 最低权益 - MaxDrawdownPct float64 // 最大回撤 - Positions map[string]*position // 持仓 -} -``` - ---- - -## 4. AI 决策 (AI Decision Making) - -**核心文件:** `backtest/runner.go` - -### 4.1 决策上下文构建 - -```go -// backtest/runner.go:473-532 -func (r *Runner) buildDecisionContext() *decision.Context { - return &decision.Context{ - CurrentTime: "2023-12-15 10:30:00 UTC", - RuntimeMinutes: elapsed, - CallCount: cycleNumber, - Account: { - TotalEquity, AvailableBalance, TotalPnL, MarginUsedPct - }, - Positions: []PositionInfo{...}, - CandidateCoins: []string{symbols...}, - MarketDataMap: map[symbol]*market.Data{...}, - MultiTFMarket: map[symbol]map[timeframe]*market.Data{...}, - } -} -``` - -### 4.2 AI 调用 - -```go -// backtest/runner.go:544-563 -func (r *Runner) invokeAIWithRetry() (*decision.FullDecision, error) { - // 最多重试 3 次 - // 指数退避: 500ms, 1000ms, 1500ms - // 使用 decision.GetFullDecisionWithStrategy() 统一提示词生成 -} -``` - -### 4.3 AI 缓存 - -```go -// backtest/aicache.go:127-168 -// 缓存键: SHA256(context payload) -// 包含: variant, timestamp, account, positions, market data -``` - -### 4.4 支持的 AI 模型 - -| 模型 | 客户端文件 | -|------|-----------| -| DeepSeek | `mcp/deepseek_client.go` | -| Qwen | `mcp/qwen_client.go` | -| Claude | `mcp/claude_client.go` | -| Gemini | `mcp/gemini_client.go` | -| Grok | `mcp/grok_client.go` | -| OpenAI | `mcp/openai_client.go` | -| Kimi | `mcp/kimi_client.go` | - ---- - -## 5. 性能指标 (Performance Metrics) - -**核心文件:** `backtest/metrics.go` - -### 5.1 指标计算 - -| 指标 | 公式 | 代码位置 | -|------|------|----------| -| **总收益率** | (最终权益 - 初始资金) / 初始资金 × 100 | metrics.go:36-42 | -| **最大回撤** | max((峰值 - 当前) / 峰值 × 100) | metrics.go:64-91 | -| **夏普比率** | 平均收益 / 收益标准差 | metrics.go:94-138 | -| **胜率** | 盈利交易数 / 总交易数 × 100 | metrics.go:180-181 | -| **盈亏比** | 总盈利 / 总亏损 | metrics.go:189-193 | - -### 5.2 交易统计 - -```go -// backtest/metrics.go:141-225 -type TradeMetrics struct { - TotalTrades int - WinningTrades int - LosingTrades int - AvgWin float64 - AvgLoss float64 - BestSymbol string - WorstSymbol string - SymbolStats map[string]*SymbolStat -} -``` - ---- - -## 6. 权益曲线 (Equity Curve) - -**核心文件:** `backtest/equity.go` - -### 6.1 权益点结构 - -```json -{ - "ts": 1702566000000, - "equity": 10500.50, - "available": 8000.00, - "pnl": 500.50, - "pnl_pct": 5.005, - "dd_pct": 2.34, - "cycle": 42 -} -``` - -### 6.2 权益更新 - -```go -// backtest/runner.go:829-872 -func (r *Runner) updateState() { - // 1. 计算总权益: cash + margin + 未实现盈亏 - // 2. 追踪峰值 (MaxEquity) - // 3. 追踪谷值 (MinEquity) - // 4. 重新计算回撤: (MaxEquity - Equity) / MaxEquity × 100 -} -``` - -### 6.3 数据重采样 - -```go -// backtest/equity.go:10-50 -func ResampleEquity(points []EquityPoint, timeframe string) []EquityPoint { - // 按时间周期分桶 - // 保留每个桶的最后一个点 -} -``` - ---- - -## 7. 结果存储 (Result Storage) - -**核心文件:** `backtest/storage.go`, `store/backtest.go` - -### 7.1 文件存储结构 - -``` -backtests/ -├── / -│ ├── run.json # 运行元数据 -│ ├── checkpoint.json # 检查点 (用于恢复) -│ ├── equity.jsonl # 权益曲线 (逐行 JSON) -│ ├── trades.jsonl # 交易记录 (逐行 JSON) -│ ├── metrics.json # 性能指标 -│ ├── progress.json # 进度信息 -│ ├── ai_cache.json # AI 决策缓存 -│ └── decision_logs/ # 决策日志 -│ ├── 0.json -│ ├── 1.json -│ └── ... -``` - -### 7.2 数据库表结构 - -```sql --- 回测运行元数据 -CREATE TABLE backtest_runs ( - run_id TEXT PRIMARY KEY, - user_id TEXT, - config_json TEXT, - state TEXT, -- pending, running, completed, failed - processed_bars INTEGER, - progress_pct REAL, - equity_last REAL, - max_drawdown_pct REAL, - liquidated BOOLEAN, - ai_provider TEXT, - ai_model TEXT, - created_at DATETIME, - updated_at DATETIME -); - --- 权益曲线 -CREATE TABLE backtest_equity ( - id INTEGER PRIMARY KEY, - run_id TEXT, - ts INTEGER, - equity REAL, - available REAL, - pnl REAL, - pnl_pct REAL, - dd_pct REAL, - cycle INTEGER -); - --- 交易记录 -CREATE TABLE backtest_trades ( - id INTEGER PRIMARY KEY, - run_id TEXT, - ts INTEGER, - symbol TEXT, - action TEXT, - side TEXT, - qty REAL, - price REAL, - fee REAL, - slippage REAL, - realized_pnl REAL, - leverage INTEGER, - liquidation BOOLEAN -); - --- 性能指标 -CREATE TABLE backtest_metrics ( - run_id TEXT PRIMARY KEY, - payload BLOB, - updated_at DATETIME -); - --- 检查点 (暂停/恢复) -CREATE TABLE backtest_checkpoints ( - run_id TEXT PRIMARY KEY, - payload BLOB, - updated_at DATETIME -); -``` - ---- - -## 8. API 接口 - -**核心文件:** `api/backtest.go` - -### 8.1 接口列表 - -| 接口 | 方法 | 说明 | -|------|------|------| -| `/backtest/start` | POST | 开始回测 | -| `/backtest/pause` | POST | 暂停回测 | -| `/backtest/resume` | POST | 恢复回测 | -| `/backtest/stop` | POST | 停止回测 | -| `/backtest/status` | GET | 获取状态 | -| `/backtest/runs` | GET | 列出所有回测 | -| `/backtest/equity` | GET | 获取权益曲线 | -| `/backtest/trades` | GET | 获取交易记录 | -| `/backtest/metrics` | GET | 获取性能指标 | -| `/backtest/trace` | GET | 获取决策日志 | -| `/backtest/export` | GET | 导出 ZIP | -| `/backtest/delete` | POST | 删除回测 | - -### 8.2 请求示例 - -```bash -# 开始回测 -POST /backtest/start -{ - "config": { - "run_id": "bt_20231215", - "symbols": ["BTCUSDT", "ETHUSDT"], - "timeframes": ["3m", "15m", "4h"], - "start_ts": 1702566000, - "end_ts": 1702652400, - "initial_balance": 10000, - "ai_model_id": "model_001" - } -} - -# 获取权益曲线 -GET /backtest/equity?run_id=bt_20231215&tf=1h&limit=1000 - -# 获取指标 -GET /backtest/metrics?run_id=bt_20231215 -``` - -### 8.3 响应示例 - -```json -// 状态响应 -{ - "run_id": "bt_20231215", - "state": "running", - "progress_pct": 45.5, - "processed_bars": 1234, - "equity": 10234.50, - "unrealized_pnl": 234.50 -} - -// 指标响应 -{ - "total_return_pct": 12.34, - "max_drawdown_pct": 5.67, - "sharpe_ratio": 1.89, - "profit_factor": 2.34, - "win_rate": 65.5, - "trades": 123 -} -``` - ---- - -## 9. 账户与持仓管理 - -**核心文件:** `backtest/account.go` - -### 9.1 持仓结构 - -```go -type position struct { - Symbol string - Side string // "long" 或 "short" - Quantity float64 - EntryPrice float64 - Leverage int - Margin float64 // 保证金 - Notional float64 // 名义价值 - LiquidationPrice float64 // 爆仓价格 - OpenTime int64 -} -``` - -### 9.2 开仓逻辑 - -```go -// backtest/account.go:61-104 -func (a *BacktestAccount) Open(symbol, side string, qty, price float64, leverage int) { - // 1. 应用滑点 - // 2. 计算名义价值 (qty × price) - // 3. 计算保证金 (notional / leverage) - // 4. 扣除保证金 + 手续费 - // 5. 创建/加仓 - // 6. 计算爆仓价格 -} -``` - -### 9.3 平仓逻辑 - -```go -// backtest/account.go:106-140 -func (a *BacktestAccount) Close(symbol, side string, qty, price float64) { - // 1. 验证持仓存在 - // 2. 应用滑点 (反向) - // 3. 计算已实现盈亏 - // long: (exit - entry) × qty - // short: (entry - exit) × qty - // 4. 返还保证金 + 盈亏 - 手续费 - // 5. 更新/删除持仓 -} -``` - -### 9.4 爆仓价格计算 - -```go -// backtest/account.go:177-186 -func computeLiquidation(entry float64, leverage int, side string) float64 { - if side == "long" { - return entry * (1 - 1.0/float64(leverage)) // 做多: 下跌爆仓 - } - return entry * (1 + 1.0/float64(leverage)) // 做空: 上涨爆仓 -} -``` - ---- - -## 10. 检查点与恢复 - -**核心文件:** `backtest/runner.go` - -### 10.1 检查点结构 - -```json -{ - "bar_index": 1234, - "bar_ts": 1702609200000, - "cash": 8000.00, - "equity": 10234.50, - "max_equity": 10500.00, - "max_drawdown_pct": 5.67, - "positions": [...], - "decision_cycle": 62, - "liquidated": false -} -``` - -### 10.2 检查点触发 - -```go -// backtest/runner.go:874-898 -func (r *Runner) maybeCheckpoint() { - // 每 N 根 K 线保存 - // 或每 N 秒保存 -} -``` - -### 10.3 恢复流程 - -```go -func (r *Runner) RestoreFromCheckpoint() { - // 1. 加载检查点 - // 2. 恢复账户状态 - // 3. 恢复 K 线索引 (从下一根继续) - // 4. 恢复权益曲线、交易记录 -} -``` - ---- - -## 核心文件索引 - -| 模块 | 文件 | 关键方法 | -|------|------|----------| -| **配置** | `backtest/config.go` | `BacktestConfig`, `Validate()` | -| **数据加载** | `backtest/datafeed.go` | `NewDataFeed()`, `loadAll()`, `BuildMarketData()` | -| **模拟引擎** | `backtest/runner.go` | `Start()`, `loop()`, `stepOnce()` | -| **决策** | `backtest/runner.go` | `buildDecisionContext()`, `invokeAIWithRetry()` | -| **执行** | `backtest/runner.go` | `executeDecision()` | -| **账户** | `backtest/account.go` | `Open()`, `Close()`, `TotalEquity()` | -| **指标** | `backtest/metrics.go` | `CalculateMetrics()` | -| **权益** | `backtest/equity.go` | `ResampleEquity()`, `LimitEquityPoints()` | -| **存储** | `backtest/storage.go` | `SaveCheckpoint()`, `appendEquityPoint()` | -| **数据库** | `store/backtest.go` | 表结构和 CRUD 操作 | -| **API** | `api/backtest.go` | HTTP 处理器 | -| **AI 缓存** | `backtest/aicache.go` | `Get()`, `Put()`, `save()` | - ---- - -**文档版本:** 1.0.0 -**最后更新:** 2025-01-15 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index fd20dde3..3f18083c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -24,12 +24,12 @@ NOFX is a full-stack AI trading platform for cryptocurrency and US stock markets │ NOFX Platform │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐│ -│ │ Strategy │ │ Backtest │ │ Live Trading ││ -│ │ Studio │ │ Engine │ │ (Auto Trader) ││ -│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘│ -│ │ │ │ │ -│ └────────────────┴────────────────────┘ │ +│ ┌─────────────┐ ┌─────────────────────────────────────┐│ +│ │ Strategy │ │ Live Trading ││ +│ │ Studio │ │ (Auto Trader) ││ +│ └──────┬──────┘ └──────────────────┬──────────────────┘│ +│ │ │ │ +│ └────────────────────────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ │ Core Services │ │ @@ -57,7 +57,6 @@ NOFX is a full-stack AI trading platform for cryptocurrency and US stock markets | Module | Description | Documentation | |--------|-------------|---------------| | **Strategy Studio** | Strategy configuration, coin selection, data assembly, AI prompts | [STRATEGY_MODULE.md](STRATEGY_MODULE.md) | -| **Backtest Engine** | Historical simulation, performance metrics, AI decision replay | [BACKTEST_MODULE.md](BACKTEST_MODULE.md) | ### Module Overview @@ -71,16 +70,6 @@ Complete strategy configuration system including: **[Read Full Documentation →](STRATEGY_MODULE.md)** -#### Backtest Module -Historical trading simulation engine: -- Multi-symbol, multi-timeframe backtesting -- AI decision replay with caching -- Performance metrics (Sharpe, drawdown, win rate) -- Real-time progress streaming via SSE -- Checkpoint and resume support - -**[Read Full Documentation →](BACKTEST_MODULE.md)** - --- ## Project Structure @@ -91,7 +80,6 @@ nofx/ ├── api/ # HTTP API (Gin framework) ├── trader/ # Trading execution layer ├── strategy/ # Strategy engine -├── backtest/ # Backtest simulation engine ├── market/ # Market data service ├── mcp/ # AI model clients ├── store/ # Database operations @@ -131,7 +119,6 @@ nofx/ ## Quick Links - [Strategy Module](STRATEGY_MODULE.md) - How strategies work -- [Backtest Module](BACKTEST_MODULE.md) - How backtesting works - [Getting Started](../getting-started/README.md) - Setup guide - [FAQ](../faq/README.md) - Frequently asked questions diff --git a/docs/architecture/README.zh-CN.md b/docs/architecture/README.zh-CN.md index f3a16b59..ae4d9228 100644 --- a/docs/architecture/README.zh-CN.md +++ b/docs/architecture/README.zh-CN.md @@ -24,12 +24,12 @@ NOFX 是一个支持加密货币和美股市场的全栈 AI 交易平台: │ NOFX 平台 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐│ -│ │ 策略 │ │ 回测 │ │ 实盘交易 ││ -│ │ 工作室 │ │ 引擎 │ │ (自动交易员) ││ -│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘│ -│ │ │ │ │ -│ └────────────────┴────────────────────┘ │ +│ ┌─────────────┐ ┌─────────────────────────────────────┐│ +│ │ 策略 │ │ 实盘交易 ││ +│ │ 工作室 │ │ (自动交易员) ││ +│ └──────┬──────┘ └──────────────────┬──────────────────┘│ +│ │ │ │ +│ └────────────────────────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ │ 核心服务 │ │ @@ -57,7 +57,6 @@ NOFX 是一个支持加密货币和美股市场的全栈 AI 交易平台: | 模块 | 描述 | 文档 | |------|------|------| | **策略工作室** | 策略配置、币种选择、数据组装、AI 提示词 | [STRATEGY_MODULE.md](STRATEGY_MODULE.md) | -| **回测引擎** | 历史模拟、性能指标、AI 决策回放 | [BACKTEST_MODULE.md](BACKTEST_MODULE.md) | ### 模块概览 @@ -71,16 +70,6 @@ NOFX 是一个支持加密货币和美股市场的全栈 AI 交易平台: **[阅读完整文档 →](STRATEGY_MODULE.md)** -#### 回测模块 -历史交易模拟引擎: -- 多币种、多时间周期回测 -- AI 决策回放与缓存 -- 性能指标(夏普比率、最大回撤、胜率) -- SSE 实时进度推送 -- 断点续测支持 - -**[阅读完整文档 →](BACKTEST_MODULE.md)** - --- ## 项目结构 @@ -91,7 +80,6 @@ nofx/ ├── api/ # HTTP API (Gin 框架) ├── trader/ # 交易执行层 ├── strategy/ # 策略引擎 -├── backtest/ # 回测模拟引擎 ├── market/ # 行情数据服务 ├── mcp/ # AI 模型客户端 ├── store/ # 数据库操作 @@ -131,7 +119,6 @@ nofx/ ## 快速链接 - [策略模块](STRATEGY_MODULE.md) - 策略如何运作 -- [回测模块](BACKTEST_MODULE.md) - 回测如何运作 - [快速开始](../getting-started/README.zh-CN.md) - 部署指南 - [常见问题](../faq/README.md) - FAQ diff --git a/docs/i18n/vi/README.md b/docs/i18n/vi/README.md index 0eafcd48..e89805ef 100644 --- a/docs/i18n/vi/README.md +++ b/docs/i18n/vi/README.md @@ -79,7 +79,6 @@ Tương thích với **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** | **Strategy Studio** | Trình xây dựng trực quan — nguồn coin, chỉ báo, kiểm soát rủi ro | | **AI Competition** | AI cạnh tranh thời gian thực, bảng xếp hạng hiệu suất | | **Telegram Agent** | Chat với trợ lý giao dịch — streaming, gọi công cụ, bộ nhớ | -| **Backtest Lab** | Mô phỏng lịch sử, đường vốn và chỉ số hiệu suất | | **Dashboard** | Vị thế trực tiếp, P/L, nhật ký quyết định AI với Chain of Thought | ### Thị trường diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index d25edb6e..28bc8565 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -191,7 +191,6 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas |:--|:--| | [架构概览](../../architecture/README.md) | 系统设计和模块索引 | | [策略模块](../../architecture/STRATEGY_MODULE.md) | 币种选择、AI 提示词、执行 | -| [回测模块](../../architecture/BACKTEST_MODULE.md) | 历史模拟、指标计算 | | [常见问题](../../faq/README.md) | FAQ | | [快速开始](../../getting-started/README.md) | 部署指南 | diff --git a/docs/plans/2026-03-06-telegram-agent-redesign.md b/docs/plans/2026-03-06-telegram-agent-redesign.md index 9764d985..cde09a7c 100644 --- a/docs/plans/2026-03-06-telegram-agent-redesign.md +++ b/docs/plans/2026-03-06-telegram-agent-redesign.md @@ -243,7 +243,6 @@ s.route(protected, "GET", "/statistics", "Trading statistics (?trader_id Note: keep the existing special-case handlers that don't use `s.route` unchanged: - `api.Any("/health", ...)` — health check, no need to document - `api.GET("/crypto/...")` — crypto/encryption routes, bot doesn't need these -- `backtest.*` routes (registered separately) — add descriptions to the backtest group similarly **Step 3: Build** diff --git a/docs/plans/2026-03-06-telegram-bot.md b/docs/plans/2026-03-06-telegram-bot.md index 1627dce5..1365259c 100644 --- a/docs/plans/2026-03-06-telegram-bot.md +++ b/docs/plans/2026-03-06-telegram-bot.md @@ -993,7 +993,7 @@ func Start(cfg *config.Config, st *store.Store, tm *manager.TraderManager) { logger.Infof("🤖 Telegram bot started: @%s", bot.Self.UserName) - // Build the LLM client for intent parsing (use DeepSeek by default, same as backtest) + // Build the LLM client for intent parsing (use DeepSeek by default) llmClient := mcp.New() // Configure with whatever key is available in env (intent parsing is lightweight) // The service layer will use store to get user-configured models for actual trading diff --git a/docs/research/AI-Trader-Analysis-Report.md b/docs/research/AI-Trader-Analysis-Report.md index f0ad4838..b73c8329 100644 --- a/docs/research/AI-Trader-Analysis-Report.md +++ b/docs/research/AI-Trader-Analysis-Report.md @@ -2488,7 +2488,6 @@ KNOWN_ISSUES = [ 3. **金融量化** - Lo, Andrew W. "The Adaptive Markets Hypothesis." Journal of Portfolio Management, 2004. - - Bailey et al. "The Probability of Backtest Overfitting." Journal of Computational Finance, 2014. ### 13.3 代码示例索引 diff --git a/main.go b/main.go index 4dcf8bb6..7fa7295c 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,11 @@ package main import ( "nofx/api" "nofx/auth" - "nofx/backtest" "nofx/config" "nofx/crypto" "nofx/telemetry" "nofx/logger" "nofx/manager" - "nofx/mcp" _ "nofx/mcp/payment" _ "nofx/mcp/provider" "nofx/store" @@ -81,7 +79,6 @@ func main() { logger.Fatalf("❌ Failed to initialize database: %v", err) } defer st.Close() - backtest.UseDatabaseWithType(st.DB(), st.DBType() == store.DBTypePostgres) // Initialize installation ID for experience improvement (anonymous statistics) initInstallationID(st) @@ -98,13 +95,8 @@ func main() { // time.Sleep(500 * time.Millisecond) logger.Info("📊 Using CoinAnk API for all market data (WebSocket cache disabled)") - // Create TraderManager and BacktestManager + // Create TraderManager traderManager := manager.NewTraderManager() - mcpClient := newSharedMCPClient() - backtestManager := backtest.NewManager(mcpClient) - if err := backtestManager.RestoreRuns(); err != nil { - logger.Warnf("⚠️ Failed to restore backtest history: %v", err) - } // Load all traders from database to memory (may auto-start traders with IsRunning=true) if err := traderManager.LoadTradersFromStore(st); err != nil { @@ -132,7 +124,7 @@ func main() { } // Start API server - server := api.NewServer(traderManager, st, cryptoService, backtestManager, cfg.APIServerPort) + server := api.NewServer(traderManager, st, cryptoService, cfg.APIServerPort) // Create hot-reload channel for Telegram bot; wire it to the API server // so that POST /api/telegram can trigger a bot restart when the token changes. @@ -163,16 +155,6 @@ func main() { logger.Info("✅ System shut down safely") } -// newSharedMCPClient creates a shared MCP AI client (for backtesting) -func newSharedMCPClient() mcp.AIClient { - apiKey := os.Getenv("DEEPSEEK_API_KEY") - if apiKey == "" { - logger.Warn("⚠️ DEEPSEEK_API_KEY not set, AI features will be unavailable") - return nil - } - return mcp.NewAIClientByProvider("deepseek") -} - // initInstallationID initializes the anonymous installation ID for experience improvement // This ID is persisted in database and used for anonymous usage statistics func initInstallationID(st *store.Store) { diff --git a/market/data.go b/market/data.go index 99dbfc9c..41548348 100644 --- a/market/data.go +++ b/market/data.go @@ -602,7 +602,7 @@ func parseFloat(v interface{}) (float64, error) { } } -// BuildDataFromKlines constructs market data snapshot from preloaded K-line series (for backtesting/simulation). +// BuildDataFromKlines constructs market data snapshot from preloaded K-line series. func BuildDataFromKlines(symbol string, primary []Kline, longer []Kline) (*Data, error) { if len(primary) == 0 { return nil, fmt.Errorf("primary series is empty") diff --git a/screenshots/backtest-lab.png b/screenshots/backtest-lab.png deleted file mode 100644 index 41f433d5472efc99d13723c930655115476ece3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 287909 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

4A(MgX?9 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

yTCOk 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%0D?H-0YvolSvY3GgM0c_7mqoz8k|THk z#Nvr@xDCz$y!pyMbX&lDt}yTSX!MPq@&4g}Fa$R+;QHo@bF-D4?*V7WrQ_o9EiQe8 z<7(U6xhz7obYgt{n8yZYRuIoTB4u$p|Dvm?OO$*{0pH&e)bpglKXi{M`>eV9XsXm^+goj$*b()1&FV#$gcnQYbSkk(hPo;3n9Np!F)UKoJiSuyDzO z<%wXU-KR#3HcN&qMg{KAZWZ+39FIE47Q_>osb68+ueFiAzV}K2V!ZwZUh%{kC?jy9 z#+6~%=B6Fd$~9^<4b!AzwK%(2+(WRT+w`gowcQC$0rxx>wTv7D)Y=l2YYwxzG4vTg z`d0=Bs8XA=walGfxbEY|5TkTP` zPuoA+IS%9%iV${Ws9JBbpDm2p6+XXAmh1mI9oP-Xhx>nAG2nl;up@$jOLNEFE$n=1 zSIg3u23A`s#M<2pKF9Hx1&mh?(RbwR zm)Ke)o3e@dM%VbkY<2+xc$j)XHS_S-VYZM6&8*e%=qh1&% zo1*yTv4v4DZ#fDF`J56OyY8SPZz6v`mM+nr>7FOxNH~y%vyNeex+PRj10^Qud7ETk z($)vpH4(dkeOLqU=OFZ0Rv6J`)ocNnVz6>hohS$PH~d5<_kC)MpfaQ