mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2025-12-06 05:44:04 +08:00
* refactor: 简化交易动作,移除 update_stop_loss/update_take_profit/partial_close - 移除 Decision 结构体中的 NewStopLoss, NewTakeProfit, ClosePercentage 字段 - 删除 executeUpdateStopLossWithRecord, executeUpdateTakeProfitWithRecord, executePartialCloseWithRecord 函数 - 简化 logger 中的 partial_close 聚合逻辑 - 更新 AI prompt 和验证逻辑,只保留 6 个核心动作 - 清理相关测试代码 保留的交易动作: open_long, open_short, close_long, close_short, hold, wait * refactor: 移除 AI学习与反思 模块 - 删除前端 AILearning.tsx 组件和相关引用 - 删除后端 /performance API 接口 - 删除 logger 中 AnalyzePerformance、calculateSharpeRatio 等函数 - 删除 PerformanceAnalysis、TradeOutcome、SymbolPerformance 等结构体 - 删除 Context 中的 Performance 字段 - 移除 AI prompt 中夏普比率自我进化相关内容 - 清理 i18n 翻译文件中的相关条目 该模块基于磁盘存储计算,经常出错,做减法移除 * refactor: 将数据库操作统一迁移到 store 包 - 新增 store/ 包,统一管理所有数据库操作 - store.go: 主 Store 结构,懒加载各子模块 - user.go, ai_model.go, exchange.go, trader.go 等子模块 - 支持加密/解密函数注入 (SetCryptoFuncs) - 更新 main.go 使用 store.New() 替代 config.NewDatabase() - 更新 api/server.go 使用 *store.Store 替代 *config.Database - 更新 manager/trader_manager.go: - 新增 LoadTradersFromStore, LoadUserTradersFromStore 方法 - 删除旧版 LoadUserTraders, LoadTraderByID, loadSingleTrader 等方法 - 移除 nofx/config 依赖 - 删除 config/database.go 和 config/database_test.go - 更新 api/server_test.go 使用 store.Trader 类型 - 清理 logger/ 包中未使用的 telegram 相关代码 * refactor: unify encryption key management via .env - Remove redundant EncryptionManager and SecureStorage - Simplify CryptoService to load keys from environment variables only - RSA_PRIVATE_KEY: RSA private key for client-server encryption - DATA_ENCRYPTION_KEY: AES-256 key for database encryption - JWT_SECRET: JWT signing key for authentication - Update start.sh to auto-generate missing keys on first run - Remove secrets/ directory and file-based key storage - Delete obsolete encryption setup scripts - Update .env.example with all required keys * refactor: unify logger usage across mcp package - Add MCPLogger adapter in logger package to implement mcp.Logger interface - Update mcp/config.go to use global logger by default - Remove redundant defaultLogger from mcp/logger.go - Keep noopLogger for testing purposes * chore: remove leftover test RSA key file * chore: remove unused bootstrap package * refactor: unify logging to use logger package instead of fmt/log - Replace all fmt.Print/log.Print calls with logger package - Add auto-initialization in logger package init() for test compatibility - Update main.go to initialize logger at startup - Migrate all packages: api, backtest, config, decision, manager, market, store, trader * refactor: rename database file from config.db to data.db - Update main.go, start.sh, docker-compose.yml - Update migration script and documentation - Update .gitignore and translations * fix: add RSA_PRIVATE_KEY to docker-compose environment * fix: add registration_enabled to /api/config response * fix: Fix navigation between login and register pages Use window.location.href instead of react-router's navigate() to fix the issue where URL changes but the page doesn't reload due to App.tsx using custom route state management. * fix: Switch SQLite from WAL to DELETE mode for Docker compatibility WAL mode causes data sync issues with Docker bind mounts on macOS due to incompatible file locking mechanisms between the container and host. DELETE mode (traditional journaling) ensures data is written directly to the main database file. * refactor: Remove default user from database initialization The default user was a legacy placeholder that is no longer needed now that proper user registration is in place. * feat: Add order tracking system with centralized status sync - Add trader_orders table for tracking all order lifecycle - Implement GetOrderStatus interface for all exchanges (Binance, Bybit, Hyperliquid, Aster, Lighter) - Create OrderSyncManager for centralized order status polling - Add trading statistics (Sharpe ratio, win rate, profit factor) to AI context - Include recent completed orders in AI decision input - Remove per-order goroutine polling in favor of global sync manager * feat: Add TradingView K-line chart to dashboard - Create TradingViewChart component with exchange/symbol selectors - Support Binance, Bybit, OKX, Coinbase, Kraken, KuCoin exchanges - Add popular symbols quick selection - Support multiple timeframes (1m to 1W) - Add fullscreen mode - Integrate with Dashboard page below equity chart - Add i18n translations for zh/en * refactor: Replace separate charts with tabbed ChartTabs component - Create ChartTabs component with tab switching between equity curve and K-line - Add embedded mode support for EquityChart and TradingViewChart - User can now switch between account equity and market chart in same area * fix: Use ChartTabs in App.tsx and fix embedded mode in EquityChart - Replace EquityChart with ChartTabs in App.tsx (the actual dashboard renderer) - Fix EquityChart embedded mode for error and empty data states - Rename interval state to timeInterval to avoid shadowing window.setInterval - Add debug logging to ChartTabs component * feat: Add position tracking system for accurate trade history - Add trader_positions table to track complete open/close trades - Add PositionSyncManager to detect manual closes via polling - Record position on open, update on close with PnL calculation - Use positions table for trading stats and recent trades (replacing orders table) - Fix TradingView chart symbol format (add .P suffix for futures) - Fix DecisionCard wait/hold action color (gray instead of red) - Auto-append USDT suffix for custom symbol input * update ---------
432 lines
14 KiB
Go
432 lines
14 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"nofx/api"
|
||
"nofx/auth"
|
||
"nofx/backtest"
|
||
"nofx/config"
|
||
"nofx/crypto"
|
||
"nofx/logger"
|
||
"nofx/manager"
|
||
"nofx/market"
|
||
"nofx/mcp"
|
||
"nofx/pool"
|
||
"nofx/store"
|
||
"nofx/trader"
|
||
"os"
|
||
"os/signal"
|
||
"strconv"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/joho/godotenv"
|
||
)
|
||
|
||
// ConfigFile 配置文件结构,只包含需要同步到数据库的字段
|
||
// TODO 现在与config.Config相同,未来会被替换, 现在为了兼容性不得不保留当前文件
|
||
type ConfigFile struct {
|
||
BetaMode bool `json:"beta_mode"`
|
||
APIServerPort int `json:"api_server_port"`
|
||
UseDefaultCoins bool `json:"use_default_coins"`
|
||
DefaultCoins []string `json:"default_coins"`
|
||
CoinPoolAPIURL string `json:"coin_pool_api_url"`
|
||
OITopAPIURL string `json:"oi_top_api_url"`
|
||
MaxDailyLoss float64 `json:"max_daily_loss"`
|
||
MaxDrawdown float64 `json:"max_drawdown"`
|
||
StopTradingMinutes int `json:"stop_trading_minutes"`
|
||
Leverage config.LeverageConfig `json:"leverage"`
|
||
JWTSecret string `json:"jwt_secret"`
|
||
DataKLineTime string `json:"data_k_line_time"`
|
||
Log *config.LogConfig `json:"log"` // 日志配置
|
||
}
|
||
|
||
// loadConfigFile 读取并解析config.json文件
|
||
func loadConfigFile() (*ConfigFile, error) {
|
||
// 检查config.json是否存在
|
||
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
|
||
logger.Info("📄 config.json不存在,使用默认配置")
|
||
return &ConfigFile{}, nil
|
||
}
|
||
|
||
// 读取config.json
|
||
data, err := os.ReadFile("config.json")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取config.json失败: %w", err)
|
||
}
|
||
|
||
// 解析JSON
|
||
var configFile ConfigFile
|
||
if err := json.Unmarshal(data, &configFile); err != nil {
|
||
return nil, fmt.Errorf("解析config.json失败: %w", err)
|
||
}
|
||
|
||
return &configFile, nil
|
||
}
|
||
|
||
// syncConfigToDatabase 将配置同步到数据库
|
||
func syncConfigToDatabase(st *store.Store, configFile *ConfigFile) error {
|
||
if configFile == nil {
|
||
return nil
|
||
}
|
||
|
||
logger.Info("🔄 开始同步config.json到数据库...")
|
||
|
||
// 同步各配置项到数据库
|
||
configs := map[string]string{
|
||
"beta_mode": fmt.Sprintf("%t", configFile.BetaMode),
|
||
"api_server_port": strconv.Itoa(configFile.APIServerPort),
|
||
"use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins),
|
||
"coin_pool_api_url": configFile.CoinPoolAPIURL,
|
||
"oi_top_api_url": configFile.OITopAPIURL,
|
||
"max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss),
|
||
"max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown),
|
||
"stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes),
|
||
}
|
||
|
||
// 同步default_coins(转换为JSON字符串存储)
|
||
if len(configFile.DefaultCoins) > 0 {
|
||
defaultCoinsJSON, err := json.Marshal(configFile.DefaultCoins)
|
||
if err == nil {
|
||
configs["default_coins"] = string(defaultCoinsJSON)
|
||
}
|
||
}
|
||
|
||
// 同步杠杆配置
|
||
if configFile.Leverage.BTCETHLeverage > 0 {
|
||
configs["btc_eth_leverage"] = strconv.Itoa(configFile.Leverage.BTCETHLeverage)
|
||
}
|
||
if configFile.Leverage.AltcoinLeverage > 0 {
|
||
configs["altcoin_leverage"] = strconv.Itoa(configFile.Leverage.AltcoinLeverage)
|
||
}
|
||
|
||
// 如果JWT密钥不为空,也同步
|
||
if configFile.JWTSecret != "" {
|
||
configs["jwt_secret"] = configFile.JWTSecret
|
||
}
|
||
|
||
// 更新数据库配置
|
||
for key, value := range configs {
|
||
if err := st.SystemConfig().Set(key, value); err != nil {
|
||
logger.Warnf("⚠️ 更新配置 %s 失败: %v", key, err)
|
||
} else {
|
||
logger.Infof("✓ 同步配置: %s = %s", key, value)
|
||
}
|
||
}
|
||
|
||
logger.Info("✅ config.json同步完成")
|
||
return nil
|
||
}
|
||
|
||
// loadBetaCodesToDatabase 加载内测码文件到数据库
|
||
func loadBetaCodesToDatabase(st *store.Store) error {
|
||
betaCodeFile := "beta_codes.txt"
|
||
|
||
// 检查内测码文件是否存在
|
||
if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) {
|
||
logger.Infof("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile)
|
||
return nil
|
||
}
|
||
|
||
// 获取文件信息
|
||
fileInfo, err := os.Stat(betaCodeFile)
|
||
if err != nil {
|
||
return fmt.Errorf("获取内测码文件信息失败: %w", err)
|
||
}
|
||
|
||
logger.Infof("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024)
|
||
|
||
// 加载内测码到数据库
|
||
err = st.BetaCode().LoadFromFile(betaCodeFile)
|
||
if err != nil {
|
||
return fmt.Errorf("加载内测码失败: %w", err)
|
||
}
|
||
|
||
// 显示统计信息
|
||
total, used, err := st.BetaCode().GetStats()
|
||
if err != nil {
|
||
logger.Warnf("⚠️ 获取内测码统计失败: %v", err)
|
||
} else {
|
||
logger.Infof("✅ 内测码加载完成: 总计 %d 个,已使用 %d 个,剩余 %d 个", total, used, total-used)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func main() {
|
||
// Load environment variables from .env file if present (for local/dev runs)
|
||
// In Docker Compose, variables are injected by the runtime and this is harmless.
|
||
_ = godotenv.Load()
|
||
|
||
// 初始化日志
|
||
logger.Init(nil)
|
||
|
||
logger.Info("╔════════════════════════════════════════════════════════════╗")
|
||
logger.Info("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║")
|
||
logger.Info("╚════════════════════════════════════════════════════════════╝")
|
||
|
||
// 初始化数据库配置
|
||
dbPath := "data.db"
|
||
if len(os.Args) > 1 {
|
||
dbPath = os.Args[1]
|
||
}
|
||
|
||
// 读取配置文件
|
||
configFile, err := loadConfigFile()
|
||
if err != nil {
|
||
logger.Fatalf("❌ 读取config.json失败: %v", err)
|
||
}
|
||
|
||
logger.Infof("📋 初始化配置数据库: %s", dbPath)
|
||
st, err := store.New(dbPath)
|
||
if err != nil {
|
||
logger.Fatalf("❌ 初始化数据库失败: %v", err)
|
||
}
|
||
defer st.Close()
|
||
backtest.UseDatabase(st.DB())
|
||
|
||
// 初始化加密服务
|
||
logger.Info("🔐 初始化加密服务...")
|
||
cryptoService, err := crypto.NewCryptoService()
|
||
if err != nil {
|
||
logger.Fatalf("❌ 初始化加密服务失败: %v", err)
|
||
}
|
||
// 创建加密/解密包装函数
|
||
encryptFunc := func(plaintext string) string {
|
||
if plaintext == "" {
|
||
return plaintext
|
||
}
|
||
encrypted, err := cryptoService.EncryptForStorage(plaintext)
|
||
if err != nil {
|
||
logger.Warnf("⚠️ 加密失败: %v", err)
|
||
return plaintext
|
||
}
|
||
return encrypted
|
||
}
|
||
decryptFunc := func(encrypted string) string {
|
||
if encrypted == "" {
|
||
return encrypted
|
||
}
|
||
if !cryptoService.IsEncryptedStorageValue(encrypted) {
|
||
return encrypted
|
||
}
|
||
decrypted, err := cryptoService.DecryptFromStorage(encrypted)
|
||
if err != nil {
|
||
logger.Warnf("⚠️ 解密失败: %v", err)
|
||
return encrypted
|
||
}
|
||
return decrypted
|
||
}
|
||
st.SetCryptoFuncs(encryptFunc, decryptFunc)
|
||
logger.Info("✅ 加密服务初始化成功")
|
||
|
||
// 同步config.json到数据库
|
||
if err := syncConfigToDatabase(st, configFile); err != nil {
|
||
logger.Warnf("⚠️ 同步config.json到数据库失败: %v", err)
|
||
}
|
||
|
||
// 加载内测码到数据库
|
||
if err := loadBetaCodesToDatabase(st); err != nil {
|
||
logger.Warnf("⚠️ 加载内测码到数据库失败: %v", err)
|
||
}
|
||
|
||
// 获取系统配置
|
||
useDefaultCoinsStr, _ := st.SystemConfig().Get("use_default_coins")
|
||
useDefaultCoins := useDefaultCoinsStr == "true"
|
||
apiPortStr, _ := st.SystemConfig().Get("api_server_port")
|
||
|
||
// 设置JWT密钥(优先使用环境变量)
|
||
jwtSecret := strings.TrimSpace(os.Getenv("JWT_SECRET"))
|
||
if jwtSecret == "" {
|
||
// 回退到数据库配置
|
||
jwtSecret, _ = st.SystemConfig().Get("jwt_secret")
|
||
if jwtSecret == "" {
|
||
jwtSecret = "your-jwt-secret-key-change-in-production-make-it-long-and-random"
|
||
logger.Warn("⚠️ 使用默认JWT密钥,建议使用加密设置脚本生成安全密钥")
|
||
} else {
|
||
logger.Info("🔑 使用数据库中JWT密钥")
|
||
}
|
||
} else {
|
||
logger.Info("🔑 使用环境变量JWT密钥")
|
||
}
|
||
auth.SetJWTSecret(jwtSecret)
|
||
|
||
// 管理员模式下需要管理员密码,缺失则退出
|
||
|
||
logger.Info("✓ 配置数据库初始化成功")
|
||
|
||
// 从数据库读取默认主流币种列表
|
||
defaultCoinsJSON, _ := st.SystemConfig().Get("default_coins")
|
||
var defaultCoins []string
|
||
|
||
if defaultCoinsJSON != "" {
|
||
// 尝试从JSON解析
|
||
if err := json.Unmarshal([]byte(defaultCoinsJSON), &defaultCoins); err != nil {
|
||
logger.Warnf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err)
|
||
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
||
} else {
|
||
logger.Infof("✓ 从数据库加载默认币种列表(共%d个): %v", len(defaultCoins), defaultCoins)
|
||
}
|
||
} else {
|
||
// 如果数据库中没有配置,使用硬编码默认值
|
||
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
||
logger.Warn("⚠️ 数据库中未配置default_coins,使用硬编码默认值")
|
||
}
|
||
|
||
pool.SetDefaultCoins(defaultCoins)
|
||
// 设置是否使用默认主流币种
|
||
pool.SetUseDefaultCoins(useDefaultCoins)
|
||
if useDefaultCoins {
|
||
logger.Info("✓ 已启用默认主流币种列表")
|
||
}
|
||
|
||
// 设置币种池API URL
|
||
coinPoolAPIURL, _ := st.SystemConfig().Get("coin_pool_api_url")
|
||
if coinPoolAPIURL != "" {
|
||
pool.SetCoinPoolAPI(coinPoolAPIURL)
|
||
logger.Info("✓ 已配置AI500币种池API")
|
||
}
|
||
|
||
oiTopAPIURL, _ := st.SystemConfig().Get("oi_top_api_url")
|
||
if oiTopAPIURL != "" {
|
||
pool.SetOITopAPI(oiTopAPIURL)
|
||
logger.Info("✓ 已配置OI Top API")
|
||
}
|
||
|
||
// 创建TraderManager 与 BacktestManager
|
||
cfgForAI, cfgErr := config.LoadConfig("config.json")
|
||
if cfgErr != nil {
|
||
logger.Warnf("⚠️ 加载config.json用于AI客户端失败: %v", cfgErr)
|
||
}
|
||
|
||
traderManager := manager.NewTraderManager()
|
||
mcpClient := newSharedMCPClient(cfgForAI)
|
||
backtestManager := backtest.NewManager(mcpClient)
|
||
if err := backtestManager.RestoreRuns(); err != nil {
|
||
logger.Warnf("⚠️ 恢复历史回测失败: %v", err)
|
||
}
|
||
|
||
// 从数据库加载所有交易员到内存
|
||
err = traderManager.LoadTradersFromStore(st)
|
||
if err != nil {
|
||
logger.Fatalf("❌ 加载交易员失败: %v", err)
|
||
}
|
||
|
||
// 获取数据库中的所有交易员配置(用于显示,使用default用户)
|
||
traders, err := st.Trader().List("default")
|
||
if err != nil {
|
||
logger.Fatalf("❌ 获取交易员列表失败: %v", err)
|
||
}
|
||
|
||
// 显示加载的交易员信息
|
||
logger.Info("🤖 数据库中的AI交易员配置:")
|
||
if len(traders) == 0 {
|
||
logger.Info(" • 暂无配置的交易员,请通过Web界面创建")
|
||
} else {
|
||
for _, trader := range traders {
|
||
status := "停止"
|
||
if trader.IsRunning {
|
||
status = "运行中"
|
||
}
|
||
logger.Infof(" • %s (%s + %s) - 初始资金: %.0f USDT [%s]",
|
||
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
|
||
trader.InitialBalance, status)
|
||
}
|
||
}
|
||
|
||
logger.Info("🤖 AI全权决策模式:")
|
||
logger.Info(" • AI将自主决定每笔交易的杠杆倍数(山寨币最高5倍,BTC/ETH最高5倍)")
|
||
logger.Info(" • AI将自主决定每笔交易的仓位大小")
|
||
logger.Info(" • AI将自主设置止损和止盈价格")
|
||
logger.Info(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
|
||
logger.Warn("⚠️ 风险提示: AI自动交易有风险,建议小额资金测试!")
|
||
logger.Info("按 Ctrl+C 停止运行")
|
||
logger.Info(strings.Repeat("=", 60))
|
||
|
||
// 获取API服务器端口(优先级:环境变量 > 数据库配置 > 默认值)
|
||
apiPort := 8080 // 默认端口
|
||
|
||
// 1. 优先从环境变量 NOFX_BACKEND_PORT 读取
|
||
if envPort := strings.TrimSpace(os.Getenv("NOFX_BACKEND_PORT")); envPort != "" {
|
||
if port, err := strconv.Atoi(envPort); err == nil && port > 0 {
|
||
apiPort = port
|
||
logger.Infof("🔌 使用环境变量端口: %d (NOFX_BACKEND_PORT)", apiPort)
|
||
} else {
|
||
logger.Warnf("⚠️ 环境变量 NOFX_BACKEND_PORT 无效: %s", envPort)
|
||
}
|
||
} else if apiPortStr != "" {
|
||
// 2. 从数据库配置读取(config.json 同步过来的)
|
||
if port, err := strconv.Atoi(apiPortStr); err == nil && port > 0 {
|
||
apiPort = port
|
||
logger.Infof("🔌 使用数据库配置端口: %d (api_server_port)", apiPort)
|
||
}
|
||
} else {
|
||
logger.Infof("🔌 使用默认端口: %d", apiPort)
|
||
}
|
||
|
||
// 启动订单同步管理器
|
||
orderSyncManager := trader.NewOrderSyncManager(st, 10*time.Second)
|
||
orderSyncManager.Start()
|
||
|
||
// 启动仓位同步管理器(检测手动平仓等变化)
|
||
positionSyncManager := trader.NewPositionSyncManager(st, 10*time.Second)
|
||
positionSyncManager.Start()
|
||
|
||
// 创建并启动API服务器
|
||
apiServer := api.NewServer(traderManager, st, cryptoService, backtestManager, apiPort)
|
||
go func() {
|
||
if err := apiServer.Start(); err != nil {
|
||
logger.Errorf("❌ API服务器错误: %v", err)
|
||
}
|
||
}()
|
||
|
||
// 启动流行情数据 - 默认使用所有交易员设置的币种 如果没有设置币种 则优先使用系统默认
|
||
go market.NewWSMonitor(150).Start(st.Trader().GetCustomCoins())
|
||
//go market.NewWSMonitor(150).Start([]string{}) //这里是一个使用方式 传入空的话 则使用market市场的所有币种
|
||
// 设置优雅退出
|
||
sigChan := make(chan os.Signal, 1)
|
||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||
|
||
// TODO: 启动数据库中配置为运行状态的交易员
|
||
// traderManager.StartAll()
|
||
|
||
// 等待退出信号
|
||
<-sigChan
|
||
logger.Info("📛 收到退出信号,正在优雅关闭...")
|
||
|
||
// 步骤 1: 停止所有交易员
|
||
logger.Info("⏸️ 停止所有交易员...")
|
||
traderManager.StopAll()
|
||
logger.Info("✅ 所有交易员已停止")
|
||
|
||
// 步骤 2: 停止订单同步管理器和仓位同步管理器
|
||
logger.Info("📦 停止订单同步管理器...")
|
||
orderSyncManager.Stop()
|
||
logger.Info("📊 停止仓位同步管理器...")
|
||
positionSyncManager.Stop()
|
||
|
||
// 步骤 3: 关闭 API 服务器
|
||
logger.Info("🛑 停止 API 服务器...")
|
||
if err := apiServer.Shutdown(); err != nil {
|
||
logger.Warnf("⚠️ 关闭 API 服务器时出错: %v", err)
|
||
} else {
|
||
logger.Info("✅ API 服务器已安全关闭")
|
||
}
|
||
|
||
// 步骤 4: 关闭数据库连接 (确保所有写入完成)
|
||
logger.Info("💾 关闭数据库连接...")
|
||
if err := st.Close(); err != nil {
|
||
logger.Errorf("❌ 关闭数据库失败: %v", err)
|
||
} else {
|
||
logger.Info("✅ 数据库已安全关闭,所有数据已持久化")
|
||
}
|
||
|
||
logger.Info("👋 感谢使用AI交易系统!")
|
||
}
|
||
|
||
func newSharedMCPClient(cfg *config.Config) mcp.AIClient {
|
||
return mcp.NewClient()
|
||
}
|