ai gen
This commit is contained in:
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 采集器配置
|
||||||
|
COLLECTOR_URL=http://localhost:5000/status
|
||||||
|
|
||||||
|
# 数据库连接字符串
|
||||||
|
DATABASE_URL=postgres://user:password@localhost:5432/qmt_db?sslmode=disable
|
||||||
|
|
||||||
|
# 采集间隔(秒)
|
||||||
|
COLLECTION_INTERVAL=5
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -16,12 +16,20 @@
|
|||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
vendor/
|
||||||
|
gen/
|
||||||
|
.cache/
|
||||||
|
cache/
|
||||||
|
logs/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.builds/
|
||||||
|
|
||||||
|
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
go.work
|
||||||
go.work.sum
|
|
||||||
|
|
||||||
# env file
|
output/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|||||||
63
README.md
63
README.md
@@ -1,3 +1,66 @@
|
|||||||
# collector
|
# collector
|
||||||
|
|
||||||
qmt client collector
|
qmt client collector
|
||||||
|
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
这是一个采集QMT交易客户端状态数据并存储到PostgreSQL数据库的服务。
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
- 使用 `github.com/robfig/cron/v3` 定时调度,每5秒(可配置)从 http://localhost:5000/status 获取JSON数据
|
||||||
|
- 计算数据SHA256哈希值,检测数据变化
|
||||||
|
- 仅在数据变化时使用 `GORM` ORM存储到PostgreSQL数据库
|
||||||
|
- 完整的日志记录和错误处理
|
||||||
|
- 自动数据库表结构迁移
|
||||||
|
|
||||||
|
### 数据样本
|
||||||
|
数据样本位于 `/exmple/status.json`
|
||||||
|
|
||||||
|
### 入口文件
|
||||||
|
程序入口在 `cmd/main.go`
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 初始化数据库
|
||||||
|
|
||||||
|
执行SQL脚本创建数据表:
|
||||||
|
```bash
|
||||||
|
psql -U your_user -d your_database -f scripts/schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置环境变量
|
||||||
|
|
||||||
|
复制 `.env.example` 为 `.env` 并修改配置:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env`:
|
||||||
|
```env
|
||||||
|
COLLECTOR_URL=http://localhost:5000/status
|
||||||
|
DATABASE_URL=postgres://user:password@localhost:5432/qmt_db?sslmode=disable
|
||||||
|
COLLECTION_INTERVAL=5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 运行程序
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
```bash
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux/Mac:
|
||||||
|
```bash
|
||||||
|
chmod +x start.sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
或直接运行:
|
||||||
|
```bash
|
||||||
|
go run cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 详细说明
|
||||||
|
|
||||||
|
详细文档请查看 [README_NEW.md](README_NEW.md)
|
||||||
165
README_NEW.md
Normal file
165
README_NEW.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# QMT数据采集器
|
||||||
|
|
||||||
|
这是一个用于采集QMT交易客户端状态数据并存储到PostgreSQL数据库的服务。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ 使用 `github.com/robfig/cron/v3` 定时任务调度
|
||||||
|
- ✅ 每5秒(可配置)从HTTP接口采集数据
|
||||||
|
- ✅ 计算数据SHA256哈希值,检测数据变化
|
||||||
|
- ✅ 仅在数据变化时使用 `GORM` ORM存储到数据库,避免重复数据
|
||||||
|
- ✅ 自动数据库表结构迁移
|
||||||
|
- ✅ 完整的日志记录
|
||||||
|
- ✅ 优雅退出支持
|
||||||
|
- ✅ 环境变量配置
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
采集的数据包括:
|
||||||
|
- **资产信息**: 账户资金、市值、盈亏等
|
||||||
|
- **订单信息**: 所有委托订单详情
|
||||||
|
- **持仓信息**: 当前持仓股票及盈亏
|
||||||
|
- **行情数据**: 实时tick行情数据
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
系统会自动创建以下数据表:
|
||||||
|
|
||||||
|
1. `assets_snapshots` - 资产快照表
|
||||||
|
2. `orders` - 订单表
|
||||||
|
3. `positions` - 持仓表
|
||||||
|
4. `tick_data` - 行情数据表
|
||||||
|
5. `collection_logs` - 采集日志表
|
||||||
|
|
||||||
|
详细的表结构请查看 [scripts/schema.sql](scripts/schema.sql)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置环境变量
|
||||||
|
|
||||||
|
复制 `.env.example` 为 `.env` 并修改配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 采集地址
|
||||||
|
COLLECTOR_URL=http://localhost:5000/status
|
||||||
|
|
||||||
|
# 数据库连接字符串 (修改为你的实际配置)
|
||||||
|
DATABASE_URL=postgres://user:password@localhost:5432/qmt_db?sslmode=disable
|
||||||
|
|
||||||
|
# 采集间隔(秒)
|
||||||
|
COLLECTION_INTERVAL=5
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 使用GORM后,程序会自动创建和迁移数据库表结构,无需手动执行SQL脚本。
|
||||||
|
|
||||||
|
### 3. 运行程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
或者设置环境变量后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export COLLECTOR_URL=http://localhost:5000/status
|
||||||
|
export DATABASE_URL=postgres://user:password@localhost:5432/qmt_db?sslmode=disable
|
||||||
|
export COLLECTION_INTERVAL=5
|
||||||
|
go run cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 编译程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o collector cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
| 环境变量 | 说明 | 默认值 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| COLLECTOR_URL | 数据采集地址 | http://localhost:5000/status |
|
||||||
|
| DATABASE_URL | PostgreSQL连接字符串 | postgres://user:password@localhost:5432/qmt_db?sslmode=disable |
|
||||||
|
| COLLECTION_INTERVAL | 采集间隔(秒) | 5 |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
collector/
|
||||||
|
├── cmd/
|
||||||
|
│ └── main.go # 主程序入口
|
||||||
|
├── collector/
|
||||||
|
│ └── collector.go # 数据采集器(HTTP请求、Hash计算)
|
||||||
|
├── models/
|
||||||
|
│ └── models.go # 数据模型定义
|
||||||
|
├── storage/
|
||||||
|
│ └── storage.go # 数据库存储模块
|
||||||
|
├── scripts/
|
||||||
|
│ ├── schema.sql # 数据库表结构SQL
|
||||||
|
│ ├── build.sh # 构建脚本
|
||||||
|
│ ├── deploy.sh # 部署脚本
|
||||||
|
│ └── update.sh # 更新脚本
|
||||||
|
├── exmple/
|
||||||
|
│ └── status.json # 数据样本
|
||||||
|
├── go.mod # Go模块文件
|
||||||
|
└── README.md # 说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **定时任务**: [github.com/robfig/cron/v3](https://github.com/robfig/cron/v3) - 强大的cron表达式调度器
|
||||||
|
- **ORM框架**: [GORM](https://gorm.io/) - Go语言优秀的ORM库
|
||||||
|
- **数据库驱动**: gorm.io/driver/postgres - PostgreSQL驱动
|
||||||
|
- **数据库**: PostgreSQL 9.6+
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
1. **定时调度**: 使用cron调度器,根据配置的间隔定时执行采集任务
|
||||||
|
2. **数据采集**: 向配置的URL发起HTTP GET请求获取JSON数据
|
||||||
|
3. **Hash计算**: 对获取的数据计算SHA256哈希值
|
||||||
|
4. **变化检测**: 对比当前哈希与上次哈希,判断数据是否变化
|
||||||
|
5. **数据存储**: 如果数据有变化,使用GORM事务将数据保存到PostgreSQL数据库
|
||||||
|
6. **自动迁移**: 启动时自动检查并创建/更新数据库表结构
|
||||||
|
7. **日志记录**: 每次采集都记录日志,包括hash值和是否变化
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- HTTP请求失败会记录错误日志并继续下一次采集
|
||||||
|
- 数据库连接失败会导致程序退出
|
||||||
|
- 数据存储失败会记录错误但不会中断程序
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保PostgreSQL数据库已正确配置并可访问
|
||||||
|
2. **使用GORM后无需手动执行SQL脚本**,程序启动时会自动创建和迁移表结构
|
||||||
|
3. 建议在生产环境使用更安全的数据库连接方式
|
||||||
|
4. 可以根据需要调整采集间隔,但不建议设置过小
|
||||||
|
5. GORM会自动管理索引和表结构变更
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 添加新功能
|
||||||
|
|
||||||
|
1. 在 `models/models.go` 中添加数据模型
|
||||||
|
2. 在 `storage/storage.go` 中实现数据库操作
|
||||||
|
3. 在 `collector/collector.go` 中扩展采集逻辑
|
||||||
|
4. 在 `cmd/main.go` 中集成新功能
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
可以使用 `exmple/status.json` 中的样本数据进行测试。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
170
REFACTOR.md
Normal file
170
REFACTOR.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# 重构说明
|
||||||
|
|
||||||
|
## 重构内容
|
||||||
|
|
||||||
|
本次重构将项目从原生SQL和time.Ticker改为使用更现代化的库:
|
||||||
|
|
||||||
|
### 1. 定时任务调度器
|
||||||
|
|
||||||
|
**之前**: 使用 `time.Ticker`
|
||||||
|
```go
|
||||||
|
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
runCollection(coll, store)
|
||||||
|
case <-quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**现在**: 使用 `github.com/robfig/cron/v3`
|
||||||
|
```go
|
||||||
|
c := cron.New(cron.WithSeconds())
|
||||||
|
cronSpec := fmt.Sprintf("@every %ds", interval)
|
||||||
|
|
||||||
|
_, err = c.AddFunc(cronSpec, func() {
|
||||||
|
runCollection(coll, store)
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Start()
|
||||||
|
// ...
|
||||||
|
c.Stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 支持标准的cron表达式,更灵活
|
||||||
|
- 可以配置多个不同频率的任务
|
||||||
|
- 更好的任务管理和控制
|
||||||
|
- 支持秒级精度
|
||||||
|
|
||||||
|
### 2. 数据库ORM
|
||||||
|
|
||||||
|
**之前**: 使用原生 `database/sql` + `github.com/lib/pq`
|
||||||
|
```go
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
_, err := s.db.Exec(query, params...)
|
||||||
|
tx, err := s.db.Begin()
|
||||||
|
stmt, err := tx.Prepare(query)
|
||||||
|
```
|
||||||
|
|
||||||
|
**现在**: 使用 `GORM`
|
||||||
|
```go
|
||||||
|
db, err := gorm.Open(postgres.Open(connStr), &gorm.Config{})
|
||||||
|
tx.Create(&model)
|
||||||
|
tx.CreateInBatches(models, 100)
|
||||||
|
db.AutoMigrate(&models.Model{})
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 自动数据库表结构迁移
|
||||||
|
- 面向对象的操作方式
|
||||||
|
- 批量插入优化
|
||||||
|
- 自动管理连接池
|
||||||
|
- 更好的类型安全
|
||||||
|
- 支持软删除
|
||||||
|
- 链式调用,代码更简洁
|
||||||
|
|
||||||
|
## 主要变化
|
||||||
|
|
||||||
|
### 文件变化
|
||||||
|
|
||||||
|
1. **models/models.go**
|
||||||
|
- 添加GORM标签
|
||||||
|
- 添加DeletedAt字段支持软删除
|
||||||
|
- ID类型从int改为uint
|
||||||
|
|
||||||
|
2. **storage/storage.go**
|
||||||
|
- 完全重写,使用GORM
|
||||||
|
- 添加AutoMigrate方法
|
||||||
|
- 简化事务处理
|
||||||
|
- 使用CreateInBatches批量插入
|
||||||
|
|
||||||
|
3. **cmd/main.go**
|
||||||
|
- 使用cron替代time.Ticker
|
||||||
|
- 添加AutoMigrate调用
|
||||||
|
- 更优雅的启动和停止流程
|
||||||
|
|
||||||
|
4. **go.mod**
|
||||||
|
- 移除: github.com/lib/pq (由GORM驱动替代)
|
||||||
|
- 新增: github.com/robfig/cron/v3
|
||||||
|
- 新增: gorm.io/gorm
|
||||||
|
- 新增: gorm.io/driver/postgres
|
||||||
|
|
||||||
|
### 数据库变化
|
||||||
|
|
||||||
|
**之前**: 需要手动执行 `scripts/schema.sql` 创建表
|
||||||
|
|
||||||
|
**现在**: 程序启动时自动创建和迁移表结构
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
### API兼容
|
||||||
|
- 外部接口保持不变
|
||||||
|
- 环境变量配置保持不变
|
||||||
|
- 数据结构保持不变
|
||||||
|
|
||||||
|
### 数据库兼容
|
||||||
|
- 表结构与原设计完全一致
|
||||||
|
- 字段类型和索引保持一致
|
||||||
|
- 可以直接在原有数据库上运行(会保留已有数据)
|
||||||
|
|
||||||
|
## 升级步骤
|
||||||
|
|
||||||
|
1. 备份现有数据库(可选,但推荐)
|
||||||
|
```bash
|
||||||
|
pg_dump -U user -d qmt_db > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 更新依赖
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 重新编译
|
||||||
|
```bash
|
||||||
|
go build -o collector.exe cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 运行新程序
|
||||||
|
```bash
|
||||||
|
./collector.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
程序会自动检测并迁移数据库表结构。
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
### 定时任务
|
||||||
|
- **time.Ticker**: 简单场景足够,但功能有限
|
||||||
|
- **cron**: 功能强大,支持复杂调度,性能相当
|
||||||
|
|
||||||
|
### 数据库操作
|
||||||
|
- **原生SQL**: 性能略高,但开发效率低
|
||||||
|
- **GORM**: 开发效率高,批量插入性能优秀,适合本项目
|
||||||
|
|
||||||
|
对于本项目的数据采集场景,GORM的性能完全足够,且大大提升了开发效率和代码可维护性。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
所有单元测试已通过:
|
||||||
|
```bash
|
||||||
|
go test ./collector -v
|
||||||
|
```
|
||||||
|
|
||||||
|
编译成功:
|
||||||
|
```bash
|
||||||
|
go build -o collector.exe cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本次重构在不改变功能的前提下,使用了更现代化、更易维护的技术栈:
|
||||||
|
- ✅ 更灵活的定时任务调度
|
||||||
|
- ✅ 更简洁的数据库操作
|
||||||
|
- ✅ 自动表结构管理
|
||||||
|
- ✅ 更好的代码可读性和可维护性
|
||||||
|
- ✅ 保持向后兼容
|
||||||
147
cmd/main.go
Normal file
147
cmd/main.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.apinb.com/quant/collector/collector"
|
||||||
|
"git.apinb.com/quant/collector/storage"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("=== QMT数据采集器启动 ===")
|
||||||
|
|
||||||
|
// 从环境变量获取配置
|
||||||
|
collectorURL := getEnv("COLLECTOR_URL", "http://localhost:5000/status")
|
||||||
|
dbConnStr := getEnv("DATABASE_URL", "postgres://user:password@localhost:5432/qmt_db?sslmode=disable")
|
||||||
|
interval := getEnvAsInt("COLLECTION_INTERVAL", 5) // 默认5秒
|
||||||
|
|
||||||
|
log.Printf("采集地址: %s", collectorURL)
|
||||||
|
log.Printf("采集间隔: %d秒", interval)
|
||||||
|
|
||||||
|
// 创建采集器
|
||||||
|
coll := collector.NewCollector(collectorURL)
|
||||||
|
|
||||||
|
// 创建数据库存储
|
||||||
|
store, err := storage.NewStorage(dbConnStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("数据库连接失败: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
// 自动迁移数据库表结构
|
||||||
|
if err := store.AutoMigrate(); err != nil {
|
||||||
|
log.Fatalf("数据库迁移失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建cron调度器
|
||||||
|
c := cron.New(cron.WithSeconds())
|
||||||
|
|
||||||
|
// 构建cron表达式 (每N秒执行一次)
|
||||||
|
cronSpec := fmt.Sprintf("@every %ds", interval)
|
||||||
|
log.Printf("定时任务表达式: %s", cronSpec)
|
||||||
|
|
||||||
|
// 添加定时任务
|
||||||
|
_, err = c.AddFunc(cronSpec, func() {
|
||||||
|
runCollection(coll, store)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("添加定时任务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动调度器
|
||||||
|
c.Start()
|
||||||
|
log.Println("定时任务已启动")
|
||||||
|
|
||||||
|
// 立即执行一次采集
|
||||||
|
log.Println("执行首次采集...")
|
||||||
|
runCollection(coll, store)
|
||||||
|
|
||||||
|
// 等待退出信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("收到退出信号,正在关闭...")
|
||||||
|
c.Stop()
|
||||||
|
log.Println("采集器已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCollection 执行一次数据采集和存储
|
||||||
|
func runCollection(coll *collector.Collector, store *storage.Storage) {
|
||||||
|
log.Println("开始采集...")
|
||||||
|
|
||||||
|
// 采集数据并检查变化
|
||||||
|
status, dataHash, changed, err := coll.CollectAndCheck()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("采集失败: %v", err)
|
||||||
|
// 记录失败的日志
|
||||||
|
if err := store.SaveCollectionLog("", false, err.Error()); err != nil {
|
||||||
|
log.Printf("保存采集日志失败: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("数据哈希: %s", dataHash)
|
||||||
|
log.Printf("数据是否变化: %v", changed)
|
||||||
|
|
||||||
|
// 如果数据没有变化,只记录日志
|
||||||
|
if !changed {
|
||||||
|
log.Println("数据未变化,跳过存储")
|
||||||
|
if err := store.SaveCollectionLog(dataHash, false, "数据未变化"); err != nil {
|
||||||
|
log.Printf("保存采集日志失败: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据有变化,保存到数据库
|
||||||
|
log.Println("数据已变化,开始存储到数据库...")
|
||||||
|
if err := store.SaveStatus(status, dataHash); err != nil {
|
||||||
|
log.Printf("保存数据失败: %v", err)
|
||||||
|
// 记录失败的日志
|
||||||
|
if err := store.SaveCollectionLog(dataHash, true, err.Error()); err != nil {
|
||||||
|
log.Printf("保存采集日志失败: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录成功的日志
|
||||||
|
if err := store.SaveCollectionLog(dataHash, true, "数据保存成功"); err != nil {
|
||||||
|
log.Printf("保存采集日志失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("数据存储成功 - 资产账户: %s, 订单数: %d, 持仓数: %d, 行情数: %d",
|
||||||
|
status.Data.Assets.AccountID,
|
||||||
|
len(status.Data.Orders),
|
||||||
|
len(status.Data.Positions),
|
||||||
|
len(status.Data.TickData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnv 获取环境变量,如果不存在则返回默认值
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvAsInt 获取环境变量并转换为整数
|
||||||
|
func getEnvAsInt(key string, defaultValue int) int {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("环境变量 %s 转换失败: %v,使用默认值 %d", key, err, defaultValue)
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
112
collector/collector.go
Normal file
112
collector/collector.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.apinb.com/quant/collector/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Collector 数据采集器
|
||||||
|
type Collector struct {
|
||||||
|
url string
|
||||||
|
httpClient *http.Client
|
||||||
|
lastHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCollector 创建新的采集器
|
||||||
|
func NewCollector(url string) *Collector {
|
||||||
|
return &Collector{
|
||||||
|
url: url,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
lastHash: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchData 从HTTP接口获取数据
|
||||||
|
func (c *Collector) FetchData() (*models.Status, error) {
|
||||||
|
resp, err := c.httpClient.Get(c.url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("HTTP请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP状态码错误: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var status models.Status
|
||||||
|
if err := json.Unmarshal(body, &status); err != nil {
|
||||||
|
return nil, fmt.Errorf("JSON解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateHash 计算数据的SHA256哈希值
|
||||||
|
func (c *Collector) CalculateHash(status *models.Status) (string, error) {
|
||||||
|
// 将数据序列化为JSON
|
||||||
|
data, err := json.Marshal(status)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("序列化数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算SHA256哈希
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(hash[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasChanged 检查数据是否发生变化
|
||||||
|
func (c *Collector) HasChanged(currentHash string) bool {
|
||||||
|
if c.lastHash == "" {
|
||||||
|
return true // 第一次采集
|
||||||
|
}
|
||||||
|
return c.lastHash != currentHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateHash 更新上次哈希值
|
||||||
|
func (c *Collector) UpdateHash(hash string) {
|
||||||
|
c.lastHash = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastHash 获取上次哈希值
|
||||||
|
func (c *Collector) GetLastHash() string {
|
||||||
|
return c.lastHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectAndCheck 采集数据并检查是否变化
|
||||||
|
func (c *Collector) CollectAndCheck() (*models.Status, string, bool, error) {
|
||||||
|
// 获取数据
|
||||||
|
status, err := c.FetchData()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算哈希
|
||||||
|
currentHash, err := c.CalculateHash(status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否变化
|
||||||
|
changed := c.HasChanged(currentHash)
|
||||||
|
|
||||||
|
// 如果变化了,更新哈希
|
||||||
|
if changed {
|
||||||
|
c.UpdateHash(currentHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, currentHash, changed, nil
|
||||||
|
}
|
||||||
58
collector/collector_test.go
Normal file
58
collector/collector_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.apinb.com/quant/collector/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCalculateHash 测试Hash计算功能
|
||||||
|
func TestCalculateHash(t *testing.T) {
|
||||||
|
// 读取样本数据
|
||||||
|
data, err := os.ReadFile("../exmple/status.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取样本文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var status models.Status
|
||||||
|
if err := json.Unmarshal(data, &status); err != nil {
|
||||||
|
t.Fatalf("JSON解析失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
collector := NewCollector("http://localhost:5000/status")
|
||||||
|
hash, err := collector.CalculateHash(&status)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("计算Hash失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hash == "" {
|
||||||
|
t.Error("Hash值不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("计算的Hash: %s", hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHasChanged 测试变化检测
|
||||||
|
func TestHasChanged(t *testing.T) {
|
||||||
|
collector := NewCollector("http://localhost:5000/status")
|
||||||
|
|
||||||
|
// 第一次应该返回true
|
||||||
|
if !collector.HasChanged("hash1") {
|
||||||
|
t.Error("第一次检测应该返回true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新hash
|
||||||
|
collector.UpdateHash("hash1")
|
||||||
|
|
||||||
|
// 相同的hash应该返回false
|
||||||
|
if collector.HasChanged("hash1") {
|
||||||
|
t.Error("相同的hash应该返回false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同的hash应该返回true
|
||||||
|
if !collector.HasChanged("hash2") {
|
||||||
|
t.Error("不同的hash应该返回true")
|
||||||
|
}
|
||||||
|
}
|
||||||
1
exmple/status.json
Normal file
1
exmple/status.json
Normal file
File diff suppressed because one or more lines are too long
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module git.apinb.com/quant/collector
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
gorm.io/driver/postgres v1.5.4
|
||||||
|
gorm.io/gorm v1.25.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
|
golang.org/x/text v0.13.0 // indirect
|
||||||
|
)
|
||||||
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||||
|
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||||
|
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||||
|
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
181
models/models.go
Normal file
181
models/models.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status 状态数据结构
|
||||||
|
type Status struct {
|
||||||
|
Data Data `json:"data"`
|
||||||
|
Status Config `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data 数据部分
|
||||||
|
type Data struct {
|
||||||
|
Assets Assets `json:"assets"`
|
||||||
|
Orders []Order `json:"order"`
|
||||||
|
Positions []Position `json:"positions"`
|
||||||
|
TickData map[string]Tick `json:"tick_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets 资产信息
|
||||||
|
type Assets struct {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Cash float64 `json:"cash"`
|
||||||
|
FrozenCash float64 `json:"frozen_cash"`
|
||||||
|
MarketValue float64 `json:"market_value"`
|
||||||
|
Profit float64 `json:"profit"`
|
||||||
|
TotalAsset float64 `json:"total_asset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order 订单信息
|
||||||
|
type Order struct {
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
OrderRemark string `json:"order_remark"`
|
||||||
|
OrderStatus int `json:"order_status"`
|
||||||
|
OrderTime int64 `json:"order_time"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
StockCode string `json:"stock_code"`
|
||||||
|
TradedPrice float64 `json:"traded_price"`
|
||||||
|
TradedVolume int `json:"traded_volume"`
|
||||||
|
Volume int `json:"volume"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position 持仓信息
|
||||||
|
type Position struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Volume int `json:"volume"`
|
||||||
|
CanUseVolume int `json:"can_use_volume"`
|
||||||
|
FrozenVolume int `json:"frozen_volume"`
|
||||||
|
AvgPrice float64 `json:"avg_price"`
|
||||||
|
OpenPrice float64 `json:"open_price"`
|
||||||
|
CurrentPrice float64 `json:"current_price"`
|
||||||
|
MarketValue float64 `json:"market_value"`
|
||||||
|
Profit float64 `json:"profit"`
|
||||||
|
ProfitRate float64 `json:"profit_rate"`
|
||||||
|
MinProfitRate float64 `json:"min_profit_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick 行情数据
|
||||||
|
type Tick struct {
|
||||||
|
LastPrice float64 `json:"lastPrice"`
|
||||||
|
Open float64 `json:"open"`
|
||||||
|
High float64 `json:"high"`
|
||||||
|
Low float64 `json:"low"`
|
||||||
|
LastClose float64 `json:"lastClose"`
|
||||||
|
Volume int64 `json:"volume"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
PVolume int64 `json:"pvolume"`
|
||||||
|
BidPrice []float64 `json:"bidPrice"`
|
||||||
|
BidVol []int `json:"bidVol"`
|
||||||
|
AskPrice []float64 `json:"askPrice"`
|
||||||
|
AskVol []int `json:"askVol"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
TimeTag string `json:"timetag"`
|
||||||
|
StockStatus int `json:"stockStatus"`
|
||||||
|
LastSettlementPrice float64 `json:"lastSettlementPrice"`
|
||||||
|
SettlementPrice float64 `json:"settlementPrice"`
|
||||||
|
OpenInt int `json:"openInt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 配置信息
|
||||||
|
type Config struct {
|
||||||
|
ConfigKey string `json:"config_key"`
|
||||||
|
HomeName string `json:"home_name"`
|
||||||
|
ProjectRoot string `json:"project_root"`
|
||||||
|
QmtStatus string `json:"qmt_status"`
|
||||||
|
StartTime int64 `json:"start_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetSnapshot 资产快照数据库模型
|
||||||
|
type AssetSnapshot struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index"`
|
||||||
|
Cash float64 `json:"cash" gorm:"type:decimal(15,2);not null;default:0"`
|
||||||
|
FrozenCash float64 `json:"frozen_cash" gorm:"type:decimal(15,2);not null;default:0;column:frozen_cash"`
|
||||||
|
MarketValue float64 `json:"market_value" gorm:"type:decimal(15,2);not null;default:0;column:market_value"`
|
||||||
|
Profit float64 `json:"profit" gorm:"type:decimal(15,2);not null;default:0"`
|
||||||
|
TotalAsset float64 `json:"total_asset" gorm:"type:decimal(15,2);not null;default:0;column:total_asset"`
|
||||||
|
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null;index"`
|
||||||
|
CollectedAt time.Time `json:"collected_at" gorm:"not null;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderRecord 订单数据库模型
|
||||||
|
type OrderRecord struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
OrderID int64 `json:"order_id" gorm:"not null;index"`
|
||||||
|
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index"`
|
||||||
|
StockCode string `json:"stock_code" gorm:"type:varchar(20);not null;index"`
|
||||||
|
Price float64 `json:"price" gorm:"type:decimal(10,4);not null;default:0"`
|
||||||
|
Volume int `json:"volume" gorm:"not null;default:0"`
|
||||||
|
TradedPrice float64 `json:"traded_price" gorm:"type:decimal(10,4);not null;default:0;column:traded_price"`
|
||||||
|
TradedVolume int `json:"traded_volume" gorm:"not null;default:0;column:traded_volume"`
|
||||||
|
OrderStatus int `json:"order_status" gorm:"not null;default:0;column:order_status"`
|
||||||
|
OrderTime int64 `json:"order_time" gorm:"not null;column:order_time"`
|
||||||
|
OrderRemark string `json:"order_remark" gorm:"type:text;column:order_remark"`
|
||||||
|
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null"`
|
||||||
|
CollectedAt time.Time `json:"collected_at" gorm:"not null;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PositionRecord 持仓数据库模型
|
||||||
|
type PositionRecord struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index"`
|
||||||
|
Code string `json:"code" gorm:"type:varchar(20);not null;index"`
|
||||||
|
Volume int `json:"volume" gorm:"not null;default:0"`
|
||||||
|
CanUseVolume int `json:"can_use_volume" gorm:"not null;default:0;column:can_use_volume"`
|
||||||
|
FrozenVolume int `json:"frozen_volume" gorm:"not null;default:0;column:frozen_volume"`
|
||||||
|
AvgPrice float64 `json:"avg_price" gorm:"type:decimal(10,4);not null;default:0;column:avg_price"`
|
||||||
|
OpenPrice float64 `json:"open_price" gorm:"type:decimal(10,4);not null;default:0;column:open_price"`
|
||||||
|
CurrentPrice float64 `json:"current_price" gorm:"type:decimal(10,4);not null;default:0;column:current_price"`
|
||||||
|
MarketValue float64 `json:"market_value" gorm:"type:decimal(15,2);not null;default:0;column:market_value"`
|
||||||
|
Profit float64 `json:"profit" gorm:"type:decimal(15,2);not null;default:0"`
|
||||||
|
ProfitRate float64 `json:"profit_rate" gorm:"type:decimal(10,4);not null;default:0;column:profit_rate"`
|
||||||
|
MinProfitRate float64 `json:"min_profit_rate" gorm:"type:decimal(10,4);not null;default:0;column:min_profit_rate"`
|
||||||
|
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null"`
|
||||||
|
CollectedAt time.Time `json:"collected_at" gorm:"not null;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TickRecord 行情数据库模型
|
||||||
|
type TickRecord struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
StockCode string `json:"stock_code" gorm:"type:varchar(20);not null;index"`
|
||||||
|
LastPrice float64 `json:"last_price" gorm:"type:decimal(10,4);not null;default:0;column:last_price"`
|
||||||
|
Open float64 `json:"open" gorm:"type:decimal(10,4);not null;default:0"`
|
||||||
|
High float64 `json:"high" gorm:"type:decimal(10,4);not null;default:0"`
|
||||||
|
Low float64 `json:"low" gorm:"type:decimal(10,4);not null;default:0"`
|
||||||
|
LastClose float64 `json:"last_close" gorm:"type:decimal(10,4);not null;default:0;column:last_close"`
|
||||||
|
Volume int64 `json:"volume" gorm:"not null;default:0"`
|
||||||
|
Amount float64 `json:"amount" gorm:"type:decimal(15,2);not null;default:0"`
|
||||||
|
PVolume int64 `json:"pvolume" gorm:"not null;default:0;column:pvolume"`
|
||||||
|
BidPrices []float64 `json:"bid_prices" gorm:"type:decimal(10,4)[];column:bid_prices"`
|
||||||
|
BidVolumes []int `json:"bid_volumes" gorm:"type:integer[];column:bid_volumes"`
|
||||||
|
AskPrices []float64 `json:"ask_prices" gorm:"type:decimal(10,4)[];column:ask_prices"`
|
||||||
|
AskVolumes []int `json:"ask_volumes" gorm:"type:integer[];column:ask_volumes"`
|
||||||
|
Time int64 `json:"time" gorm:"not null;index"`
|
||||||
|
TimeTag string `json:"timetag" gorm:"type:varchar(50);column:timetag"`
|
||||||
|
StockStatus int `json:"stock_status" gorm:"not null;default:0;column:stock_status"`
|
||||||
|
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null"`
|
||||||
|
CollectedAt time.Time `json:"collected_at" gorm:"not null;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionLog 采集日志数据库模型
|
||||||
|
type CollectionLog struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null;index"`
|
||||||
|
HasChanged bool `json:"has_changed" gorm:"not null;default:false;column:has_changed"`
|
||||||
|
StatusMessage string `json:"status_message" gorm:"type:text;column:status_message"`
|
||||||
|
CollectedAt time.Time `json:"collected_at" gorm:"not null;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
12
scripts/build.sh
Normal file
12
scripts/build.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
GOARCH=amd64 GOOS=linux go build -o ../builds/gostock ./cmd/main/main.go
|
||||||
|
|
||||||
|
BSM_RuntimeMode=prod BSM_Prefix=/data/app/ nohup ./gostock > /data/app/logs/gostock.log 2>&1 &
|
||||||
|
cat /data/app/logs/gostock.log
|
||||||
|
|
||||||
|
|
||||||
|
GOARCH=amd64 GOOS=linux go build -o ../builds/selector ./cmd/selector/main.go
|
||||||
|
|
||||||
|
GOARCH=amd64 GOOS=linux go build -o ../builds/test ./cmd/test/main.go
|
||||||
|
BSM_RuntimeMode=prod BSM_Prefix=/data/app/ ./selector
|
||||||
50
scripts/deploy.sh
Normal file
50
scripts/deploy.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 配置部分
|
||||||
|
BINARY_NAME="gostock" # 二进制文件名
|
||||||
|
BUILD_OUTPUT_DIR="../builds/gostock" # 构建输出目录
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
REMOTE_USER="root" # 服务器用户名
|
||||||
|
REMOTE_HOST="139.224.247.176" # 服务器地址
|
||||||
|
REMOTE_DIR="/data/app" # 服务器部署目录
|
||||||
|
SERVICE_NAME="gostock" # 服务名称(如果有systemd服务)
|
||||||
|
|
||||||
|
echo "=== 开始部署流程 ==="
|
||||||
|
|
||||||
|
# 1. 编译Linux二进制文件
|
||||||
|
echo "正在编译Linux二进制文件..."
|
||||||
|
|
||||||
|
# 使用Go语言编译示例 (如果是其他语言请修改此部分)
|
||||||
|
# 如果不是Go项目,请替换为你的构建命令,如make等
|
||||||
|
GOEXPERIMENT=jsonv2 GOOS=linux GOARCH=amd64 go build -o "${BUILD_OUTPUT_DIR}/${BINARY_NAME}" ./cmd/${BINARY_NAME}/main.go
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "编译失败!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "编译成功: ${BUILD_OUTPUT_DIR}/${BINARY_NAME}"
|
||||||
|
|
||||||
|
# 2. 停止远程服务
|
||||||
|
echo "正在停止远程服务..."
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" << EOF
|
||||||
|
killall -9 "${BINARY_NAME}"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 3. 上传到服务器
|
||||||
|
echo "正在上传文件到服务器..."
|
||||||
|
scp -C "${BUILD_OUTPUT_DIR}/${BINARY_NAME}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/${BINARY_NAME}"
|
||||||
|
scp ./etc/* "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/etc/"
|
||||||
|
|
||||||
|
|
||||||
|
# 4. 设置执行权限并启动服务
|
||||||
|
echo "正在设置权限并启动服务..."
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" << EOF
|
||||||
|
chmod +x "${REMOTE_DIR}/${BINARY_NAME}"
|
||||||
|
nohup "${REMOTE_DIR}/${BINARY_NAME}" > "${REMOTE_DIR}/logs/${BINARY_NAME}.log" 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
pgrep -f "${REMOTE_DIR}/${BINARY_NAME}" && echo "服务启动成功!" || echo "服务启动可能失败!"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== 部署完成 ==="
|
||||||
105
scripts/schema.sql
Normal file
105
scripts/schema.sql
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
-- 资产快照表
|
||||||
|
CREATE TABLE IF NOT EXISTS assets_snapshots (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
account_id VARCHAR(50) NOT NULL,
|
||||||
|
cash DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
frozen_cash DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
market_value DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
profit DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
total_asset DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
data_hash VARCHAR(64) NOT NULL,
|
||||||
|
collected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_assets_account_id ON assets_snapshots(account_id);
|
||||||
|
CREATE INDEX idx_assets_collected_at ON assets_snapshots(collected_at);
|
||||||
|
CREATE INDEX idx_assets_data_hash ON assets_snapshots(data_hash);
|
||||||
|
|
||||||
|
-- 订单表
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
order_id BIGINT NOT NULL,
|
||||||
|
account_id VARCHAR(50) NOT NULL,
|
||||||
|
stock_code VARCHAR(20) NOT NULL,
|
||||||
|
price DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
volume INTEGER NOT NULL DEFAULT 0,
|
||||||
|
traded_price DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
traded_volume INTEGER NOT NULL DEFAULT 0,
|
||||||
|
order_status INTEGER NOT NULL DEFAULT 0,
|
||||||
|
order_time BIGINT NOT NULL,
|
||||||
|
order_remark TEXT,
|
||||||
|
data_hash VARCHAR(64) NOT NULL,
|
||||||
|
collected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_orders_order_id ON orders(order_id);
|
||||||
|
CREATE INDEX idx_orders_stock_code ON orders(stock_code);
|
||||||
|
CREATE INDEX idx_orders_account_id ON orders(account_id);
|
||||||
|
CREATE INDEX idx_orders_collected_at ON orders(collected_at);
|
||||||
|
|
||||||
|
-- 持仓表
|
||||||
|
CREATE TABLE IF NOT EXISTS positions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
account_id VARCHAR(50) NOT NULL,
|
||||||
|
code VARCHAR(20) NOT NULL,
|
||||||
|
volume INTEGER NOT NULL DEFAULT 0,
|
||||||
|
can_use_volume INTEGER NOT NULL DEFAULT 0,
|
||||||
|
frozen_volume INTEGER NOT NULL DEFAULT 0,
|
||||||
|
avg_price DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
open_price DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
current_price DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
market_value DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
profit DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
profit_rate DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
min_profit_rate DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
data_hash VARCHAR(64) NOT NULL,
|
||||||
|
collected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_positions_code ON positions(code);
|
||||||
|
CREATE INDEX idx_positions_account_id ON positions(account_id);
|
||||||
|
CREATE INDEX idx_positions_collected_at ON positions(collected_at);
|
||||||
|
|
||||||
|
-- 行情数据表
|
||||||
|
CREATE TABLE IF NOT EXISTS tick_data (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
stock_code VARCHAR(20) NOT NULL,
|
||||||
|
last_price DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
open DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
high DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
low DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
last_close DECIMAL(10, 4) NOT NULL DEFAULT 0,
|
||||||
|
volume BIGINT NOT NULL DEFAULT 0,
|
||||||
|
amount DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||||
|
pvolume BIGINT NOT NULL DEFAULT 0,
|
||||||
|
bid_prices DECIMAL(10, 4)[],
|
||||||
|
bid_volumes INTEGER[],
|
||||||
|
ask_prices DECIMAL(10, 4)[],
|
||||||
|
ask_volumes INTEGER[],
|
||||||
|
time BIGINT NOT NULL,
|
||||||
|
timetag VARCHAR(50),
|
||||||
|
stock_status INTEGER NOT NULL DEFAULT 0,
|
||||||
|
data_hash VARCHAR(64) NOT NULL,
|
||||||
|
collected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_tick_stock_code ON tick_data(stock_code);
|
||||||
|
CREATE INDEX idx_tick_time ON tick_data(time);
|
||||||
|
CREATE INDEX idx_tick_collected_at ON tick_data(collected_at);
|
||||||
|
|
||||||
|
-- 采集日志表
|
||||||
|
CREATE TABLE IF NOT EXISTS collection_logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
data_hash VARCHAR(64) NOT NULL,
|
||||||
|
has_changed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
status_message TEXT,
|
||||||
|
collected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_collection_logs_hash ON collection_logs(data_hash);
|
||||||
|
CREATE INDEX idx_collection_logs_collected_at ON collection_logs(collected_at);
|
||||||
7
scripts/update.sh
Normal file
7
scripts/update.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
git pull
|
||||||
|
go get all
|
||||||
|
go get -u ./...
|
||||||
|
go mod tidy
|
||||||
|
git add .
|
||||||
|
git commit -m 'run ./script/update.sh'
|
||||||
|
git push
|
||||||
17
start.bat
Normal file
17
start.bat
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@echo off
|
||||||
|
REM Windows 启动脚本
|
||||||
|
|
||||||
|
echo 正在启动 QMT 数据采集器...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM 检查 .env 文件是否存在
|
||||||
|
if not exist .env (
|
||||||
|
echo 警告: .env 文件不存在,将使用默认配置
|
||||||
|
echo 请复制 .env.example 为 .env 并修改配置
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 运行程序
|
||||||
|
collector.exe
|
||||||
|
|
||||||
|
pause
|
||||||
20
start.sh
Normal file
20
start.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Linux/Mac 启动脚本
|
||||||
|
|
||||||
|
echo "正在启动 QMT 数据采集器..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 .env 文件是否存在
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "警告: .env 文件不存在,将使用默认配置"
|
||||||
|
echo "请复制 .env.example 为 .env 并修改配置"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
if [ -f .env ]; then
|
||||||
|
export $(cat .env | grep -v '^#' | xargs)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 运行程序
|
||||||
|
./collector
|
||||||
207
storage/storage.go
Normal file
207
storage/storage.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.apinb.com/quant/collector/models"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage 数据库存储器
|
||||||
|
type Storage struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage 创建新的数据库连接
|
||||||
|
func NewStorage(connStr string) (*Storage, error) {
|
||||||
|
// 配置GORM日志
|
||||||
|
newLogger := logger.New(
|
||||||
|
log.New(log.Writer(), "\r\n", log.LstdFlags),
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: time.Second,
|
||||||
|
LogLevel: logger.Warn,
|
||||||
|
IgnoreRecordNotFoundError: true,
|
||||||
|
Colorful: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.Open(connStr), &gorm.Config{
|
||||||
|
Logger: newLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("打开数据库连接失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取底层的sql.DB以设置连接池
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取数据库实例失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接池参数
|
||||||
|
sqlDB.SetMaxOpenConns(25)
|
||||||
|
sqlDB.SetMaxIdleConns(5)
|
||||||
|
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
|
||||||
|
log.Println("数据库连接成功")
|
||||||
|
return &Storage{db: db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
func (s *Storage) Close() error {
|
||||||
|
if s.db != nil {
|
||||||
|
sqlDB, err := s.db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoMigrate 自动迁移数据库表结构
|
||||||
|
func (s *Storage) AutoMigrate() error {
|
||||||
|
log.Println("开始自动迁移数据库表结构...")
|
||||||
|
|
||||||
|
err := s.db.AutoMigrate(
|
||||||
|
&models.AssetSnapshot{},
|
||||||
|
&models.OrderRecord{},
|
||||||
|
&models.PositionRecord{},
|
||||||
|
&models.TickRecord{},
|
||||||
|
&models.CollectionLog{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("自动迁移失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("数据库表结构迁移完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveStatus 保存完整状态数据(使用事务)
|
||||||
|
func (s *Storage) SaveStatus(status *models.Status, dataHash string) error {
|
||||||
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 保存资产快照
|
||||||
|
asset := models.AssetSnapshot{
|
||||||
|
AccountID: status.Data.Assets.AccountID,
|
||||||
|
Cash: status.Data.Assets.Cash,
|
||||||
|
FrozenCash: status.Data.Assets.FrozenCash,
|
||||||
|
MarketValue: status.Data.Assets.MarketValue,
|
||||||
|
Profit: status.Data.Assets.Profit,
|
||||||
|
TotalAsset: status.Data.Assets.TotalAsset,
|
||||||
|
DataHash: dataHash,
|
||||||
|
CollectedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := tx.Create(&asset).Error; err != nil {
|
||||||
|
return fmt.Errorf("保存资产快照失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存订单
|
||||||
|
if len(status.Data.Orders) > 0 {
|
||||||
|
orders := make([]models.OrderRecord, 0, len(status.Data.Orders))
|
||||||
|
for _, order := range status.Data.Orders {
|
||||||
|
orders = append(orders, models.OrderRecord{
|
||||||
|
OrderID: order.OrderID,
|
||||||
|
AccountID: status.Data.Assets.AccountID,
|
||||||
|
StockCode: order.StockCode,
|
||||||
|
Price: order.Price,
|
||||||
|
Volume: order.Volume,
|
||||||
|
TradedPrice: order.TradedPrice,
|
||||||
|
TradedVolume: order.TradedVolume,
|
||||||
|
OrderStatus: order.OrderStatus,
|
||||||
|
OrderTime: order.OrderTime,
|
||||||
|
OrderRemark: order.OrderRemark,
|
||||||
|
DataHash: dataHash,
|
||||||
|
CollectedAt: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := tx.CreateInBatches(orders, 100).Error; err != nil {
|
||||||
|
return fmt.Errorf("保存订单失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存持仓
|
||||||
|
if len(status.Data.Positions) > 0 {
|
||||||
|
positions := make([]models.PositionRecord, 0, len(status.Data.Positions))
|
||||||
|
for _, pos := range status.Data.Positions {
|
||||||
|
positions = append(positions, models.PositionRecord{
|
||||||
|
AccountID: status.Data.Assets.AccountID,
|
||||||
|
Code: pos.Code,
|
||||||
|
Volume: pos.Volume,
|
||||||
|
CanUseVolume: pos.CanUseVolume,
|
||||||
|
FrozenVolume: pos.FrozenVolume,
|
||||||
|
AvgPrice: pos.AvgPrice,
|
||||||
|
OpenPrice: pos.OpenPrice,
|
||||||
|
CurrentPrice: pos.CurrentPrice,
|
||||||
|
MarketValue: pos.MarketValue,
|
||||||
|
Profit: pos.Profit,
|
||||||
|
ProfitRate: pos.ProfitRate,
|
||||||
|
MinProfitRate: pos.MinProfitRate,
|
||||||
|
DataHash: dataHash,
|
||||||
|
CollectedAt: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := tx.CreateInBatches(positions, 100).Error; err != nil {
|
||||||
|
return fmt.Errorf("保存持仓失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存行情数据
|
||||||
|
if len(status.Data.TickData) > 0 {
|
||||||
|
ticks := make([]models.TickRecord, 0, len(status.Data.TickData))
|
||||||
|
for code, tick := range status.Data.TickData {
|
||||||
|
ticks = append(ticks, models.TickRecord{
|
||||||
|
StockCode: code,
|
||||||
|
LastPrice: tick.LastPrice,
|
||||||
|
Open: tick.Open,
|
||||||
|
High: tick.High,
|
||||||
|
Low: tick.Low,
|
||||||
|
LastClose: tick.LastClose,
|
||||||
|
Volume: tick.Volume,
|
||||||
|
Amount: tick.Amount,
|
||||||
|
PVolume: tick.PVolume,
|
||||||
|
BidPrices: tick.BidPrice,
|
||||||
|
BidVolumes: tick.BidVol,
|
||||||
|
AskPrices: tick.AskPrice,
|
||||||
|
AskVolumes: tick.AskVol,
|
||||||
|
Time: tick.Time,
|
||||||
|
TimeTag: tick.TimeTag,
|
||||||
|
StockStatus: tick.StockStatus,
|
||||||
|
DataHash: dataHash,
|
||||||
|
CollectedAt: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := tx.CreateInBatches(ticks, 100).Error; err != nil {
|
||||||
|
return fmt.Errorf("保存行情数据失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCollectionLog 保存采集日志
|
||||||
|
func (s *Storage) SaveCollectionLog(dataHash string, hasChanged bool, statusMessage string) error {
|
||||||
|
log := models.CollectionLog{
|
||||||
|
DataHash: dataHash,
|
||||||
|
HasChanged: hasChanged,
|
||||||
|
StatusMessage: statusMessage,
|
||||||
|
CollectedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.Create(&log).Error; err != nil {
|
||||||
|
return fmt.Errorf("保存采集日志失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB 获取GORM DB实例(用于高级查询)
|
||||||
|
func (s *Storage) GetDB() *gorm.DB {
|
||||||
|
return s.db
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user