mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 03:21:04 +08:00
300
PM2_DEPLOYMENT.md
Normal file
300
PM2_DEPLOYMENT.md
Normal file
@@ -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
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
107
mcp/client.go
107
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)
|
||||
|
||||
41
pm2.config.js
Normal file
41
pm2.config.js
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
258
pm2.sh
Executable file
258
pm2.sh
Executable file
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position Details */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#94A3B8' }}>Quantity</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
|
||||
{trade.quantity ? trade.quantity.toFixed(4) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div style={{ color: '#94A3B8' }}>Leverage</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#FCD34D' }}>
|
||||
{trade.leverage ? `${trade.leverage}x` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94A3B8' }}>Position Value</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
|
||||
{trade.position_value ? `$${trade.position_value.toFixed(2)}` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div style={{ color: '#94A3B8' }}>Margin Used</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#A78BFA' }}>
|
||||
{trade.margin_used ? `$${trade.margin_used.toFixed(2)}` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg p-2 mb-2" style={{
|
||||
background: isProfitable ? 'rgba(16, 185, 129, 0.1)' : 'rgba(248, 113, 113, 0.1)'
|
||||
}}>
|
||||
|
||||
@@ -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': {
|
||||
|
||||
Reference in New Issue
Block a user