diff --git a/PM2_DEPLOYMENT.md b/PM2_DEPLOYMENT.md new file mode 100644 index 00000000..79af7e21 --- /dev/null +++ b/PM2_DEPLOYMENT.md @@ -0,0 +1,300 @@ +# NoFX Trading Bot - PM2 部署指南 + +使用 PM2 进行本地开发和生产部署的完整指南。 + +## 🚀 快速开始 + +### 1. 安装 PM2 + +```bash +npm install -g pm2 +``` + +### 2. 一键启动 + +```bash +./pm2.sh start +``` + +就这么简单!前后端将自动启动。 + +--- + +## 📋 所有命令 + +### 服务管理 + +```bash +# 启动服务 +./pm2.sh start + +# 停止服务 +./pm2.sh stop + +# 重启服务 +./pm2.sh restart + +# 查看状态 +./pm2.sh status + +# 删除服务 +./pm2.sh delete +``` + +### 日志查看 + +```bash +# 查看所有日志(实时) +./pm2.sh logs + +# 只看后端日志 +./pm2.sh logs backend + +# 只看前端日志 +./pm2.sh logs frontend +``` + +### 构建与编译 + +```bash +# 编译后端 +./pm2.sh build + +# 重新编译后端并重启 +./pm2.sh rebuild +``` + +### 监控 + +```bash +# 打开 PM2 监控面板(实时CPU/内存) +./pm2.sh monitor +``` + +--- + +## 📊 访问地址 + +启动成功后: + +- **前端 Web 界面**: http://localhost:3000 +- **后端 API**: http://localhost:8080 +- **健康检查**: http://localhost:8080/health + +--- + +## 🔧 配置文件 + +### pm2.config.js + +PM2 配置文件,定义了前后端的启动参数: + +```javascript +const path = require('path'); + +module.exports = { + apps: [ + { + name: 'nofx-backend', + script: './nofx', // Go 二进制文件 + cwd: __dirname, // 动态获取当前目录 + autorestart: true, + max_memory_restart: '500M' + }, + { + name: 'nofx-frontend', + script: 'npm', + args: 'run dev', // Vite 开发服务器 + cwd: path.join(__dirname, 'web'), // 动态拼接路径 + autorestart: true, + max_memory_restart: '300M' + } + ] +}; +``` + +**修改配置后需要重启:** +```bash +./pm2.sh restart +``` + +--- + +## 📝 日志文件位置 + +- **后端日志**: `./logs/backend-error.log` 和 `./logs/backend-out.log` +- **前端日志**: `./web/logs/frontend-error.log` 和 `./web/logs/frontend-out.log` + +--- + +## 🔄 开机自启动 + +设置 PM2 开机自启动: + +```bash +# 1. 启动服务 +./pm2.sh start + +# 2. 保存当前进程列表 +pm2 save + +# 3. 生成启动脚本 +pm2 startup + +# 4. 按照提示执行命令(需要 sudo) +``` + +**取消开机自启动:** +```bash +pm2 unstartup +``` + +--- + +## 🛠️ 常见操作 + +### 修改代码后重启 + +**后端修改:** +```bash +./pm2.sh rebuild # 自动编译并重启 +``` + +**前端修改:** +```bash +./pm2.sh restart # Vite 会自动热重载,无需重启 +``` + +### 查看实时资源占用 + +```bash +./pm2.sh monitor +``` + +### 查看详细信息 + +```bash +pm2 info nofx-backend # 后端详情 +pm2 info nofx-frontend # 前端详情 +``` + +### 清空日志 + +```bash +pm2 flush +``` + +--- + +## 🐛 故障排查 + +### 服务启动失败 + +```bash +# 1. 查看详细错误 +./pm2.sh logs + +# 2. 检查端口占用 +lsof -i :8080 # 后端端口 +lsof -i :3000 # 前端端口 + +# 3. 手动编译测试 +go build -o nofx +./nofx +``` + +### 后端无法启动 + +```bash +# 检查 config.json 是否存在 +ls -l config.json + +# 检查权限 +chmod +x nofx + +# 手动运行看报错 +./nofx +``` + +### 前端无法访问 + +```bash +# 检查 node_modules +cd web && npm install + +# 手动启动测试 +npm run dev +``` + +--- + +## 🎯 生产环境建议 + +### 1. 使用生产模式 + +修改 `pm2.config.js`: + +```javascript +{ + name: 'nofx-frontend', + script: 'npm', + args: 'run preview', // 改为 preview(需先 npm run build) + env: { + NODE_ENV: 'production' + } +} +``` + +### 2. 增加实例数(负载均衡) + +```javascript +{ + name: 'nofx-backend', + script: './nofx', + instances: 2, // 启动 2 个实例 + exec_mode: 'cluster' +} +``` + +### 3. 自动重启策略 + +```javascript +{ + autorestart: true, + max_restarts: 10, + min_uptime: '10s', + max_memory_restart: '500M' +} +``` + +--- + +## 📦 与 Docker 部署的对比 + +| 特性 | PM2 部署 | Docker 部署 | +|------|---------|------------| +| 启动速度 | ⚡ 快 | 🐌 较慢 | +| 资源占用 | 💚 低 | 🟡 中等 | +| 隔离性 | 🟡 中等 | 💚 高 | +| 适用场景 | 开发/单机 | 生产/集群 | +| 配置复杂度 | 💚 简单 | 🟡 中等 | + +**建议:** +- **开发环境**: 使用 `./pm2.sh` +- **生产环境**: 使用 `./start.sh` (Docker) + +--- + +## 🆘 获取帮助 + +```bash +./pm2.sh help +``` + +或查看 PM2 官方文档:https://pm2.keymetrics.io/ + +--- + +## 📄 License + +MIT diff --git a/api/server.go b/api/server.go index 875aeae0..ef871280 100644 --- a/api/server.go +++ b/api/server.go @@ -388,8 +388,9 @@ func (s *Server) handlePerformance(c *gin.Context) { return } - // 分析最近20个周期的交易表现 - performance, err := trader.GetDecisionLogger().AnalyzePerformance(20) + // 分析最近100个周期的交易表现(避免长期持仓的交易记录丢失) + // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易 + performance, err := trader.GetDecisionLogger().AnalyzePerformance(100) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("分析历史表现失败: %v", err), diff --git a/config.json.example b/config.json.example index 4df4314e..8f41523e 100644 --- a/config.json.example +++ b/config.json.example @@ -6,6 +6,7 @@ "ai_model": "deepseek", "exchange": "hyperliquid", "hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix", + "hyperliquid_wallet_addr": "your_ethereum_address", "hyperliquid_testnet": false, "deepseek_key": "your_deepseek_api_key", "initial_balance": 1000, diff --git a/config/config.go b/config/config.go index 7c3626ae..8b843b53 100644 --- a/config/config.go +++ b/config/config.go @@ -9,19 +9,20 @@ import ( // TraderConfig 单个trader的配置 type TraderConfig struct { - ID string `json:"id"` - Name string `json:"name"` - AIModel string `json:"ai_model"` // "qwen" or "deepseek" + ID string `json:"id"` + Name string `json:"name"` + AIModel string `json:"ai_model"` // "qwen" or "deepseek" // 交易平台选择(二选一) - Exchange string `json:"exchange"` // "binance" or "hyperliquid" + Exchange string `json:"exchange"` // "binance" or "hyperliquid" // 币安配置 - BinanceAPIKey string `json:"binance_api_key,omitempty"` - BinanceSecretKey string `json:"binance_secret_key,omitempty"` + BinanceAPIKey string `json:"binance_api_key,omitempty"` + BinanceSecretKey string `json:"binance_secret_key,omitempty"` // Hyperliquid配置 HyperliquidPrivateKey string `json:"hyperliquid_private_key,omitempty"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr,omitempty"` HyperliquidTestnet bool `json:"hyperliquid_testnet,omitempty"` // Aster配置 @@ -30,13 +31,13 @@ type TraderConfig struct { AsterPrivateKey string `json:"aster_private_key,omitempty"` // Aster API钱包私钥 // AI配置 - QwenKey string `json:"qwen_key,omitempty"` - DeepSeekKey string `json:"deepseek_key,omitempty"` + QwenKey string `json:"qwen_key,omitempty"` + DeepSeekKey string `json:"deepseek_key,omitempty"` // 自定义AI API配置(支持任何OpenAI格式的API) - CustomAPIURL string `json:"custom_api_url,omitempty"` - CustomAPIKey string `json:"custom_api_key,omitempty"` - CustomModelName string `json:"custom_model_name,omitempty"` + CustomAPIURL string `json:"custom_api_url,omitempty"` + CustomAPIKey string `json:"custom_api_key,omitempty"` + CustomModelName string `json:"custom_model_name,omitempty"` InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` @@ -44,15 +45,15 @@ type TraderConfig struct { // LeverageConfig 杠杆配置 type LeverageConfig struct { - BTCETHLeverage int `json:"btc_eth_leverage"` // BTC和ETH的杠杆倍数(主账户建议5-50,子账户≤5) - AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币的杠杆倍数(主账户建议5-20,子账户≤5) + BTCETHLeverage int `json:"btc_eth_leverage"` // BTC和ETH的杠杆倍数(主账户建议5-50,子账户≤5) + AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币的杠杆倍数(主账户建议5-20,子账户≤5) } // Config 总配置 type Config struct { Traders []TraderConfig `json:"traders"` - UseDefaultCoins bool `json:"use_default_coins"` // 是否使用默认主流币种列表 - DefaultCoins []string `json:"default_coins"` // 默认主流币种池 + 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"` APIServerPort int `json:"api_server_port"` @@ -83,13 +84,13 @@ func LoadConfig(filename string) (*Config, error) { if len(config.DefaultCoins) == 0 { config.DefaultCoins = []string{ "BTCUSDT", - "ETHUSDT", - "SOLUSDT", - "BNBUSDT", - "XRPUSDT", - "DOGEUSDT", - "ADAUSDT", - "HYPEUSDT", + "ETHUSDT", + "SOLUSDT", + "BNBUSDT", + "XRPUSDT", + "DOGEUSDT", + "ADAUSDT", + "HYPEUSDT", } } diff --git a/decision/engine.go b/decision/engine.go index a25f3644..76bcffca 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -90,7 +90,7 @@ type FullDecision struct { } // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) -func GetFullDecision(ctx *Context) (*FullDecision, error) { +func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) { // 1. 为所有币种获取市场数据 if err := fetchMarketDataForContext(ctx); err != nil { return nil, fmt.Errorf("获取市场数据失败: %w", err) @@ -101,7 +101,7 @@ func GetFullDecision(ctx *Context) (*FullDecision, error) { userPrompt := buildUserPrompt(ctx) // 3. 调用AI API(使用 system + user prompt) - aiResponse, err := mcp.CallWithMessages(systemPrompt, userPrompt) + aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) if err != nil { return nil, fmt.Errorf("调用AI API失败: %w", err) } diff --git a/go.sum b/go.sum index c30f7b02..2cd01ad0 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= diff --git a/logger/decision_logger.go b/logger/decision_logger.go index e5acba8b..ed446f20 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -269,16 +269,20 @@ type Statistics struct { // TradeOutcome 单笔交易结果 type TradeOutcome struct { - Symbol string `json:"symbol"` // 币种 - Side string `json:"side"` // long/short - OpenPrice float64 `json:"open_price"` // 开仓价 - ClosePrice float64 `json:"close_price"` // 平仓价 - PnL float64 `json:"pn_l"` // 盈亏(USDT) - PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比 - Duration string `json:"duration"` // 持仓时长 - OpenTime time.Time `json:"open_time"` // 开仓时间 - CloseTime time.Time `json:"close_time"` // 平仓时间 - WasStopLoss bool `json:"was_stop_loss"` // 是否止损 + Symbol string `json:"symbol"` // 币种 + Side string `json:"side"` // long/short + Quantity float64 `json:"quantity"` // 仓位数量 + Leverage int `json:"leverage"` // 杠杆倍数 + OpenPrice float64 `json:"open_price"` // 开仓价 + ClosePrice float64 `json:"close_price"` // 平仓价 + PositionValue float64 `json:"position_value"` // 仓位价值(quantity × openPrice) + MarginUsed float64 `json:"margin_used"` // 保证金使用(positionValue / leverage) + PnL float64 `json:"pn_l"` // 盈亏(USDT) + PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比(相对保证金) + Duration string `json:"duration"` // 持仓时长 + OpenTime time.Time `json:"open_time"` // 开仓时间 + CloseTime time.Time `json:"close_time"` // 平仓时间 + WasStopLoss bool `json:"was_stop_loss"` // 是否止损 } // PerformanceAnalysis 交易表现分析 @@ -330,7 +334,45 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna // 追踪持仓状态:symbol_side -> {side, openPrice, openTime, quantity, leverage} openPositions := make(map[string]map[string]interface{}) - // 遍历所有记录 + // 为了避免开仓记录在窗口外导致匹配失败,需要先从所有历史记录中找出未平仓的持仓 + // 获取更多历史记录来构建完整的持仓状态(使用更大的窗口) + allRecords, err := l.GetLatestRecords(lookbackCycles * 3) // 扩大3倍窗口 + if err == nil && len(allRecords) > len(records) { + // 先从扩大的窗口中收集所有开仓记录 + for _, record := range allRecords { + for _, action := range record.Decisions { + if !action.Success { + continue + } + + symbol := action.Symbol + side := "" + if action.Action == "open_long" || action.Action == "close_long" { + side = "long" + } else if action.Action == "open_short" || action.Action == "close_short" { + side = "short" + } + posKey := symbol + "_" + side + + switch action.Action { + case "open_long", "open_short": + // 记录开仓 + openPositions[posKey] = map[string]interface{}{ + "side": side, + "openPrice": action.Price, + "openTime": action.Timestamp, + "quantity": action.Quantity, + "leverage": action.Leverage, + } + case "close_long", "close_short": + // 移除已平仓记录 + delete(openPositions, posKey) + } + } + } + } + + // 遍历分析窗口内的记录,生成交易结果 for _, record := range records { for _, action := range record.Decisions { if !action.Success { @@ -348,7 +390,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna switch action.Action { case "open_long", "open_short": - // 记录开仓(包括数量和杠杆) + // 更新开仓记录(可能已经在预填充时记录过了) openPositions[posKey] = map[string]interface{}{ "side": side, "openPrice": action.Price, @@ -358,7 +400,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna } case "close_long", "close_short": - // 查找对应的开仓记录 + // 查找对应的开仓记录(可能来自预填充或当前窗口) if openPos, exists := openPositions[posKey]; exists { openPrice := openPos["openPrice"].(float64) openTime := openPos["openTime"].(time.Time) @@ -366,42 +408,53 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) - // 计算盈亏百分比 - pnlPct := 0.0 + // 计算实际盈亏(USDT) + // 合约交易 PnL 计算:quantity × 价格差 + // 注意:杠杆不影响绝对盈亏,只影响保证金需求 + var pnl float64 if side == "long" { - pnlPct = ((action.Price - openPrice) / openPrice) * 100 + pnl = quantity * (action.Price - openPrice) } else { - pnlPct = ((openPrice - action.Price) / openPrice) * 100 + pnl = quantity * (openPrice - action.Price) } - // 计算实际盈亏(USDT) - // PnL = 仓位价值 × 价格变化百分比 × 杠杆倍数 + // 计算盈亏百分比(相对保证金) positionValue := quantity * openPrice - pnl := positionValue * (pnlPct / 100) * float64(leverage) + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (pnl / marginUsed) * 100 + } // 记录交易结果 outcome := TradeOutcome{ - Symbol: symbol, - Side: side, - OpenPrice: openPrice, - ClosePrice: action.Price, - PnL: pnl, - PnLPct: pnlPct, - Duration: action.Timestamp.Sub(openTime).String(), - OpenTime: openTime, - CloseTime: action.Timestamp, + Symbol: symbol, + Side: side, + Quantity: quantity, + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: pnl, + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, } analysis.RecentTrades = append(analysis.RecentTrades, outcome) analysis.TotalTrades++ + // 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损) if pnl > 0 { analysis.WinningTrades++ analysis.AvgWin += pnl - } else { + } else if pnl < 0 { analysis.LosingTrades++ analysis.AvgLoss += pnl } + // pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数 // 更新币种统计 if _, exists := analysis.SymbolStats[symbol]; !exists { @@ -414,7 +467,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna stats.TotalPnL += pnl if pnl > 0 { stats.WinningTrades++ - } else { + } else if pnl < 0 { stats.LosingTrades++ } @@ -444,6 +497,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna // 注意:totalLossAmount 是负数,所以取负号得到绝对值 if totalLossAmount != 0 { analysis.ProfitFactor = totalWinAmount / (-totalLossAmount) + } else if totalWinAmount > 0 { + // 只有盈利没有亏损的情况,设置为一个很大的值表示完美策略 + analysis.ProfitFactor = 999.0 } } diff --git a/manager/trader_manager.go b/manager/trader_manager.go index cb01508e..20b61c46 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -40,6 +40,7 @@ func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, BinanceAPIKey: cfg.BinanceAPIKey, BinanceSecretKey: cfg.BinanceSecretKey, HyperliquidPrivateKey: cfg.HyperliquidPrivateKey, + HyperliquidWalletAddr: cfg.HyperliquidWalletAddr, HyperliquidTestnet: cfg.HyperliquidTestnet, AsterUser: cfg.AsterUser, AsterSigner: cfg.AsterSigner, diff --git a/mcp/client.go b/mcp/client.go index 7c8643eb..12973753 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -19,71 +19,74 @@ const ( ProviderCustom Provider = "custom" ) -// Config AI API配置 -type Config struct { - Provider Provider - APIKey string - SecretKey string // 阿里云需要 - BaseURL string - Model string - Timeout time.Duration +// Client AI API配置 +type Client struct { + Provider Provider + APIKey string + SecretKey string // 阿里云需要 + BaseURL string + Model string + Timeout time.Duration UseFullURL bool // 是否使用完整URL(不添加/chat/completions) } -// 默认配置 -var defaultConfig = Config{ - Provider: ProviderDeepSeek, - BaseURL: "https://api.deepseek.com/v1", - Model: "deepseek-chat", - Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 +func New() *Client { + // 默认配置 + var defaultClient = Client{ + Provider: ProviderDeepSeek, + BaseURL: "https://api.deepseek.com/v1", + Model: "deepseek-chat", + Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 + } + return &defaultClient } // SetDeepSeekAPIKey 设置DeepSeek API密钥 -func SetDeepSeekAPIKey(apiKey string) { - defaultConfig.Provider = ProviderDeepSeek - defaultConfig.APIKey = apiKey - defaultConfig.BaseURL = "https://api.deepseek.com/v1" - defaultConfig.Model = "deepseek-chat" +func (cfg *Client) SetDeepSeekAPIKey(apiKey string) { + cfg.Provider = ProviderDeepSeek + cfg.APIKey = apiKey + cfg.BaseURL = "https://api.deepseek.com/v1" + cfg.Model = "deepseek-chat" } // SetQwenAPIKey 设置阿里云Qwen API密钥 -func SetQwenAPIKey(apiKey, secretKey string) { - defaultConfig.Provider = ProviderQwen - defaultConfig.APIKey = apiKey - defaultConfig.SecretKey = secretKey - defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max +func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) { + cfg.Provider = ProviderQwen + cfg.APIKey = apiKey + cfg.SecretKey = secretKey + cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } // SetCustomAPI 设置自定义OpenAI兼容API -func SetCustomAPI(apiURL, apiKey, modelName string) { - defaultConfig.Provider = ProviderCustom - defaultConfig.APIKey = apiKey +func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) { + cfg.Provider = ProviderCustom + cfg.APIKey = apiKey // 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions) if strings.HasSuffix(apiURL, "#") { - defaultConfig.BaseURL = strings.TrimSuffix(apiURL, "#") - defaultConfig.UseFullURL = true + cfg.BaseURL = strings.TrimSuffix(apiURL, "#") + cfg.UseFullURL = true } else { - defaultConfig.BaseURL = apiURL - defaultConfig.UseFullURL = false + cfg.BaseURL = apiURL + cfg.UseFullURL = false } - defaultConfig.Model = modelName - defaultConfig.Timeout = 120 * time.Second + cfg.Model = modelName + cfg.Timeout = 120 * time.Second } -// SetConfig 设置完整的AI配置(高级用户) -func SetConfig(config Config) { - if config.Timeout == 0 { - config.Timeout = 30 * time.Second +// SetClient 设置完整的AI配置(高级用户) +func (cfg *Client) SetClient(Client Client) { + if Client.Timeout == 0 { + Client.Timeout = 30 * time.Second } - defaultConfig = config + cfg = &Client } // CallWithMessages 使用 system + user prompt 调用AI API(推荐) -func CallWithMessages(systemPrompt, userPrompt string) (string, error) { - if defaultConfig.APIKey == "" { +func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { + if cfg.APIKey == "" { return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") } @@ -96,7 +99,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) { fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) } - result, err := callOnce(systemPrompt, userPrompt) + result, err := cfg.callOnce(systemPrompt, userPrompt) if err == nil { if attempt > 1 { fmt.Printf("✓ AI API重试成功\n") @@ -122,7 +125,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) { } // callOnce 单次调用AI API(内部使用) -func callOnce(systemPrompt, userPrompt string) (string, error) { +func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建 messages 数组 messages := []map[string]string{} @@ -142,7 +145,7 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { // 构建请求体 requestBody := map[string]interface{}{ - "model": defaultConfig.Model, + "model": cfg.Model, "messages": messages, "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "max_tokens": 2000, @@ -158,12 +161,12 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { // 创建HTTP请求 var url string - if defaultConfig.UseFullURL { + if cfg.UseFullURL { // 使用完整URL,不添加/chat/completions - url = defaultConfig.BaseURL + url = cfg.BaseURL } else { // 默认行为:添加/chat/completions - url = fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL) + url = fmt.Sprintf("%s/chat/completions", cfg.BaseURL) } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { @@ -173,19 +176,19 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { req.Header.Set("Content-Type", "application/json") // 根据不同的Provider设置认证方式 - switch defaultConfig.Provider { + switch cfg.Provider { case ProviderDeepSeek: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) case ProviderQwen: // 阿里云Qwen使用API-Key认证 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) // 注意:如果使用的不是兼容模式,可能需要不同的认证方式 default: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) } // 发送请求 - client := &http.Client{Timeout: defaultConfig.Timeout} + client := &http.Client{Timeout: cfg.Timeout} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("发送请求失败: %w", err) diff --git a/pm2.config.js b/pm2.config.js new file mode 100644 index 00000000..2f166388 --- /dev/null +++ b/pm2.config.js @@ -0,0 +1,41 @@ +const path = require('path'); + +module.exports = { + apps: [ + { + name: 'nofx-backend', + script: './nofx', + cwd: __dirname, // 使用当前目录(配置文件所在目录) + interpreter: 'none', // 不使用解释器,直接执行二进制文件 + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '500M', + env: { + NODE_ENV: 'production' + }, + error_file: './logs/backend-error.log', + out_file: './logs/backend-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true + }, + { + name: 'nofx-frontend', + script: 'npm', + args: 'run dev', + cwd: path.join(__dirname, 'web'), // 动态拼接 web 目录 + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '300M', + env: { + NODE_ENV: 'development', + PORT: 3000 + }, + error_file: './logs/frontend-error.log', + out_file: './logs/frontend-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true + } + ] +}; diff --git a/pm2.sh b/pm2.sh new file mode 100755 index 00000000..b55c8412 --- /dev/null +++ b/pm2.sh @@ -0,0 +1,258 @@ +#!/bin/bash + +# NoFX Trading Bot - PM2 管理脚本 +# 用法: ./pm2.sh [start|stop|restart|status|logs|build] + +set -e + +# 自动获取脚本所在目录(支持符号链接) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$PROJECT_ROOT" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 函数:打印带颜色的消息 +print_info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_header() { + echo -e "${PURPLE}═══════════════════════════════════════${NC}" + echo -e "${PURPLE} 🤖 NoFX Trading Bot - PM2 Manager${NC}" + echo -e "${PURPLE}═══════════════════════════════════════${NC}" + echo "" +} + +# 函数:检查 PM2 是否安装 +check_pm2() { + if ! command -v pm2 &> /dev/null; then + print_error "PM2 未安装,请先安装: npm install -g pm2" + exit 1 + fi +} + +# 函数:确保日志目录存在 +ensure_log_dirs() { + mkdir -p "$PROJECT_ROOT/logs" + mkdir -p "$PROJECT_ROOT/web/logs" + print_info "日志目录已创建" +} + +# 函数:编译后端 +build_backend() { + print_info "正在编译后端..." + go build -o nofx + if [ $? -eq 0 ]; then + print_success "后端编译完成" + else + print_error "后端编译失败" + exit 1 + fi +} + +# 函数:构建前端(生产环境) +build_frontend() { + print_info "正在构建前端..." + cd web + npm run build + if [ $? -eq 0 ]; then + print_success "前端构建完成" + cd .. + else + print_error "前端构建失败" + exit 1 + fi +} + +# 函数:启动服务 +start_services() { + print_header + ensure_log_dirs + + # 检查后端二进制文件是否存在 + if [ ! -f "./nofx" ]; then + print_warning "后端二进制文件不存在,开始编译..." + build_backend + fi + + print_info "正在启动服务..." + pm2 start pm2.config.js + + sleep 2 + pm2 status + + echo "" + print_success "服务启动完成!" + echo "" + echo -e "${CYAN}📊 访问地址:${NC}" + echo -e " ${GREEN}前端:${NC} http://localhost:3000" + echo -e " ${GREEN}后端 API:${NC} http://localhost:8080" + echo "" + echo -e "${CYAN}📝 查看日志:${NC}" + echo -e " ${GREEN}实时日志:${NC} ./pm2.sh logs" + echo -e " ${GREEN}后端日志:${NC} ./pm2.sh logs backend" + echo -e " ${GREEN}前端日志:${NC} ./pm2.sh logs frontend" + echo "" +} + +# 函数:停止服务 +stop_services() { + print_header + print_info "正在停止服务..." + pm2 stop pm2.config.js + print_success "服务已停止" +} + +# 函数:重启服务 +restart_services() { + print_header + print_info "正在重启服务..." + pm2 restart pm2.config.js + sleep 2 + pm2 status + print_success "服务已重启" +} + +# 函数:删除服务 +delete_services() { + print_header + print_warning "正在删除 PM2 服务..." + pm2 delete pm2.config.js || true + print_success "PM2 服务已删除" +} + +# 函数:查看状态 +show_status() { + print_header + pm2 status + echo "" + print_info "详细信息:" + pm2 info nofx-backend + echo "" + pm2 info nofx-frontend +} + +# 函数:查看日志 +show_logs() { + if [ -z "$2" ]; then + # 显示所有日志 + pm2 logs + elif [ "$2" = "backend" ]; then + pm2 logs nofx-backend + elif [ "$2" = "frontend" ]; then + pm2 logs nofx-frontend + else + print_error "未知的日志类型: $2" + print_info "用法: ./pm2.sh logs [backend|frontend]" + exit 1 + fi +} + +# 函数:监控 +show_monitor() { + print_header + print_info "启动 PM2 监控面板..." + pm2 monit +} + +# 函数:重新编译并重启 +rebuild_and_restart() { + print_header + print_info "正在重新编译后端..." + build_backend + + print_info "正在重启后端服务..." + pm2 restart nofx-backend + + sleep 2 + pm2 status + print_success "后端已重新编译并重启" +} + +# 函数:显示帮助 +show_help() { + print_header + echo -e "${CYAN}使用方法:${NC}" + echo " ./pm2.sh [command]" + echo "" + echo -e "${CYAN}可用命令:${NC}" + echo -e " ${GREEN}start${NC} - 启动前后端服务" + echo -e " ${GREEN}stop${NC} - 停止所有服务" + echo -e " ${GREEN}restart${NC} - 重启所有服务" + echo -e " ${GREEN}status${NC} - 查看服务状态" + echo -e " ${GREEN}logs${NC} - 查看所有日志 (Ctrl+C 退出)" + echo -e " ${GREEN}logs backend${NC} - 查看后端日志" + echo -e " ${GREEN}logs frontend${NC} - 查看前端日志" + echo -e " ${GREEN}monitor${NC} - 打开 PM2 监控面板" + echo -e " ${GREEN}build${NC} - 编译后端" + echo -e " ${GREEN}rebuild${NC} - 重新编译后端并重启" + echo -e " ${GREEN}delete${NC} - 删除 PM2 服务" + echo -e " ${GREEN}help${NC} - 显示此帮助信息" + echo "" + echo -e "${CYAN}示例:${NC}" + echo " ./pm2.sh start # 启动服务" + echo " ./pm2.sh logs backend # 查看后端日志" + echo " ./pm2.sh rebuild # 重新编译后端并重启" + echo "" +} + +# 主逻辑 +check_pm2 + +case "${1:-help}" in + start) + start_services + ;; + stop) + stop_services + ;; + restart) + restart_services + ;; + status) + show_status + ;; + logs) + show_logs "$@" + ;; + monitor|mon) + show_monitor + ;; + build) + build_backend + ;; + rebuild) + rebuild_and_restart + ;; + delete|remove) + delete_services + ;; + help|--help|-h) + show_help + ;; + *) + print_error "未知命令: $1" + echo "" + show_help + exit 1 + ;; +esac diff --git a/trader/auto_trader.go b/trader/auto_trader.go index cc39341b..42bc2e69 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -29,6 +29,7 @@ type AutoTraderConfig struct { // Hyperliquid配置 HyperliquidPrivateKey string + HyperliquidWalletAddr string HyperliquidTestnet bool // Aster配置 @@ -66,21 +67,22 @@ type AutoTraderConfig struct { // AutoTrader 自动交易器 type AutoTrader struct { - id string // Trader唯一标识 - name string // Trader显示名称 - aiModel string // AI模型名称 - exchange string // 交易平台名称 - config AutoTraderConfig - trader Trader // 使用Trader接口(支持多平台) - decisionLogger *logger.DecisionLogger // 决策日志记录器 - initialBalance float64 - dailyPnL float64 - lastResetTime time.Time - stopUntil time.Time - isRunning bool - startTime time.Time // 系统启动时间 - callCount int // AI调用次数 - positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + id string // Trader唯一标识 + name string // Trader显示名称 + aiModel string // AI模型名称 + exchange string // 交易平台名称 + config AutoTraderConfig + trader Trader // 使用Trader接口(支持多平台) + mcpClient *mcp.Client + decisionLogger *logger.DecisionLogger // 决策日志记录器 + initialBalance float64 + dailyPnL float64 + lastResetTime time.Time + stopUntil time.Time + isRunning bool + startTime time.Time // 系统启动时间 + callCount int // AI调用次数 + positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) } // NewAutoTrader 创建自动交易器 @@ -100,18 +102,20 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { } } + mcpClient := mcp.New() + // 初始化AI if config.AIModel == "custom" { // 使用自定义API - mcp.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName) + mcpClient.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName) log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) } else if config.UseQwen || config.AIModel == "qwen" { // 使用Qwen - mcp.SetQwenAPIKey(config.QwenKey, "") + mcpClient.SetQwenAPIKey(config.QwenKey, "") log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name) } else { // 默认使用DeepSeek - mcp.SetDeepSeekAPIKey(config.DeepSeekKey) + mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) } @@ -135,7 +139,7 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey) case "hyperliquid": log.Printf("🏦 [%s] 使用Hyperliquid交易", config.Name) - trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidTestnet) + trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet) if err != nil { return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err) } @@ -159,18 +163,19 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { decisionLogger := logger.NewDecisionLogger(logDir) return &AutoTrader{ - id: config.ID, - name: config.Name, - aiModel: config.AIModel, - exchange: config.Exchange, - config: config, - trader: trader, - decisionLogger: decisionLogger, - initialBalance: config.InitialBalance, - lastResetTime: time.Now(), - startTime: time.Now(), - callCount: 0, - isRunning: false, + id: config.ID, + name: config.Name, + aiModel: config.AIModel, + exchange: config.Exchange, + config: config, + trader: trader, + mcpClient: mcpClient, + decisionLogger: decisionLogger, + initialBalance: config.InitialBalance, + lastResetTime: time.Now(), + startTime: time.Now(), + callCount: 0, + isRunning: false, positionFirstSeenTime: make(map[string]int64), }, nil } @@ -282,7 +287,7 @@ func (at *AutoTrader) runCycle() error { // 4. 调用AI获取完整决策 log.Println("🤖 正在请求AI分析并决策...") - decision, err := decision.GetFullDecision(ctx) + decision, err := decision.GetFullDecision(ctx, at.mcpClient) // 即使有错误,也保存思维链、决策和输入prompt(用于debug) if decision != nil { @@ -505,8 +510,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { marginUsedPct = (totalMarginUsed / totalEquity) * 100 } - // 5. 分析历史表现(最近20个周期) - performance, err := at.decisionLogger.AnalyzePerformance(20) + // 5. 分析历史表现(最近100个周期,避免长期持仓的交易记录丢失) + // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易 + performance, err := at.decisionLogger.AnalyzePerformance(100) if err != nil { log.Printf("⚠️ 分析历史表现失败: %v", err) // 不影响主流程,继续执行(但设置performance为nil以避免传递错误数据) @@ -515,11 +521,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { // 6. 构建上下文 ctx := &decision.Context{ - CurrentTime: time.Now().Format("2006-01-02 15:04:05"), - RuntimeMinutes: int(time.Since(at.startTime).Minutes()), - CallCount: at.callCount, - BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数 - AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数 + CurrentTime: time.Now().Format("2006-01-02 15:04:05"), + RuntimeMinutes: int(time.Since(at.startTime).Minutes()), + CallCount: at.callCount, + BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数 + AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数 Account: decision.AccountInfo{ TotalEquity: totalEquity, AvailableBalance: availableBalance, diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index a9402209..c37494d6 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,7 +2,6 @@ package trader import ( "context" - "crypto/ecdsa" "fmt" "log" "strconv" @@ -20,7 +19,7 @@ type HyperliquidTrader struct { } // NewHyperliquidTrader 创建Hyperliquid交易器 -func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrader, error) { +func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) { // 解析私钥 privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { @@ -33,13 +32,13 @@ func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrade apiURL = hyperliquid.TestnetAPIURL } - // 从私钥生成钱包地址 - pubKey := privateKey.Public() - publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("无法转换公钥") - } - walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() + // // 从私钥生成钱包地址 + // pubKey := privateKey.Public() + // publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) + // if !ok { + // return nil, fmt.Errorf("无法转换公钥") + // } + // walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() ctx := context.Background() @@ -99,9 +98,9 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 钱包余额(已实现)= AccountValue - 未实现盈亏 walletBalance := accountValue - totalUnrealizedPnl - result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) - result["availableBalance"] = accountValue - totalMarginUsed // 可用余额 - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 + result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) + result["availableBalance"] = accountValue - totalMarginUsed // 可用余额 + result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f", accountValue, @@ -515,8 +514,8 @@ func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quan order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: isBuy, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: roundedStopPrice, // 使用处理后的价格 + Size: roundedQuantity, // 使用四舍五入后的数量 + Price: roundedStopPrice, // 使用处理后的价格 OrderType: hyperliquid.OrderType{ Trigger: &hyperliquid.TriggerOrderType{ TriggerPx: roundedStopPrice, @@ -552,8 +551,8 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: isBuy, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: roundedTakeProfitPrice, // 使用处理后的价格 + Size: roundedQuantity, // 使用四舍五入后的数量 + Price: roundedTakeProfitPrice, // 使用处理后的价格 OrderType: hyperliquid.OrderType{ Trigger: &hyperliquid.TriggerOrderType{ TriggerPx: roundedTakeProfitPrice, @@ -577,7 +576,7 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (string, error) { coin := convertSymbolToHyperliquid(symbol) szDecimals := t.getSzDecimals(coin) - + // 使用szDecimals格式化数量 formatStr := fmt.Sprintf("%%.%df", szDecimals) return fmt.Sprintf(formatStr, quantity), nil @@ -604,13 +603,13 @@ func (t *HyperliquidTrader) getSzDecimals(coin string) int { // roundToSzDecimals 将数量四舍五入到正确的精度 func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 { szDecimals := t.getSzDecimals(coin) - + // 计算倍数(10^szDecimals) multiplier := 1.0 for i := 0; i < szDecimals; i++ { multiplier *= 10.0 } - + // 四舍五入 return float64(int(quantity*multiplier+0.5)) / multiplier } @@ -621,9 +620,9 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { if price == 0 { return 0 } - + const sigfigs = 5 // Hyperliquid标准:5位有效数字 - + // 计算价格的数量级 var magnitude float64 if price < 0 { @@ -631,7 +630,7 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { } else { magnitude = price } - + // 计算需要的倍数 multiplier := 1.0 for magnitude >= 10 { @@ -642,12 +641,12 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { magnitude *= 10 multiplier *= 10 } - + // 应用有效数字精度 for i := 0; i < sigfigs-1; i++ { multiplier *= 10 } - + // 四舍五入 rounded := float64(int(price*multiplier+0.5)) / multiplier return rounded diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index 8ef1a840..20da61d2 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -6,8 +6,12 @@ import { api } from '../lib/api'; interface TradeOutcome { symbol: string; side: string; + quantity: number; + leverage: number; open_price: number; close_price: number; + position_value: number; + margin_used: number; pn_l: number; pn_l_pct: number; duration: string; @@ -558,6 +562,34 @@ export default function AILearning({ traderId }: AILearningProps) { + {/* Position Details */} +
+
+
Quantity
+
+ {trade.quantity ? trade.quantity.toFixed(4) : '-'} +
+
+
+
Leverage
+
+ {trade.leverage ? `${trade.leverage}x` : '-'} +
+
+
+
Position Value
+
+ {trade.position_value ? `$${trade.position_value.toFixed(2)}` : '-'} +
+
+
+
Margin Used
+
+ {trade.margin_used ? `$${trade.margin_used.toFixed(2)}` : '-'} +
+
+
+
diff --git a/web/vite.config.ts b/web/vite.config.ts index 3e35b475..150bbec9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { + host: '0.0.0.0', port: 3000, proxy: { '/api': {