mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-28 14:32:45 +08:00
refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity - Split 15+ large Go files (800-2200 lines) into focused modules: kernel/engine.go, backtest/runner.go, market/data.go, store/position.go, api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders - Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx into domain-specific modules with barrel re-exports - Remove stale files: screenshots, .yml.old, pyproject.toml - Remove unused scripts/ and cmd/ directories - Remove broken/outdated test files (network-dependent, stale expectations)
This commit is contained in:
@@ -1,302 +0,0 @@
|
||||
# Mars AI交易系统 - 加密密钥生成脚本
|
||||
|
||||
本目录包含用于Mars AI交易系统加密环境设置的脚本工具。
|
||||
|
||||
## 🔐 加密架构
|
||||
|
||||
Mars AI交易系统使用双重加密架构来保护敏感数据:
|
||||
|
||||
1. **RSA-OAEP + AES-GCM 混合加密** - 用于前端到后端的安全通信
|
||||
2. **AES-256-GCM 数据库加密** - 用于敏感数据的存储加密
|
||||
|
||||
### 加密流程
|
||||
|
||||
```
|
||||
前端 → RSA-OAEP加密AES密钥 + AES-GCM加密数据 → 后端 → 存储时AES-256-GCM加密
|
||||
```
|
||||
|
||||
## 📝 脚本说明
|
||||
|
||||
### 1. `setup_encryption.sh` - 一键环境设置 ⭐推荐⭐
|
||||
|
||||
**功能**: 自动生成所有必要的密钥并配置环境
|
||||
|
||||
```bash
|
||||
./scripts/setup_encryption.sh
|
||||
```
|
||||
|
||||
**生成内容**:
|
||||
- RSA-2048 密钥对 (`secrets/rsa_key`, `secrets/rsa_key.pub`)
|
||||
- AES-256 数据加密密钥 (保存到 `.env`)
|
||||
- 自动权限设置和验证
|
||||
|
||||
**适用场景**:
|
||||
- 首次部署
|
||||
- 开发环境快速设置
|
||||
- 生产环境初始化
|
||||
|
||||
### 2. `generate_rsa_keys.sh` - RSA密钥生成
|
||||
|
||||
**功能**: 专门生成RSA密钥对
|
||||
|
||||
```bash
|
||||
./scripts/generate_rsa_keys.sh
|
||||
```
|
||||
|
||||
**生成内容**:
|
||||
- `secrets/rsa_key` (私钥, 权限 600)
|
||||
- `secrets/rsa_key.pub` (公钥, 权限 644)
|
||||
|
||||
**技术规格**:
|
||||
- 算法: RSA-OAEP
|
||||
- 密钥长度: 2048 bits
|
||||
- 格式: PEM
|
||||
|
||||
### 3. `generate_data_key.sh` - 数据加密密钥生成
|
||||
|
||||
**功能**: 生成数据库加密密钥
|
||||
|
||||
```bash
|
||||
./scripts/generate_data_key.sh
|
||||
```
|
||||
|
||||
**生成内容**:
|
||||
- 32字节(256位)随机密钥
|
||||
- Base64编码格式
|
||||
- 可选保存到 `.env` 文件
|
||||
|
||||
**技术规格**:
|
||||
- 算法: AES-256-GCM
|
||||
- 编码: Base64
|
||||
- 环境变量: `DATA_ENCRYPTION_KEY`
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方案1: 一键设置 (推荐)
|
||||
|
||||
```bash
|
||||
# 克隆项目后,直接运行一键设置
|
||||
cd mars-ai-trading
|
||||
./scripts/setup_encryption.sh
|
||||
|
||||
# 按提示确认即可完成所有设置
|
||||
```
|
||||
|
||||
### 方案2: 分步设置
|
||||
|
||||
```bash
|
||||
# 1. 生成RSA密钥对
|
||||
./scripts/generate_rsa_keys.sh
|
||||
|
||||
# 2. 生成数据加密密钥
|
||||
./scripts/generate_data_key.sh
|
||||
|
||||
# 3. 启动系统
|
||||
source .env && ./mars
|
||||
```
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
生成完成后的目录结构:
|
||||
|
||||
```
|
||||
mars-ai-trading/
|
||||
├── secrets/
|
||||
│ ├── rsa_key # RSA私钥 (600权限)
|
||||
│ └── rsa_key.pub # RSA公钥 (644权限)
|
||||
├── .env # 环境变量 (600权限)
|
||||
│ └── DATA_ENCRYPTION_KEY=xxx
|
||||
└── scripts/
|
||||
├── setup_encryption.sh # 一键设置脚本
|
||||
├── generate_rsa_keys.sh # RSA密钥生成
|
||||
└── generate_data_key.sh # 数据密钥生成
|
||||
```
|
||||
|
||||
## 🔒 安全要求
|
||||
|
||||
### 文件权限
|
||||
|
||||
| 文件 | 权限 | 说明 |
|
||||
|------|------|------|
|
||||
| `secrets/rsa_key` | 600 | 仅所有者可读写 |
|
||||
| `secrets/rsa_key.pub` | 644 | 所有人可读 |
|
||||
| `.env` | 600 | 仅所有者可读写 |
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# 必需的环境变量
|
||||
DATA_ENCRYPTION_KEY=<32字节Base64编码的AES密钥>
|
||||
```
|
||||
|
||||
## 🐳 Docker部署
|
||||
|
||||
### 使用环境文件
|
||||
|
||||
```bash
|
||||
# 生成密钥
|
||||
./scripts/setup_encryption.sh
|
||||
|
||||
# Docker运行
|
||||
docker run --env-file .env -v $(pwd)/secrets:/app/secrets mars-ai-trading
|
||||
```
|
||||
|
||||
### 使用环境变量
|
||||
|
||||
```bash
|
||||
export DATA_ENCRYPTION_KEY="<生成的密钥>"
|
||||
docker run -e DATA_ENCRYPTION_KEY mars-ai-trading
|
||||
```
|
||||
|
||||
## ☸️ Kubernetes部署
|
||||
|
||||
### 创建Secret
|
||||
|
||||
```bash
|
||||
# 从现有.env文件创建
|
||||
kubectl create secret generic mars-crypto-key --from-env-file=.env
|
||||
|
||||
# 或直接指定密钥
|
||||
kubectl create secret generic mars-crypto-key \
|
||||
--from-literal=DATA_ENCRYPTION_KEY="<生成的密钥>"
|
||||
```
|
||||
|
||||
### 挂载RSA密钥
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: mars-rsa-keys
|
||||
type: Opaque
|
||||
data:
|
||||
rsa_key: <base64编码的私钥>
|
||||
rsa_key.pub: <base64编码的公钥>
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mars-ai-trading
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mars
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: mars-crypto-key
|
||||
volumeMounts:
|
||||
- name: rsa-keys
|
||||
mountPath: /app/secrets
|
||||
volumes:
|
||||
- name: rsa-keys
|
||||
secret:
|
||||
secretName: mars-rsa-keys
|
||||
```
|
||||
|
||||
## 🔄 密钥轮换
|
||||
|
||||
### 数据加密密钥轮换
|
||||
|
||||
```bash
|
||||
# 1. 生成新密钥
|
||||
./scripts/generate_data_key.sh
|
||||
|
||||
# 2. 备份旧数据库
|
||||
cp data.db data.db.backup
|
||||
|
||||
# 3. 重启服务 (会自动处理密钥迁移)
|
||||
source .env && ./mars
|
||||
```
|
||||
|
||||
### RSA密钥轮换
|
||||
|
||||
```bash
|
||||
# 1. 生成新密钥对
|
||||
./scripts/generate_rsa_keys.sh
|
||||
|
||||
# 2. 重启服务
|
||||
./mars
|
||||
```
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **权限错误**
|
||||
```bash
|
||||
chmod 600 secrets/rsa_key .env
|
||||
chmod 644 secrets/rsa_key.pub
|
||||
```
|
||||
|
||||
2. **OpenSSL未安装**
|
||||
```bash
|
||||
# macOS
|
||||
brew install openssl
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install openssl
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install openssl
|
||||
```
|
||||
|
||||
3. **环境变量未加载**
|
||||
```bash
|
||||
source .env
|
||||
echo $DATA_ENCRYPTION_KEY
|
||||
```
|
||||
|
||||
4. **密钥验证失败**
|
||||
```bash
|
||||
# 验证RSA私钥
|
||||
openssl rsa -in secrets/rsa_key -check -noout
|
||||
|
||||
# 验证公钥
|
||||
openssl rsa -in secrets/rsa_key.pub -pubin -text -noout
|
||||
```
|
||||
|
||||
### 日志检查
|
||||
|
||||
启动时检查以下日志:
|
||||
- `🔐 初始化加密服务...`
|
||||
- `✅ 加密服务初始化成功`
|
||||
|
||||
## 📊 性能考虑
|
||||
|
||||
- **RSA加密**: 仅用于小量密钥交换,性能影响极小
|
||||
- **AES加密**: 数据库字段级加密,对读写性能影响约5-10%
|
||||
- **内存使用**: 加密服务约占用2-5MB内存
|
||||
|
||||
## 🔐 算法详细说明
|
||||
|
||||
### RSA-OAEP-2048
|
||||
- **用途**: 前端到后端的混合加密中的密钥交换
|
||||
- **密钥长度**: 2048 bits
|
||||
- **填充**: OAEP with SHA-256
|
||||
- **安全级别**: 相当于112位对称加密
|
||||
|
||||
### AES-256-GCM
|
||||
- **用途**: 数据库敏感字段存储加密
|
||||
- **密钥长度**: 256 bits
|
||||
- **模式**: GCM (Galois/Counter Mode)
|
||||
- **认证**: 内置消息认证
|
||||
- **安全级别**: 256位安全强度
|
||||
|
||||
## 📋 合规性
|
||||
|
||||
此加密实现满足以下标准:
|
||||
- **FIPS 140-2**: AES-256 和 RSA-2048
|
||||
- **Common Criteria**: EAL4+
|
||||
- **NIST推荐**: SP 800-57 密钥管理
|
||||
- **行业标准**: 符合金融业数据保护要求
|
||||
|
||||
---
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请检查:
|
||||
1. OpenSSL版本 >= 1.1.1
|
||||
2. 文件权限设置正确
|
||||
3. 环境变量加载成功
|
||||
4. 系统日志中的加密初始化信息
|
||||
@@ -1,98 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/store"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbPath string
|
||||
var dryRun bool
|
||||
|
||||
flag.StringVar(&dbPath, "db", "./data/data.db", "数据库文件路径")
|
||||
flag.BoolVar(&dryRun, "dry-run", false, "只检查不删除(预览模式)")
|
||||
flag.Parse()
|
||||
|
||||
// 确保数据库文件存在
|
||||
absPath, err := filepath.Abs(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无效的数据库路径: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
log.Fatalf("❌ 数据库文件不存在: %s", absPath)
|
||||
}
|
||||
|
||||
fmt.Printf("📂 数据库路径: %s\n", absPath)
|
||||
|
||||
// 打开数据库
|
||||
s, err := store.New(absPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无法打开数据库: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
orderStore := s.Order()
|
||||
|
||||
// 1. 检查重复订单数量
|
||||
fmt.Println("\n🔍 检查重复数据...")
|
||||
dupOrders, err := orderStore.GetDuplicateOrdersCount()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 检查重复订单失败: %v", err)
|
||||
}
|
||||
fmt.Printf(" 📋 重复订单: %d 条\n", dupOrders)
|
||||
|
||||
dupFills, err := orderStore.GetDuplicateFillsCount()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 检查重复成交失败: %v", err)
|
||||
}
|
||||
fmt.Printf(" 📊 重复成交: %d 条\n", dupFills)
|
||||
|
||||
if dupOrders == 0 && dupFills == 0 {
|
||||
fmt.Println("\n✅ 数据库没有重复记录,无需清理")
|
||||
return
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Println("\n⚠️ 预览模式(--dry-run),不会删除数据")
|
||||
fmt.Println(" 运行 'go run scripts/cleanup_duplicates.go' 来执行实际清理")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 清理重复订单
|
||||
if dupOrders > 0 {
|
||||
fmt.Println("\n🧹 清理重复订单...")
|
||||
deleted, err := orderStore.CleanupDuplicateOrders()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 清理失败: %v", err)
|
||||
}
|
||||
fmt.Printf(" ✅ 删除了 %d 条重复订单\n", deleted)
|
||||
}
|
||||
|
||||
// 3. 清理重复成交
|
||||
if dupFills > 0 {
|
||||
fmt.Println("\n🧹 清理重复成交...")
|
||||
deleted, err := orderStore.CleanupDuplicateFills()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 清理失败: %v", err)
|
||||
}
|
||||
fmt.Printf(" ✅ 删除了 %d 条重复成交\n", deleted)
|
||||
}
|
||||
|
||||
// 4. 验证清理结果
|
||||
fmt.Println("\n🔍 验证清理结果...")
|
||||
dupOrdersAfter, _ := orderStore.GetDuplicateOrdersCount()
|
||||
dupFillsAfter, _ := orderStore.GetDuplicateFillsCount()
|
||||
fmt.Printf(" 📋 剩余重复订单: %d 条\n", dupOrdersAfter)
|
||||
fmt.Printf(" 📊 剩余重复成交: %d 条\n", dupFillsAfter)
|
||||
|
||||
if dupOrdersAfter == 0 && dupFillsAfter == 0 {
|
||||
fmt.Println("\n✅ 清理完成!数据库已去重")
|
||||
} else {
|
||||
fmt.Println("\n⚠️ 仍有重复数据,可能需要手动检查")
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/store"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbPath string
|
||||
var force bool
|
||||
|
||||
flag.StringVar(&dbPath, "db", "./data/data.db", "数据库文件路径")
|
||||
flag.BoolVar(&force, "force", false, "跳过确认直接删除")
|
||||
flag.Parse()
|
||||
|
||||
// 确保数据库文件存在
|
||||
absPath, err := filepath.Abs(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无效的数据库路径: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
log.Fatalf("❌ 数据库文件不存在: %s", absPath)
|
||||
}
|
||||
|
||||
fmt.Printf("📂 数据库路径: %s\n", absPath)
|
||||
|
||||
// 打开数据库
|
||||
s, err := store.New(absPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无法打开数据库: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
db := s.DB()
|
||||
|
||||
// 统计当前数据
|
||||
var orderCount, fillCount int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM trader_orders`).Scan(&orderCount)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM trader_fills`).Scan(&fillCount)
|
||||
|
||||
fmt.Printf("\n📊 当前数据统计:\n")
|
||||
fmt.Printf(" trader_orders: %d 条记录\n", orderCount)
|
||||
fmt.Printf(" trader_fills: %d 条记录\n", fillCount)
|
||||
|
||||
if orderCount == 0 && fillCount == 0 {
|
||||
fmt.Println("\n✅ 表已经是空的,无需清空")
|
||||
return
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
if !force {
|
||||
fmt.Println("\n⚠️ 警告: 此操作将删除所有订单和成交记录,无法恢复!")
|
||||
fmt.Print("\n确认删除?请输入 'yes' 继续: ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if input != "yes" {
|
||||
fmt.Println("\n❌ 操作已取消")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n🗑️ 开始清空表...")
|
||||
|
||||
// 清空 trader_fills 表(先删除,因为有外键约束)
|
||||
result, err := db.Exec(`DELETE FROM trader_fills`)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 清空 trader_fills 失败: %v", err)
|
||||
}
|
||||
fillsDeleted, _ := result.RowsAffected()
|
||||
fmt.Printf(" ✅ 删除了 %d 条成交记录\n", fillsDeleted)
|
||||
|
||||
// 清空 trader_orders 表
|
||||
result, err = db.Exec(`DELETE FROM trader_orders`)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 清空 trader_orders 失败: %v", err)
|
||||
}
|
||||
ordersDeleted, _ := result.RowsAffected()
|
||||
fmt.Printf(" ✅ 删除了 %d 条订单记录\n", ordersDeleted)
|
||||
|
||||
// 重置自增ID(可选,让ID从1重新开始)
|
||||
_, err = db.Exec(`DELETE FROM sqlite_sequence WHERE name IN ('trader_orders', 'trader_fills')`)
|
||||
if err == nil {
|
||||
fmt.Println(" ✅ 重置了自增ID计数器")
|
||||
}
|
||||
|
||||
// 验证清空结果
|
||||
db.QueryRow(`SELECT COUNT(*) FROM trader_orders`).Scan(&orderCount)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM trader_fills`).Scan(&fillCount)
|
||||
|
||||
fmt.Printf("\n🔍 验证结果:\n")
|
||||
fmt.Printf(" trader_orders: %d 条记录\n", orderCount)
|
||||
fmt.Printf(" trader_fills: %d 条记录\n", fillCount)
|
||||
|
||||
if orderCount == 0 && fillCount == 0 {
|
||||
fmt.Println("\n✅ 表已成功清空!")
|
||||
fmt.Println("\n💡 现在可以重新运行 trader 进行测试")
|
||||
fmt.Println(" 新的订单将从 ID=1 开始记录")
|
||||
} else {
|
||||
fmt.Println("\n⚠️ 清空未完成,请检查数据库")
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/store"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbPath string
|
||||
var traderID string
|
||||
|
||||
flag.StringVar(&dbPath, "db", "./data/data.db", "数据库文件路径")
|
||||
flag.StringVar(&traderID, "trader", "", "Trader ID(可选)")
|
||||
flag.Parse()
|
||||
|
||||
// 确保数据库文件存在
|
||||
absPath, err := filepath.Abs(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无效的数据库路径: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
log.Fatalf("❌ 数据库文件不存在: %s", absPath)
|
||||
}
|
||||
|
||||
fmt.Printf("📂 数据库路径: %s\n", absPath)
|
||||
|
||||
// 打开数据库
|
||||
s, err := store.New(absPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无法打开数据库: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
orderStore := s.Order()
|
||||
|
||||
// 如果指定了 traderID,获取该 trader 的订单
|
||||
if traderID == "" {
|
||||
fmt.Println("\n⚠️ 未指定 trader_id,使用: --trader <trader_id>")
|
||||
fmt.Println(" 获取所有 trader 的统计信息...\n")
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
orders, err := orderStore.GetTraderOrders(traderID, 100)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 获取订单失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n📋 找到 %d 条订单记录\n\n", len(orders))
|
||||
|
||||
if len(orders) == 0 {
|
||||
fmt.Println("⚠️ 没有订单数据!可能的原因:")
|
||||
fmt.Println(" 1. Trader 还没有执行过交易")
|
||||
fmt.Println(" 2. CreateOrder 插入失败(重复键冲突)")
|
||||
fmt.Println(" 3. 指定的 trader_id 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
var (
|
||||
totalOrders = len(orders)
|
||||
filledOrders = 0
|
||||
withFilledAt = 0
|
||||
withAvgFillPrice = 0
|
||||
withOrderAction = 0
|
||||
missingFilledAt = 0
|
||||
missingAvgPrice = 0
|
||||
missingOrderAction = 0
|
||||
)
|
||||
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
fmt.Printf("%-15s %-10s %-10s %-15s %-10s %-15s\n", "订单ID", "状态", "动作", "平均成交价", "成交时间", "问题")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
for _, order := range orders {
|
||||
issues := []string{}
|
||||
|
||||
if order.Status == "FILLED" {
|
||||
filledOrders++
|
||||
|
||||
// 检查 filled_at
|
||||
if order.FilledAt > 0 {
|
||||
withFilledAt++
|
||||
} else {
|
||||
missingFilledAt++
|
||||
issues = append(issues, "❌ 缺少成交时间")
|
||||
}
|
||||
|
||||
// 检查 avg_fill_price
|
||||
if order.AvgFillPrice > 0 {
|
||||
withAvgFillPrice++
|
||||
} else {
|
||||
missingAvgPrice++
|
||||
issues = append(issues, "❌ 成交价为0")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 order_action
|
||||
if order.OrderAction != "" {
|
||||
withOrderAction++
|
||||
} else {
|
||||
missingOrderAction++
|
||||
issues = append(issues, "⚠️ 缺少订单动作")
|
||||
}
|
||||
|
||||
issueStr := "✅ 正常"
|
||||
if len(issues) > 0 {
|
||||
issueStr = ""
|
||||
for i, issue := range issues {
|
||||
if i > 0 {
|
||||
issueStr += ", "
|
||||
}
|
||||
issueStr += issue
|
||||
}
|
||||
}
|
||||
|
||||
filledAtStr := "N/A"
|
||||
if order.FilledAt > 0 {
|
||||
filledAtStr = time.UnixMilli(order.FilledAt).Format("01-02 15:04")
|
||||
}
|
||||
|
||||
fmt.Printf("%-15s %-10s %-10s %-15.2f %-10s %s\n",
|
||||
order.ExchangeOrderID[:min(15, len(order.ExchangeOrderID))],
|
||||
order.Status,
|
||||
order.OrderAction,
|
||||
order.AvgFillPrice,
|
||||
filledAtStr,
|
||||
issueStr,
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
// 统计摘要
|
||||
fmt.Printf("\n📊 统计摘要:\n")
|
||||
fmt.Printf(" 总订单数: %d\n", totalOrders)
|
||||
fmt.Printf(" 已成交订单: %d\n", filledOrders)
|
||||
fmt.Printf(" 有成交时间: %d / %d (%.1f%%)\n", withFilledAt, filledOrders, float64(withFilledAt)/float64(max(filledOrders, 1))*100)
|
||||
fmt.Printf(" 有成交价格: %d / %d (%.1f%%)\n", withAvgFillPrice, filledOrders, float64(withAvgFillPrice)/float64(max(filledOrders, 1))*100)
|
||||
fmt.Printf(" 有订单动作: %d / %d (%.1f%%)\n", withOrderAction, totalOrders, float64(withOrderAction)/float64(max(totalOrders, 1))*100)
|
||||
|
||||
fmt.Printf("\n⚠️ 问题订单:\n")
|
||||
if missingFilledAt > 0 {
|
||||
fmt.Printf(" ❌ %d 条订单缺少成交时间 (filled_at)\n", missingFilledAt)
|
||||
}
|
||||
if missingAvgPrice > 0 {
|
||||
fmt.Printf(" ❌ %d 条订单成交价为 0 (avg_fill_price)\n", missingAvgPrice)
|
||||
}
|
||||
if missingOrderAction > 0 {
|
||||
fmt.Printf(" ⚠️ %d 条订单缺少订单动作 (order_action)\n", missingOrderAction)
|
||||
}
|
||||
|
||||
if missingFilledAt > 0 || missingAvgPrice > 0 {
|
||||
fmt.Println("\n💡 这些订单无法在图表上显示,因为:")
|
||||
fmt.Println(" - 缺少成交时间 → 前端无法定位到K线时间轴")
|
||||
fmt.Println(" - 成交价为 0 → 前端会过滤掉 (line 164: if (!orderPrice || orderPrice === 0) return)")
|
||||
fmt.Println("\n🔧 可能的原因:")
|
||||
fmt.Println(" 1. UpdateOrderStatus 没有被正确调用")
|
||||
fmt.Println(" 2. GetOrderStatus 返回的数据缺少 avgPrice 字段")
|
||||
fmt.Println(" 3. Lighter 交易所的订单状态查询有问题")
|
||||
}
|
||||
|
||||
if missingFilledAt == 0 && missingAvgPrice == 0 && missingOrderAction == 0 {
|
||||
fmt.Println("\n✅ 所有订单数据完整!")
|
||||
fmt.Println(" 如果图表仍然没有显示 B/S 标记,检查:")
|
||||
fmt.Println(" 1. 前端是否正确调用了 /api/orders API")
|
||||
fmt.Println(" 2. 浏览器控制台是否有错误")
|
||||
fmt.Println(" 3. 订单时间是否在图表的时间范围内")
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/store"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbPath string
|
||||
var dryRun bool
|
||||
|
||||
flag.StringVar(&dbPath, "db", "./data/data.db", "数据库文件路径")
|
||||
flag.BoolVar(&dryRun, "dry-run", false, "只检查不修复(预览模式)")
|
||||
flag.Parse()
|
||||
|
||||
// 确保数据库文件存在
|
||||
absPath, err := filepath.Abs(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无效的数据库路径: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
log.Fatalf("❌ 数据库文件不存在: %s", absPath)
|
||||
}
|
||||
|
||||
fmt.Printf("📂 数据库路径: %s\n", absPath)
|
||||
|
||||
// 打开数据库
|
||||
s, err := store.New(absPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 无法打开数据库: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
db := s.DB()
|
||||
|
||||
fmt.Println("\n🔍 检查需要修复的订单...")
|
||||
|
||||
// 1. 修复缺少 filled_at 的 FILLED 订单(使用 updated_at 或 created_at)
|
||||
var needFixFilledAt int
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM trader_orders
|
||||
WHERE status = 'FILLED' AND (filled_at IS NULL OR filled_at = '')
|
||||
`).Scan(&needFixFilledAt)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 查询失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📋 缺少成交时间的订单: %d 条\n", needFixFilledAt)
|
||||
|
||||
// 2. 修复 avg_fill_price = 0 的 FILLED 订单(使用 price 字段)
|
||||
var needFixAvgPrice int
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM trader_orders
|
||||
WHERE status = 'FILLED' AND (avg_fill_price = 0 OR avg_fill_price IS NULL) AND price > 0
|
||||
`).Scan(&needFixAvgPrice)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 查询失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" 💰 成交价为0的订单: %d 条\n", needFixAvgPrice)
|
||||
|
||||
if needFixFilledAt == 0 && needFixAvgPrice == 0 {
|
||||
fmt.Println("\n✅ 没有需要修复的订单!")
|
||||
return
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Println("\n⚠️ 预览模式(--dry-run),不会修改数据")
|
||||
fmt.Println(" 运行 'go run scripts/fix_order_data.go' 来执行实际修复")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n🔧 开始修复...")
|
||||
|
||||
// 修复缺少 filled_at 的订单
|
||||
if needFixFilledAt > 0 {
|
||||
result, err := db.Exec(`
|
||||
UPDATE trader_orders
|
||||
SET filled_at = COALESCE(updated_at, created_at)
|
||||
WHERE status = 'FILLED' AND (filled_at IS NULL OR filled_at = '')
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 修复成交时间失败: %v", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
fmt.Printf(" ✅ 修复了 %d 条订单的成交时间\n", rows)
|
||||
}
|
||||
|
||||
// 修复 avg_fill_price = 0 的订单
|
||||
if needFixAvgPrice > 0 {
|
||||
result, err := db.Exec(`
|
||||
UPDATE trader_orders
|
||||
SET avg_fill_price = price,
|
||||
filled_quantity = quantity
|
||||
WHERE status = 'FILLED'
|
||||
AND (avg_fill_price = 0 OR avg_fill_price IS NULL)
|
||||
AND price > 0
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 修复成交价失败: %v", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
fmt.Printf(" ✅ 修复了 %d 条订单的成交价\n", rows)
|
||||
}
|
||||
|
||||
// 验证修复结果
|
||||
fmt.Println("\n🔍 验证修复结果...")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
var stillMissingFilledAt int
|
||||
db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM trader_orders
|
||||
WHERE status = 'FILLED' AND (filled_at IS NULL OR filled_at = '')
|
||||
`).Scan(&stillMissingFilledAt)
|
||||
|
||||
var stillMissingAvgPrice int
|
||||
db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM trader_orders
|
||||
WHERE status = 'FILLED' AND (avg_fill_price = 0 OR avg_fill_price IS NULL)
|
||||
`).Scan(&stillMissingAvgPrice)
|
||||
|
||||
fmt.Printf(" 📋 仍缺少成交时间: %d 条\n", stillMissingFilledAt)
|
||||
fmt.Printf(" 💰 仍缺少成交价: %d 条\n", stillMissingAvgPrice)
|
||||
|
||||
if stillMissingFilledAt == 0 && stillMissingAvgPrice == 0 {
|
||||
fmt.Println("\n✅ 修复完成!所有订单数据已完整")
|
||||
fmt.Println("\n💡 现在刷新图表页面,应该能看到 B/S 标记了")
|
||||
} else {
|
||||
fmt.Println("\n⚠️ 仍有部分订单无法修复,可能需要手动检查")
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"nofx/crypto"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("🔄 Starting database migration to encrypted format...")
|
||||
|
||||
// 1. Check database file
|
||||
dbPath := "data/data.db"
|
||||
if len(os.Args) > 1 {
|
||||
dbPath = os.Args[1]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
log.Fatalf("❌ Database file does not exist: %s", dbPath)
|
||||
}
|
||||
|
||||
// 2. Backup database
|
||||
backupPath := fmt.Sprintf("%s.pre_encryption_backup", dbPath)
|
||||
log.Printf("📦 Backing up database to: %s", backupPath)
|
||||
|
||||
input, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Failed to read database: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(backupPath, input, 0600); err != nil {
|
||||
log.Fatalf("❌ Backup failed: %v", err)
|
||||
}
|
||||
|
||||
// 3. Open database
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 4. Initialize CryptoService (load key from environment variables)
|
||||
cs, err := crypto.NewCryptoService()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Failed to initialize encryption service: %v", err)
|
||||
}
|
||||
|
||||
// 5. Migrate exchange configurations
|
||||
if err := migrateExchanges(db, cs); err != nil {
|
||||
log.Fatalf("❌ Failed to migrate exchange configurations: %v", err)
|
||||
}
|
||||
|
||||
// 6. Migrate AI model configurations
|
||||
if err := migrateAIModels(db, cs); err != nil {
|
||||
log.Fatalf("❌ Failed to migrate AI model configurations: %v", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Data migration completed!")
|
||||
log.Printf("📝 Original data backed up at: %s", backupPath)
|
||||
log.Println("⚠️ Please verify system functionality before manually deleting backup file")
|
||||
}
|
||||
|
||||
// migrateExchanges migrates exchange configurations
|
||||
func migrateExchanges(db *sql.DB, cs *crypto.CryptoService) error {
|
||||
log.Println("🔄 Migrating exchange configurations...")
|
||||
|
||||
// Query all unencrypted records (encrypted data starts with ENC:v1:)
|
||||
rows, err := db.Query(`
|
||||
SELECT user_id, id, api_key, secret_key,
|
||||
COALESCE(hyperliquid_private_key, ''),
|
||||
COALESCE(aster_private_key, '')
|
||||
FROM exchanges
|
||||
WHERE (api_key != '' AND api_key NOT LIKE 'ENC:v1:%')
|
||||
OR (secret_key != '' AND secret_key NOT LIKE 'ENC:v1:%')
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var userID, exchangeID, apiKey, secretKey, hlPrivateKey, asterPrivateKey string
|
||||
if err := rows.Scan(&userID, &exchangeID, &apiKey, &secretKey, &hlPrivateKey, &asterPrivateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Encrypt each field
|
||||
encAPIKey, err := cs.EncryptForStorage(apiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt API Key: %w", err)
|
||||
}
|
||||
|
||||
encSecretKey, err := cs.EncryptForStorage(secretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt Secret Key: %w", err)
|
||||
}
|
||||
|
||||
encHLPrivateKey := ""
|
||||
if hlPrivateKey != "" {
|
||||
encHLPrivateKey, err = cs.EncryptForStorage(hlPrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt Hyperliquid Private Key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
encAsterPrivateKey := ""
|
||||
if asterPrivateKey != "" {
|
||||
encAsterPrivateKey, err = cs.EncryptForStorage(asterPrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt Aster Private Key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
_, err = tx.Exec(`
|
||||
UPDATE exchanges
|
||||
SET api_key = ?, secret_key = ?,
|
||||
hyperliquid_private_key = ?, aster_private_key = ?
|
||||
WHERE user_id = ? AND id = ?
|
||||
`, encAPIKey, encSecretKey, encHLPrivateKey, encAsterPrivateKey, userID, exchangeID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update database: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ Encrypted: [%s] %s", userID, exchangeID)
|
||||
count++
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("✅ Migrated %d exchange configurations", count)
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateAIModels migrates AI model configurations
|
||||
func migrateAIModels(db *sql.DB, cs *crypto.CryptoService) error {
|
||||
log.Println("🔄 Migrating AI model configurations...")
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT user_id, id, api_key
|
||||
FROM ai_models
|
||||
WHERE api_key != '' AND api_key NOT LIKE 'ENC:v1:%'
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var userID, modelID, apiKey string
|
||||
if err := rows.Scan(&userID, &modelID, &apiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encAPIKey, err := cs.EncryptForStorage(apiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt API Key: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
UPDATE ai_models SET api_key = ? WHERE user_id = ? AND id = ?
|
||||
`, encAPIKey, userID, modelID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update database: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ Encrypted: [%s] %s", userID, modelID)
|
||||
count++
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("✅ Migrated %d AI model configurations", count)
|
||||
return nil
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🔍 PR Health Check Script
|
||||
# Analyzes your PR and gives suggestions on how to meet the new standards
|
||||
# This script only analyzes and suggests - it won't modify your code
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Counters
|
||||
ISSUES_FOUND=0
|
||||
WARNINGS_FOUND=0
|
||||
PASSED_CHECKS=0
|
||||
|
||||
# Helper functions
|
||||
log_section() {
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} $1${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
log_check() {
|
||||
echo -e "${BLUE}🔍 Checking: $1${NC}"
|
||||
}
|
||||
|
||||
log_pass() {
|
||||
echo -e "${GREEN}✅ PASS: $1${NC}"
|
||||
((PASSED_CHECKS++))
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ WARNING: $1${NC}"
|
||||
((WARNINGS_FOUND++))
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ ISSUE: $1${NC}"
|
||||
((ISSUES_FOUND++))
|
||||
}
|
||||
|
||||
log_suggestion() {
|
||||
echo -e "${CYAN}💡 Suggestion: $1${NC}"
|
||||
}
|
||||
|
||||
log_command() {
|
||||
echo -e "${GREEN} Run: ${NC}$1"
|
||||
}
|
||||
|
||||
# Welcome
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ NOFX PR Health Check ║"
|
||||
echo "║ Analyze your PR and get suggestions ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a git repo
|
||||
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
log_error "Not a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current branch
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo -e "${BLUE}Current branch: ${GREEN}$CURRENT_BRANCH${NC}"
|
||||
|
||||
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "dev" ]; then
|
||||
log_error "You're on the $CURRENT_BRANCH branch. Please switch to your PR branch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if upstream exists
|
||||
if ! git remote | grep -q "^upstream$"; then
|
||||
log_warning "Upstream remote not found"
|
||||
log_suggestion "Add upstream remote:"
|
||||
log_command "git remote add upstream https://github.com/NoFxAiOS/nofx.git"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 1. GIT BRANCH CHECKS
|
||||
# ═══════════════════════════════════════════
|
||||
log_section "1. Git Branch Status"
|
||||
|
||||
# Check if branch is up to date with upstream
|
||||
log_check "Is branch based on latest upstream/dev?"
|
||||
if git remote | grep -q "^upstream$"; then
|
||||
git fetch upstream -q 2>/dev/null || true
|
||||
|
||||
if git merge-base --is-ancestor upstream/dev HEAD 2>/dev/null; then
|
||||
log_pass "Branch is up to date with upstream/dev"
|
||||
else
|
||||
log_error "Branch is not based on latest upstream/dev"
|
||||
log_suggestion "Rebase your branch:"
|
||||
log_command "git fetch upstream && git rebase upstream/dev"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
log_warning "Cannot check - upstream remote not configured"
|
||||
fi
|
||||
|
||||
# Check for merge conflicts
|
||||
log_check "Any merge conflicts?"
|
||||
if git diff --check > /dev/null 2>&1; then
|
||||
log_pass "No merge conflicts detected"
|
||||
else
|
||||
log_error "Merge conflicts detected"
|
||||
log_suggestion "Resolve conflicts and commit"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 2. COMMIT MESSAGE CHECKS
|
||||
# ═══════════════════════════════════════════
|
||||
log_section "2. Commit Messages"
|
||||
|
||||
# Get commits in this branch (not in upstream/dev)
|
||||
if git remote | grep -q "^upstream$"; then
|
||||
COMMITS=$(git log upstream/dev..HEAD --oneline 2>/dev/null || git log --oneline -10)
|
||||
else
|
||||
COMMITS=$(git log --oneline -10)
|
||||
fi
|
||||
|
||||
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
|
||||
echo -e "${BLUE}Found $COMMIT_COUNT commit(s) in your branch${NC}"
|
||||
echo ""
|
||||
|
||||
# Check each commit message
|
||||
echo "$COMMITS" | while read -r line; do
|
||||
COMMIT_MSG=$(echo "$line" | cut -d' ' -f2-)
|
||||
|
||||
# Check if follows conventional commits
|
||||
if echo "$COMMIT_MSG" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci|security)(\(.+\))?: .+"; then
|
||||
log_pass "\"$COMMIT_MSG\""
|
||||
else
|
||||
log_warning "\"$COMMIT_MSG\""
|
||||
log_suggestion "Should follow format: type(scope): description"
|
||||
echo " Examples:"
|
||||
echo " - feat(exchange): add OKX integration"
|
||||
echo " - fix(trader): resolve position bug"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
|
||||
# Suggest PR title based on commits
|
||||
echo ""
|
||||
log_check "Suggested PR title:"
|
||||
SUGGESTED_TITLE=$(git log --pretty=%s upstream/dev..HEAD 2>/dev/null | head -1 || git log --pretty=%s -1)
|
||||
echo -e "${GREEN} \"$SUGGESTED_TITLE\"${NC}"
|
||||
echo ""
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 3. CODE QUALITY - BACKEND (Go)
|
||||
# ═══════════════════════════════════════════
|
||||
if find . -name "*.go" -not -path "./vendor/*" -not -path "./.git/*" | grep -q .; then
|
||||
log_section "3. Backend Code Quality (Go)"
|
||||
|
||||
# Check if Go is installed
|
||||
if ! command -v go &> /dev/null; then
|
||||
log_warning "Go not installed - skipping backend checks"
|
||||
log_suggestion "Install Go: https://go.dev/doc/install"
|
||||
else
|
||||
# Check go fmt
|
||||
log_check "Go code formatting (go fmt)"
|
||||
UNFORMATTED=$(gofmt -l . 2>/dev/null | grep -v vendor || true)
|
||||
if [ -z "$UNFORMATTED" ]; then
|
||||
log_pass "All Go files are formatted"
|
||||
else
|
||||
log_error "Some files need formatting:"
|
||||
echo "$UNFORMATTED" | head -5 | while read -r file; do
|
||||
echo " - $file"
|
||||
done
|
||||
log_suggestion "Format your code:"
|
||||
log_command "go fmt ./..."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check go vet
|
||||
log_check "Go static analysis (go vet)"
|
||||
if go vet ./... > /tmp/vet-output.txt 2>&1; then
|
||||
log_pass "No issues found by go vet"
|
||||
else
|
||||
log_error "Go vet found issues:"
|
||||
head -10 /tmp/vet-output.txt | sed 's/^/ /'
|
||||
log_suggestion "Fix the issues above"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check tests exist
|
||||
log_check "Do tests exist?"
|
||||
TEST_FILES=$(find . -name "*_test.go" -not -path "./vendor/*" | wc -l)
|
||||
if [ "$TEST_FILES" -gt 0 ]; then
|
||||
log_pass "Found $TEST_FILES test file(s)"
|
||||
else
|
||||
log_warning "No test files found"
|
||||
log_suggestion "Add tests for your changes"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
log_check "Running Go tests..."
|
||||
if go test ./... -v > /tmp/test-output.txt 2>&1; then
|
||||
log_pass "All tests passed"
|
||||
else
|
||||
log_error "Some tests failed:"
|
||||
grep -E "FAIL|ERROR" /tmp/test-output.txt | head -10 | sed 's/^/ /' || true
|
||||
log_suggestion "Fix failing tests:"
|
||||
log_command "go test ./... -v"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 4. CODE QUALITY - FRONTEND
|
||||
# ═══════════════════════════════════════════
|
||||
if [ -d "web" ]; then
|
||||
log_section "4. Frontend Code Quality"
|
||||
|
||||
# Check if npm is installed
|
||||
if ! command -v npm &> /dev/null; then
|
||||
log_warning "npm not installed - skipping frontend checks"
|
||||
log_suggestion "Install Node.js: https://nodejs.org/"
|
||||
else
|
||||
cd web
|
||||
|
||||
# Check if node_modules exists
|
||||
if [ ! -d "node_modules" ]; then
|
||||
log_warning "Dependencies not installed"
|
||||
log_suggestion "Install dependencies:"
|
||||
log_command "cd web && npm install"
|
||||
cd ..
|
||||
else
|
||||
# Check linting
|
||||
log_check "Frontend linting"
|
||||
if npm run lint > /tmp/lint-output.txt 2>&1; then
|
||||
log_pass "No linting issues"
|
||||
else
|
||||
log_error "Linting issues found:"
|
||||
tail -20 /tmp/lint-output.txt | sed 's/^/ /' || true
|
||||
log_suggestion "Fix linting issues:"
|
||||
log_command "cd web && npm run lint -- --fix"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check type errors
|
||||
log_check "TypeScript type checking"
|
||||
if npm run type-check > /tmp/typecheck-output.txt 2>&1; then
|
||||
log_pass "No type errors"
|
||||
else
|
||||
log_error "Type errors found:"
|
||||
tail -20 /tmp/typecheck-output.txt | sed 's/^/ /' || true
|
||||
log_suggestion "Fix type errors in your code"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check build
|
||||
log_check "Frontend build"
|
||||
if npm run build > /tmp/build-output.txt 2>&1; then
|
||||
log_pass "Build successful"
|
||||
else
|
||||
log_error "Build failed:"
|
||||
tail -20 /tmp/build-output.txt | sed 's/^/ /' || true
|
||||
log_suggestion "Fix build errors"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 5. PR SIZE CHECK
|
||||
# ═══════════════════════════════════════════
|
||||
log_section "5. PR Size"
|
||||
|
||||
if git remote | grep -q "^upstream$"; then
|
||||
ADDED=$(git diff --numstat upstream/dev...HEAD | awk '{sum+=$1} END {print sum+0}')
|
||||
DELETED=$(git diff --numstat upstream/dev...HEAD | awk '{sum+=$2} END {print sum+0}')
|
||||
TOTAL=$((ADDED + DELETED))
|
||||
FILES_CHANGED=$(git diff --name-only upstream/dev...HEAD | wc -l)
|
||||
|
||||
echo -e "${BLUE}Lines changed: ${GREEN}+$ADDED ${RED}-$DELETED ${NC}(total: $TOTAL)"
|
||||
echo -e "${BLUE}Files changed: ${GREEN}$FILES_CHANGED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$TOTAL" -lt 100 ]; then
|
||||
log_pass "Small PR (<100 lines) - ideal for quick review"
|
||||
elif [ "$TOTAL" -lt 500 ]; then
|
||||
log_pass "Medium PR (100-500 lines) - reasonable size"
|
||||
elif [ "$TOTAL" -lt 1000 ]; then
|
||||
log_warning "Large PR (500-1000 lines) - consider splitting"
|
||||
log_suggestion "Breaking into smaller PRs makes review faster"
|
||||
else
|
||||
log_error "Very large PR (>1000 lines) - strongly consider splitting"
|
||||
log_suggestion "Split into multiple smaller PRs, each with a focused change"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 6. DOCUMENTATION CHECK
|
||||
# ═══════════════════════════════════════════
|
||||
log_section "6. Documentation"
|
||||
|
||||
# Check if README or docs were updated
|
||||
log_check "Documentation updates"
|
||||
if git remote | grep -q "^upstream$"; then
|
||||
DOC_CHANGES=$(git diff --name-only upstream/dev...HEAD | grep -E "\.(md|txt)$" || true)
|
||||
|
||||
if [ -n "$DOC_CHANGES" ]; then
|
||||
log_pass "Documentation files updated"
|
||||
echo "$DOC_CHANGES" | sed 's/^/ - /'
|
||||
else
|
||||
# Check if this is a feature/fix that might need docs
|
||||
COMMIT_TYPES=$(git log --pretty=%s upstream/dev..HEAD | grep -oE "^(feat|fix)" || true)
|
||||
if [ -n "$COMMIT_TYPES" ]; then
|
||||
log_warning "No documentation updates found"
|
||||
log_suggestion "Consider updating docs if your changes affect usage"
|
||||
echo ""
|
||||
else
|
||||
log_pass "No documentation update needed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 7. ROADMAP ALIGNMENT
|
||||
# ═══════════════════════════════════════════
|
||||
log_section "7. Roadmap Alignment"
|
||||
|
||||
log_check "Does your PR align with the roadmap?"
|
||||
echo ""
|
||||
echo "Current priorities (Phase 1):"
|
||||
echo " ✅ Security enhancements"
|
||||
echo " ✅ AI model integrations"
|
||||
echo " ✅ Exchange integrations (OKX, Bybit, Lighter, EdgeX)"
|
||||
echo " ✅ UI/UX improvements"
|
||||
echo " ✅ Performance optimizations"
|
||||
echo " ✅ Bug fixes"
|
||||
echo ""
|
||||
log_suggestion "Check roadmap: https://github.com/NoFxAiOS/nofx/blob/dev/docs/roadmap/README.md"
|
||||
echo ""
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# FINAL REPORT
|
||||
# ═══════════════════════════════════════════
|
||||
log_section "Summary Report"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Passed checks: $PASSED_CHECKS${NC}"
|
||||
echo -e "${YELLOW}⚠️ Warnings: $WARNINGS_FOUND${NC}"
|
||||
echo -e "${RED}❌ Issues found: $ISSUES_FOUND${NC}"
|
||||
echo ""
|
||||
|
||||
# Overall assessment
|
||||
if [ "$ISSUES_FOUND" -eq 0 ] && [ "$WARNINGS_FOUND" -eq 0 ]; then
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ 🎉 Excellent! Your PR looks great! ║"
|
||||
echo "║ Ready to submit or update your PR ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
elif [ "$ISSUES_FOUND" -eq 0 ]; then
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ 👍 Good! Minor warnings found ║"
|
||||
echo "║ Consider addressing warnings ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
elif [ "$ISSUES_FOUND" -le 3 ]; then
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ ⚠️ Issues found - Please fix ║"
|
||||
echo "║ See suggestions above ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
else
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ ❌ Multiple issues found ║"
|
||||
echo "║ Please address issues before submitting ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📖 Next steps:"
|
||||
echo ""
|
||||
|
||||
if [ "$ISSUES_FOUND" -gt 0 ] || [ "$WARNINGS_FOUND" -gt 0 ]; then
|
||||
echo "1. Fix the issues and warnings listed above"
|
||||
echo "2. Run this script again to verify: ./scripts/pr-check.sh"
|
||||
echo "3. Commit your fixes"
|
||||
echo "4. Push to your PR: git push origin $CURRENT_BRANCH"
|
||||
else
|
||||
echo "1. Push your changes: git push origin $CURRENT_BRANCH"
|
||||
echo "2. Create or update your PR on GitHub"
|
||||
echo "3. Wait for automated CI checks"
|
||||
echo "4. Address reviewer feedback"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📚 Resources:"
|
||||
echo " - Contributing Guide: https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
||||
echo " - Migration Guide: https://github.com/NoFxAiOS/nofx/blob/dev/docs/community/MIGRATION_ANNOUNCEMENT.md"
|
||||
echo ""
|
||||
|
||||
# Cleanup temp files
|
||||
rm -f /tmp/vet-output.txt /tmp/test-output.txt /tmp/lint-output.txt /tmp/typecheck-output.txt /tmp/build-output.txt
|
||||
|
||||
echo "✨ Analysis complete! Good luck with your PR! 🚀"
|
||||
echo ""
|
||||
@@ -1,335 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🔄 PR Migration Script for Contributors
|
||||
# This script helps you migrate your PR to the new format
|
||||
# Run this in your local fork to update your PR automatically
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
confirm() {
|
||||
read -p "$(echo -e ${YELLOW}"$1 (y/N): "${NC})" -n 1 -r
|
||||
echo
|
||||
[[ $REPLY =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
# Welcome message
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ NOFX PR Migration Tool ║"
|
||||
echo "║ Migrate your PR to the new format ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a git repo
|
||||
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
log_error "Not a git repository. Please run this from your NOFX fork."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check current branch
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
log_info "Current branch: $CURRENT_BRANCH"
|
||||
|
||||
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "dev" ]; then
|
||||
log_warning "You're on the $CURRENT_BRANCH branch."
|
||||
log_info "This script should be run on your PR branch."
|
||||
|
||||
# List branches
|
||||
log_info "Your branches:"
|
||||
git branch
|
||||
|
||||
echo ""
|
||||
read -p "Enter your PR branch name: " PR_BRANCH
|
||||
|
||||
if [ -z "$PR_BRANCH" ]; then
|
||||
log_error "No branch specified. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout "$PR_BRANCH" || {
|
||||
log_error "Failed to checkout branch $PR_BRANCH"
|
||||
exit 1
|
||||
}
|
||||
|
||||
CURRENT_BRANCH="$PR_BRANCH"
|
||||
fi
|
||||
|
||||
log_success "Working on branch: $CURRENT_BRANCH"
|
||||
|
||||
echo ""
|
||||
log_info "What this script will do:"
|
||||
echo " 1. ✅ Verify you're rebased on latest upstream/dev"
|
||||
echo " 2. ✅ Check and format Go code (go fmt)"
|
||||
echo " 3. ✅ Run Go linting (go vet)"
|
||||
echo " 4. ✅ Run Go tests"
|
||||
echo " 5. ✅ Check frontend code (if modified)"
|
||||
echo " 6. ✅ Give you feedback and suggestions"
|
||||
echo ""
|
||||
log_warning "Make sure you've already run: git fetch upstream && git rebase upstream/dev"
|
||||
echo ""
|
||||
|
||||
if ! confirm "Continue with migration?"; then
|
||||
log_info "Migration cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Step 1: Verify upstream sync
|
||||
echo ""
|
||||
log_info "Step 1: Verifying upstream sync..."
|
||||
|
||||
# Check if upstream remote exists
|
||||
if ! git remote | grep -q "^upstream$"; then
|
||||
log_warning "Upstream remote not found. Adding it..."
|
||||
git remote add upstream https://github.com/NoFxAiOS/nofx.git
|
||||
git fetch upstream
|
||||
log_success "Added upstream remote"
|
||||
fi
|
||||
|
||||
# Check if we're up to date with upstream/dev
|
||||
if git merge-base --is-ancestor upstream/dev HEAD; then
|
||||
log_success "Your branch is up to date with upstream/dev"
|
||||
else
|
||||
log_warning "Your branch is not based on latest upstream/dev"
|
||||
log_info "Please run first: git fetch upstream && git rebase upstream/dev"
|
||||
|
||||
if confirm "Try to rebase now?"; then
|
||||
git fetch upstream
|
||||
if git rebase upstream/dev; then
|
||||
log_success "Successfully rebased on upstream/dev"
|
||||
else
|
||||
log_error "Rebase failed. Please resolve conflicts manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_warning "Skipping rebase. Results may not be accurate."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: Backend checks (if Go files exist)
|
||||
if find . -name "*.go" -not -path "./vendor/*" | grep -q .; then
|
||||
echo ""
|
||||
log_info "Step 2: Running backend checks..."
|
||||
|
||||
# Check if Go is installed
|
||||
if ! command -v go &> /dev/null; then
|
||||
log_warning "Go not found. Skipping backend checks."
|
||||
log_info "Install Go: https://go.dev/doc/install"
|
||||
else
|
||||
# Format Go code
|
||||
log_info "Formatting Go code..."
|
||||
if go fmt ./...; then
|
||||
log_success "Go code formatted"
|
||||
|
||||
# Check if there are changes
|
||||
if ! git diff --quiet; then
|
||||
log_info "Formatting created changes. Committing..."
|
||||
git add .
|
||||
git commit -m "chore: format Go code with go fmt" || true
|
||||
fi
|
||||
else
|
||||
log_warning "Go formatting had issues (non-critical)"
|
||||
fi
|
||||
|
||||
# Run go vet
|
||||
log_info "Running go vet..."
|
||||
if go vet ./...; then
|
||||
log_success "Go vet passed"
|
||||
else
|
||||
log_warning "Go vet found issues. Please review them."
|
||||
if confirm "Continue anyway?"; then
|
||||
log_info "Continuing..."
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
log_info "Running Go tests..."
|
||||
if go test ./...; then
|
||||
log_success "All Go tests passed"
|
||||
else
|
||||
log_warning "Some tests failed. Please fix them before pushing."
|
||||
if confirm "Continue anyway?"; then
|
||||
log_info "Continuing..."
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_info "Step 2: No Go files found, skipping backend checks"
|
||||
fi
|
||||
|
||||
# Step 3: Frontend checks (if web directory exists)
|
||||
if [ -d "web" ]; then
|
||||
echo ""
|
||||
log_info "Step 3: Running frontend checks..."
|
||||
|
||||
# Check if npm is installed
|
||||
if ! command -v npm &> /dev/null; then
|
||||
log_warning "npm not found. Skipping frontend checks."
|
||||
log_info "Install Node.js: https://nodejs.org/"
|
||||
else
|
||||
cd web
|
||||
|
||||
# Install dependencies if needed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
log_info "Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Run linter
|
||||
log_info "Running linter..."
|
||||
if npm run lint; then
|
||||
log_success "Linting passed"
|
||||
else
|
||||
log_warning "Linting found issues"
|
||||
log_info "Attempting to auto-fix..."
|
||||
npm run lint -- --fix || true
|
||||
|
||||
# Commit fixes if any
|
||||
if ! git diff --quiet; then
|
||||
git add .
|
||||
git commit -m "chore: fix linting issues" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Type check
|
||||
log_info "Running type check..."
|
||||
if npm run type-check; then
|
||||
log_success "Type checking passed"
|
||||
else
|
||||
log_warning "Type checking found issues. Please fix them."
|
||||
fi
|
||||
|
||||
# Build
|
||||
log_info "Testing build..."
|
||||
if npm run build; then
|
||||
log_success "Build successful"
|
||||
else
|
||||
log_error "Build failed. Please fix build errors."
|
||||
cd ..
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
else
|
||||
log_info "Step 3: No frontend changes, skipping frontend checks"
|
||||
fi
|
||||
|
||||
# Step 4: Check PR title format
|
||||
echo ""
|
||||
log_info "Step 4: Checking PR title format..."
|
||||
|
||||
# Get the commit messages to suggest a title
|
||||
COMMITS=$(git log upstream/dev..HEAD --oneline)
|
||||
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
|
||||
|
||||
log_info "Found $COMMIT_COUNT commit(s) in your PR"
|
||||
|
||||
if [ "$COMMIT_COUNT" -eq 1 ]; then
|
||||
SUGGESTED_TITLE=$(git log -1 --pretty=%s)
|
||||
else
|
||||
SUGGESTED_TITLE=$(git log --pretty=%s upstream/dev..HEAD | head -1)
|
||||
fi
|
||||
|
||||
log_info "Current/suggested title: $SUGGESTED_TITLE"
|
||||
|
||||
# Check if it follows conventional commits
|
||||
if echo "$SUGGESTED_TITLE" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci|security)(\(.+\))?: .+"; then
|
||||
log_success "Title follows Conventional Commits format"
|
||||
else
|
||||
log_warning "Title doesn't follow Conventional Commits format"
|
||||
echo ""
|
||||
echo "Conventional Commits format:"
|
||||
echo " <type>(<scope>): <description>"
|
||||
echo ""
|
||||
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, ci, security"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " feat(exchange): add OKX integration"
|
||||
echo " fix(trader): resolve position tracking bug"
|
||||
echo " docs(readme): update installation guide"
|
||||
echo ""
|
||||
|
||||
read -p "Enter new title (or press Enter to keep current): " NEW_TITLE
|
||||
|
||||
if [ -n "$NEW_TITLE" ]; then
|
||||
log_info "You can update the PR title on GitHub after pushing"
|
||||
log_info "Suggested title: $NEW_TITLE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 5: Push changes
|
||||
echo ""
|
||||
log_info "Step 5: Ready to push changes"
|
||||
|
||||
# Check if there are changes to push
|
||||
if git diff upstream/dev..HEAD --quiet; then
|
||||
log_info "No changes to push"
|
||||
else
|
||||
log_info "Changes ready to push to origin/$CURRENT_BRANCH"
|
||||
|
||||
if confirm "Push changes now?"; then
|
||||
log_info "Pushing to origin/$CURRENT_BRANCH..."
|
||||
if git push -f origin "$CURRENT_BRANCH"; then
|
||||
log_success "Successfully pushed changes!"
|
||||
else
|
||||
log_error "Failed to push. You may need to push manually:"
|
||||
echo " git push -f origin $CURRENT_BRANCH"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_info "Skipped push. You can push manually later:"
|
||||
echo " git push -f origin $CURRENT_BRANCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════╗"
|
||||
echo "║ ✅ Migration Complete! ║"
|
||||
echo "╚═══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
log_success "Your PR has been migrated!"
|
||||
|
||||
echo ""
|
||||
log_info "Next steps:"
|
||||
echo " 1. Check your PR on GitHub"
|
||||
echo " 2. Update PR title if needed (Conventional Commits format)"
|
||||
echo " 3. Wait for CI checks to run"
|
||||
echo " 4. Address any reviewer feedback"
|
||||
echo ""
|
||||
|
||||
log_info "Need help? Ask in the PR comments or Telegram!"
|
||||
log_info "Telegram: https://t.me/nofx_dev_community"
|
||||
|
||||
echo ""
|
||||
log_success "Thank you for contributing to NOFX! 🚀"
|
||||
echo ""
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=================================="
|
||||
echo "NOFX 后端重启和测试脚本"
|
||||
echo "=================================="
|
||||
|
||||
# 1. 停止旧进程
|
||||
echo ""
|
||||
echo "1️⃣ 停止旧进程..."
|
||||
pkill -f "bin/nofx" || echo " 没有运行中的进程"
|
||||
sleep 2
|
||||
|
||||
# 2. 清理旧数据
|
||||
echo ""
|
||||
echo "2️⃣ 清理测试数据..."
|
||||
sqlite3 data/data.db "DELETE FROM trader_fills; DELETE FROM trader_orders;"
|
||||
echo " ✅ trader_orders 和 trader_fills 表已清空"
|
||||
|
||||
# 3. 验证数据库已清空
|
||||
ORDERS_COUNT=$(sqlite3 data/data.db "SELECT COUNT(*) FROM trader_orders")
|
||||
FILLS_COUNT=$(sqlite3 data/data.db "SELECT COUNT(*) FROM trader_fills")
|
||||
echo " 验证: trader_orders=$ORDERS_COUNT, trader_fills=$FILLS_COUNT"
|
||||
|
||||
# 4. 启动新进程
|
||||
echo ""
|
||||
echo "3️⃣ 启动新编译的后端服务..."
|
||||
if [ ! -f "bin/nofx" ]; then
|
||||
echo " ❌ bin/nofx 不存在,请先运行 go build -o bin/nofx ."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
nohup ./bin/nofx > data/nofx_$(date +%Y-%m-%d).log 2>&1 &
|
||||
NOFX_PID=$!
|
||||
echo " ✅ 后端已启动 (PID: $NOFX_PID)"
|
||||
|
||||
# 5. 等待服务启动
|
||||
echo ""
|
||||
echo "4️⃣ 等待服务启动..."
|
||||
sleep 3
|
||||
|
||||
# 6. 验证进程运行
|
||||
if ps -p $NOFX_PID > /dev/null; then
|
||||
echo " ✅ 后端进程运行正常 (PID: $NOFX_PID)"
|
||||
else
|
||||
echo " ❌ 后端进程启动失败,请检查日志"
|
||||
tail -20 data/nofx_$(date +%Y-%m-%d).log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "✅ 重启完成!"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
echo "📝 下一步操作:"
|
||||
echo " 1. 访问前端页面"
|
||||
echo " 2. 执行一次平仓操作(手动或AI)"
|
||||
echo " 3. 等待 10 秒(让 pollLighterTradeHistory 完成)"
|
||||
echo " 4. 检查数据库:"
|
||||
echo " sqlite3 data/data.db \"SELECT id, status, avg_fill_price, filled_quantity FROM trader_orders\""
|
||||
echo " 5. 刷新图表页面,应该能看到 B/S 标记"
|
||||
echo ""
|
||||
echo "📊 实时日志查看:"
|
||||
echo " tail -f data/nofx_$(date +%Y-%m-%d).log | grep -E 'Order recorded|Found matching trade|Fill recorded'"
|
||||
echo ""
|
||||
@@ -1,168 +0,0 @@
|
||||
//go:build ignore
|
||||
|
||||
// Test script to verify Lighter API authentication
|
||||
// Run: go run scripts/test_lighter_orders.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
lighterClient "github.com/elliottech/lighter-go/client"
|
||||
lighterHTTP "github.com/elliottech/lighter-go/client/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configuration - update these values
|
||||
walletAddr := os.Getenv("LIGHTER_WALLET")
|
||||
apiKeyPrivateKey := os.Getenv("LIGHTER_API_KEY")
|
||||
|
||||
if walletAddr == "" || apiKeyPrivateKey == "" {
|
||||
fmt.Println("Usage: LIGHTER_WALLET=0x... LIGHTER_API_KEY=... go run scripts/test_lighter_orders.go")
|
||||
fmt.Println("Environment variables required:")
|
||||
fmt.Println(" LIGHTER_WALLET - Ethereum wallet address")
|
||||
fmt.Println(" LIGHTER_API_KEY - API key private key (40 bytes hex)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("=== Lighter API Test ===")
|
||||
fmt.Printf("Wallet: %s\n\n", walletAddr)
|
||||
|
||||
baseURL := "https://mainnet.zklighter.elliot.ai"
|
||||
chainID := uint32(304)
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
// Step 1: Get account info (no auth required)
|
||||
fmt.Println("1. Getting account info...")
|
||||
accountIndex, err := getAccountIndex(client, baseURL, walletAddr)
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" OK: account_index = %d\n\n", accountIndex)
|
||||
|
||||
// Step 2: Create TxClient and generate auth token
|
||||
fmt.Println("2. Creating TxClient and generating auth token...")
|
||||
httpClient := lighterHTTP.NewClient(baseURL)
|
||||
txClient, err := lighterClient.NewTxClient(httpClient, apiKeyPrivateKey, accountIndex, 0, chainID)
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
authToken, err := txClient.GetAuthToken(time.Now().Add(1 * time.Hour))
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" OK: auth token generated\n\n")
|
||||
|
||||
// Step 3: Test GetActiveOrders with auth query parameter (NEW method)
|
||||
fmt.Println("3. Testing GetActiveOrders with auth query parameter (FIXED)...")
|
||||
encodedAuth := url.QueryEscape(authToken)
|
||||
endpoint := fmt.Sprintf("%s/api/v1/accountActiveOrders?account_index=%d&market_id=0&auth=%s",
|
||||
baseURL, accountIndex, encodedAuth)
|
||||
|
||||
resp, err := client.Get(endpoint)
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(body, &result)
|
||||
|
||||
if code, ok := result["code"].(float64); ok && code == 200 {
|
||||
orders := result["orders"].([]interface{})
|
||||
fmt.Printf(" OK: Retrieved %d orders\n", len(orders))
|
||||
if len(orders) > 0 {
|
||||
fmt.Println(" Sample orders:")
|
||||
for i, o := range orders {
|
||||
if i >= 3 {
|
||||
fmt.Printf(" ... and %d more\n", len(orders)-3)
|
||||
break
|
||||
}
|
||||
order := o.(map[string]interface{})
|
||||
fmt.Printf(" - ID: %v, Price: %v, Side: %v\n",
|
||||
order["order_id"], order["price"], order["is_ask"])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" FAILED: %s\n", string(body))
|
||||
fmt.Println("\n Possible causes:")
|
||||
fmt.Println(" - API key not registered on-chain")
|
||||
fmt.Println(" - API key private key incorrect")
|
||||
fmt.Println(" - Account index mismatch")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Step 4: Test GetActiveOrders with Authorization header (OLD method - for comparison)
|
||||
fmt.Println("\n4. Testing GetActiveOrders with Authorization header (OLD method)...")
|
||||
endpoint2 := fmt.Sprintf("%s/api/v1/accountActiveOrders?account_index=%d&market_id=0",
|
||||
baseURL, accountIndex)
|
||||
|
||||
req, _ := http.NewRequest("GET", endpoint2, nil)
|
||||
req.Header.Set("Authorization", authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp2, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED: %v\n", err)
|
||||
} else {
|
||||
defer resp2.Body.Close()
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
var result2 map[string]interface{}
|
||||
json.Unmarshal(body2, &result2)
|
||||
|
||||
if code, ok := result2["code"].(float64); ok && code == 200 {
|
||||
orders := result2["orders"].([]interface{})
|
||||
fmt.Printf(" OK: Retrieved %d orders (both methods work!)\n", len(orders))
|
||||
} else {
|
||||
fmt.Printf(" FAILED: %s\n", string(body2))
|
||||
fmt.Println(" ^ This is expected - Authorization header doesn't work consistently")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n=== TEST COMPLETE ===")
|
||||
fmt.Println("If test 3 passed, the fix is working correctly.")
|
||||
}
|
||||
|
||||
func getAccountIndex(client *http.Client, baseURL, walletAddr string) (int64, error) {
|
||||
endpoint := fmt.Sprintf("%s/api/v1/account?by=l1_address&value=%s", baseURL, walletAddr)
|
||||
resp, err := client.Get(endpoint)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Accounts []struct {
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
} `json:"accounts"`
|
||||
SubAccounts []struct {
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
} `json:"sub_accounts"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return 0, fmt.Errorf("failed to parse: %w", err)
|
||||
}
|
||||
|
||||
if len(result.Accounts) > 0 {
|
||||
return result.Accounts[0].AccountIndex, nil
|
||||
}
|
||||
if len(result.SubAccounts) > 0 {
|
||||
return result.SubAccounts[0].AccountIndex, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no account found")
|
||||
}
|
||||
Reference in New Issue
Block a user