diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cf71b6a --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# 采集器配置 +COLLECTOR_URL=http://localhost:5000/status + +# 数据库连接字符串 +DATABASE_URL=postgres://user:password@localhost:5432/qmt_db?sslmode=disable + +# 采集间隔(秒) +COLLECTION_INTERVAL=5 diff --git a/.gitignore b/.gitignore index 5b90e79..628655f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,12 +16,20 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ +gen/ +.cache/ +cache/ +logs/ +.idea/ +.vscode/ +.builds/ + # Go workspace file go.work -go.work.sum -# env file -.env +output/ +# Environment files +.env \ No newline at end of file diff --git a/README.md b/README.md index 820a955..5497c29 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,66 @@ # collector -qmt client collector \ No newline at end of file +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) \ No newline at end of file diff --git a/README_NEW.md b/README_NEW.md new file mode 100644 index 0000000..f56cfc1 --- /dev/null +++ b/README_NEW.md @@ -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 diff --git a/REFACTOR.md b/REFACTOR.md new file mode 100644 index 0000000..334ef0c --- /dev/null +++ b/REFACTOR.md @@ -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 +``` + +## 总结 + +本次重构在不改变功能的前提下,使用了更现代化、更易维护的技术栈: +- ✅ 更灵活的定时任务调度 +- ✅ 更简洁的数据库操作 +- ✅ 自动表结构管理 +- ✅ 更好的代码可读性和可维护性 +- ✅ 保持向后兼容 diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..cc1d161 --- /dev/null +++ b/cmd/main.go @@ -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 +} diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..b71162d --- /dev/null +++ b/collector/collector.go @@ -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 +} diff --git a/collector/collector_test.go b/collector/collector_test.go new file mode 100644 index 0000000..ca01577 --- /dev/null +++ b/collector/collector_test.go @@ -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") + } +} diff --git a/exmple/status.json b/exmple/status.json new file mode 100644 index 0000000..b8b3bc4 --- /dev/null +++ b/exmple/status.json @@ -0,0 +1 @@ +{"data":{"assets":{"account_id":"8889399698","cash":1007973.27,"frozen_cash":0.0,"market_value":293981.0,"profit":1.0,"total_asset":1301955.27},"order":[{"order_id":1098913417,"order_remark":"{\"op\":53.36,\"pnl\":0,\"ac\":0}","order_status":56,"order_time":1775525400,"price":53.36,"stock_code":"600183.SH","traded_price":53.36,"traded_volume":100,"volume":100},{"order_id":1098913421,"order_remark":"{\"op\":90.96000000000001,\"pnl\":0,\"ac\":0}","order_status":54,"order_time":1775525400,"price":90.96000000000001,"stock_code":"603296.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098917977,"order_remark":"{\"op\":49.08,\"pnl\":9.84,\"ac\":0}","order_status":56,"order_time":1775525490,"price":53.910000000000004,"stock_code":"002947.SZ","traded_price":53.91,"traded_volume":100,"volume":100},{"order_id":1098917980,"order_remark":"{\"op\":95.85000000000001,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775525490,"price":95.85000000000001,"stock_code":"688313.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098918813,"order_remark":"{\"op\":82.7,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775525520,"price":82.7,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098923810,"order_remark":"{\"op\":94,\"pnl\":0,\"ac\":0}","order_status":56,"order_time":1775525730,"price":94.0,"stock_code":"603296.SH","traded_price":94.0,"traded_volume":100,"volume":100},{"order_id":1098925581,"order_remark":"{\"op\":83.24,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775525820,"price":83.24,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098926730,"order_remark":"{\"op\":103.95,\"pnl\":-5.15,\"ac\":1}","order_status":56,"order_time":1775525881,"price":98.6,"stock_code":"603259.SH","traded_price":98.59,"traded_volume":100,"volume":100},{"order_id":1098930600,"order_remark":"{\"op\":94.96000000000001,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775526090,"price":94.96000000000001,"stock_code":"688313.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098931257,"order_remark":"{\"op\":82.96000000000001,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775526120,"price":82.96000000000001,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098936143,"order_remark":"{\"op\":82.8,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775526420,"price":82.8,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098944557,"order_remark":"{\"op\":82.65,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775526990,"price":82.65,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098948975,"order_remark":"{\"op\":82.81,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775527290,"price":82.81,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098950662,"order_remark":"{\"op\":92.72,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775527410,"price":92.72,"stock_code":"688313.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098953078,"order_remark":"{\"op\":82.8,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775527591,"price":82.8,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098957232,"order_remark":"{\"op\":92.85000000000001,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775527890,"price":92.85000000000001,"stock_code":"688313.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098957691,"order_remark":"{\"op\":82.86,\"pnl\":0,\"ac\":0}","order_status":57,"order_time":1775527921,"price":82.86,"stock_code":"688183.SH","traded_price":0.0,"traded_volume":0,"volume":100},{"order_id":1098980905,"order_remark":"{\"op\":43.79,\"pnl\":-5.02,\"ac\":1}","order_status":56,"order_time":1775532570,"price":41.59,"stock_code":"002050.SZ","traded_price":41.59,"traded_volume":100,"volume":100}],"positions":[{"avg_price":18.7202,"can_use_volume":100,"code":"600062.SH","current_price":18.41,"frozen_volume":0,"market_value":1841.0,"min_profit_rate":9.0,"open_price":18.72,"profit":-31.019999999999754,"profit_rate":-1.66,"volume":100},{"avg_price":26.6903,"can_use_volume":100,"code":"600489.SH","current_price":26.16,"frozen_volume":0,"market_value":2616.0,"min_profit_rate":9.0,"open_price":26.69,"profit":-53.0300000000002,"profit_rate":-1.99,"volume":100},{"avg_price":13.1401,"can_use_volume":200,"code":"600531.SH","current_price":12.72,"frozen_volume":0,"market_value":2544.0,"min_profit_rate":9.0,"open_price":13.14,"profit":-84.01999999999998,"profit_rate":-3.2,"volume":200},{"avg_price":13.5401,"can_use_volume":100,"code":"600711.SH","current_price":13.44,"frozen_volume":0,"market_value":1344.0,"min_profit_rate":9.0,"open_price":13.54,"profit":-10.009999999999991,"profit_rate":-0.74,"volume":100},{"avg_price":17.6002,"can_use_volume":100,"code":"600961.SH","current_price":16.87,"frozen_volume":0,"market_value":1687.0,"min_profit_rate":9.0,"open_price":17.6,"profit":-73.01999999999998,"profit_rate":-4.15,"volume":100},{"avg_price":53.0805,"can_use_volume":100,"code":"601138.SH","current_price":52.26,"frozen_volume":0,"market_value":5226.0,"min_profit_rate":9.0,"open_price":53.08,"profit":-82.05000000000018,"profit_rate":-1.54,"volume":100},{"avg_price":26.450300000000002,"can_use_volume":100,"code":"601168.SH","current_price":25.37,"frozen_volume":0,"market_value":2537.0,"min_profit_rate":9.0,"open_price":26.45,"profit":-108.0300000000002,"profit_rate":-4.08,"volume":100},{"avg_price":123.65125,"can_use_volume":200,"code":"601799.SH","current_price":119.5,"frozen_volume":0,"market_value":23900.0,"min_profit_rate":7.0,"open_price":123.65,"profit":-830.25,"profit_rate":-3.36,"volume":200},{"avg_price":32.5203,"can_use_volume":100,"code":"601899.SH","current_price":32.38,"frozen_volume":0,"market_value":3238.0000000000005,"min_profit_rate":9.0,"open_price":32.52,"profit":-14.02999999999929,"profit_rate":-0.43,"volume":100},{"avg_price":33.1653,"can_use_volume":200,"code":"603067.SH","current_price":34.77,"frozen_volume":0,"market_value":6954.000000000001,"min_profit_rate":9.0,"open_price":33.16,"profit":320.9400000000005,"profit_rate":4.86,"volume":200},{"avg_price":92.9509,"can_use_volume":100,"code":"603202.SH","current_price":94.38,"frozen_volume":0,"market_value":9438.0,"min_profit_rate":9.0,"open_price":92.95,"profit":142.90999999999985,"profit_rate":1.54,"volume":100},{"avg_price":101.2705,"can_use_volume":100,"code":"603259.SH","current_price":101.28,"frozen_volume":0,"market_value":20256.0,"min_profit_rate":7.0,"open_price":101.27,"profit":1.9000000000014552,"profit_rate":0.01,"volume":200},{"avg_price":63.3256,"can_use_volume":200,"code":"603379.SH","current_price":61.41,"frozen_volume":0,"market_value":12282.0,"min_profit_rate":9.0,"open_price":63.32,"profit":-383.1200000000008,"profit_rate":-3.02,"volume":200},{"avg_price":18.1202,"can_use_volume":100,"code":"603993.SH","current_price":17.42,"frozen_volume":0,"market_value":1742.0000000000002,"min_profit_rate":9.0,"open_price":18.12,"profit":-70.01999999999975,"profit_rate":-3.86,"volume":100},{"avg_price":37.9504,"can_use_volume":100,"code":"605090.SH","current_price":38.82,"frozen_volume":0,"market_value":3882.0,"min_profit_rate":9.0,"open_price":37.95,"profit":86.96000000000004,"profit_rate":2.29,"volume":100},{"avg_price":80.97,"can_use_volume":100,"code":"000408.SZ","current_price":80.12,"frozen_volume":0,"market_value":8012.0,"min_profit_rate":9.0,"open_price":80.97,"profit":-85.0,"profit_rate":-1.05,"volume":100},{"avg_price":8.76,"can_use_volume":200,"code":"000544.SZ","current_price":7.91,"frozen_volume":0,"market_value":1582.0,"min_profit_rate":9.0,"open_price":8.76,"profit":-170.0,"profit_rate":-9.7,"volume":200},{"avg_price":11.62,"can_use_volume":200,"code":"000612.SZ","current_price":11.38,"frozen_volume":0,"market_value":2276.0,"min_profit_rate":9.0,"open_price":11.62,"profit":-48.0,"profit_rate":-2.07,"volume":200},{"avg_price":11.86,"can_use_volume":100,"code":"000700.SZ","current_price":10.93,"frozen_volume":0,"market_value":1093.0,"min_profit_rate":9.0,"open_price":11.86,"profit":-93.0,"profit_rate":-7.84,"volume":100},{"avg_price":31.61,"can_use_volume":100,"code":"000807.SZ","current_price":32.72,"frozen_volume":0,"market_value":3272.0,"min_profit_rate":9.0,"open_price":31.61,"profit":111.0,"profit_rate":3.51,"volume":100},{"avg_price":31.96,"can_use_volume":100,"code":"000933.SZ","current_price":31.59,"frozen_volume":0,"market_value":3159.0,"min_profit_rate":9.0,"open_price":31.96,"profit":-37.0,"profit_rate":-1.16,"volume":100},{"avg_price":33.295,"can_use_volume":200,"code":"000960.SZ","current_price":30.76,"frozen_volume":0,"market_value":6152.0,"min_profit_rate":9.0,"open_price":33.29,"profit":-507.0,"profit_rate":-7.6,"volume":200},{"avg_price":18.34,"can_use_volume":100,"code":"002039.SZ","current_price":18.74,"frozen_volume":0,"market_value":1874.0000000000002,"min_profit_rate":9.0,"open_price":18.34,"profit":40.00000000000023,"profit_rate":2.18,"volume":100},{"avg_price":42.69,"can_use_volume":100,"code":"002050.SZ","current_price":41.58,"frozen_volume":0,"market_value":8316.0,"min_profit_rate":9.0,"open_price":42.69,"profit":-222.0,"profit_rate":-2.6,"volume":200},{"avg_price":36.1,"can_use_volume":200,"code":"002074.SZ","current_price":33.64,"frozen_volume":0,"market_value":6728.0,"min_profit_rate":9.0,"open_price":36.1,"profit":-492.0,"profit_rate":-6.81,"volume":200},{"avg_price":23.81,"can_use_volume":100,"code":"002130.SZ","current_price":22.87,"frozen_volume":0,"market_value":2287.0,"min_profit_rate":9.0,"open_price":23.81,"profit":-94.0,"profit_rate":-3.95,"volume":100},{"avg_price":7.76,"can_use_volume":200,"code":"002131.SZ","current_price":7.2,"frozen_volume":0,"market_value":1440.0,"min_profit_rate":9.0,"open_price":7.76,"profit":-112.0,"profit_rate":-7.22,"volume":200},{"avg_price":30.04,"can_use_volume":100,"code":"002155.SZ","current_price":29.07,"frozen_volume":0,"market_value":2907.0,"min_profit_rate":9.0,"open_price":30.04,"profit":-97.0,"profit_rate":-3.23,"volume":100},{"avg_price":27.802,"can_use_volume":500,"code":"002270.SZ","current_price":26.12,"frozen_volume":0,"market_value":13060.0,"min_profit_rate":9.0,"open_price":27.8,"profit":-841.0,"profit_rate":-6.04,"volume":500},{"avg_price":40.005,"can_use_volume":200,"code":"002315.SZ","current_price":38.49,"frozen_volume":0,"market_value":7698.0,"min_profit_rate":9.0,"open_price":40.0,"profit":-303.0000000000009,"profit_rate":-3.77,"volume":200},{"avg_price":18.97,"can_use_volume":100,"code":"002517.SZ","current_price":18.23,"frozen_volume":0,"market_value":1823.0,"min_profit_rate":9.0,"open_price":18.97,"profit":-74.0,"profit_rate":-3.9,"volume":100},{"avg_price":22.545,"can_use_volume":200,"code":"002555.SZ","current_price":21.58,"frozen_volume":0,"market_value":4316.0,"min_profit_rate":9.0,"open_price":22.54,"profit":-193.0,"profit_rate":-4.26,"volume":200},{"avg_price":102.925,"can_use_volume":200,"code":"002594.SZ","current_price":97.8,"frozen_volume":0,"market_value":19560.0,"min_profit_rate":7.0,"open_price":102.92,"profit":-1025.0,"profit_rate":-4.97,"volume":200},{"avg_price":17.24,"can_use_volume":100,"code":"002602.SZ","current_price":17.31,"frozen_volume":0,"market_value":1730.9999999999998,"min_profit_rate":9.0,"open_price":17.23,"profit":7.0,"profit_rate":0.46,"volume":100},{"avg_price":32.45,"can_use_volume":200,"code":"002801.SZ","current_price":29.6,"frozen_volume":0,"market_value":5920.0,"min_profit_rate":9.0,"open_price":32.45,"profit":-570.0000000000009,"profit_rate":-8.78,"volume":200},{"avg_price":0.0,"can_use_volume":0,"code":"002947.SZ","current_price":0.0,"frozen_volume":0,"market_value":0.0,"min_profit_rate":8.0,"open_price":0.0,"profit":0.0,"profit_rate":0.0,"volume":0},{"avg_price":259.57,"can_use_volume":100,"code":"300476.SZ","current_price":263.7,"frozen_volume":0,"market_value":26370.0,"min_profit_rate":7.0,"open_price":259.57,"profit":413.0,"profit_rate":1.59,"volume":100},{"avg_price":115.77,"can_use_volume":100,"code":"300604.SZ","current_price":117.32,"frozen_volume":0,"market_value":11732.0,"min_profit_rate":7.0,"open_price":115.77,"profit":155.0,"profit_rate":1.34,"volume":100},{"avg_price":395.14,"can_use_volume":100,"code":"300750.SZ","current_price":382.29,"frozen_volume":0,"market_value":38229.0,"min_profit_rate":4.0,"open_price":395.14,"profit":-1285.0,"profit_rate":-3.25,"volume":100},{"avg_price":53.36,"can_use_volume":0,"code":"600183.SH","current_price":0.0,"frozen_volume":0,"market_value":5467.0,"min_profit_rate":8.0,"open_price":53.36,"profit":131.0,"profit_rate":0.0,"volume":100},{"avg_price":94.0,"can_use_volume":0,"code":"603296.SH","current_price":0.0,"frozen_volume":0,"market_value":9490.0,"min_profit_rate":8.0,"open_price":94.0,"profit":90.0,"profit_rate":0.0,"volume":100}],"tick_data":{"000408.SZ":{"amount":544021900,"askPrice":[80.12,80.13,80.2,80.23,80.26],"askVol":[10,142,8,26,5],"bidPrice":[80.03,80.02,80.01,80,79.99],"bidVol":[3,15,201,1143,59],"high":81.87,"lastClose":81.4,"lastPrice":80.12,"lastSettlementPrice":81.4,"low":80.01,"open":81.27,"openInt":14,"pvolume":6753019,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":67530},"000544.SZ":{"amount":48384900,"askPrice":[7.92,7.930000000000001,7.94,7.95,7.96],"askVol":[185,120,377,82,80],"bidPrice":[7.91,7.9,7.890000000000001,7.88,7.87],"bidVol":[188,1225,679,992,181],"high":7.99,"lastClose":7.98,"lastPrice":7.91,"lastSettlementPrice":7.98,"low":7.88,"open":7.96,"openInt":14,"pvolume":6099666,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":60997},"000612.SZ":{"amount":229221300,"askPrice":[11.38,11.39,11.4,11.41,11.42],"askVol":[306,171,304,88,80],"bidPrice":[11.37,11.36,11.35,11.34,11.33],"bidVol":[322,2407,395,26,364],"high":11.56,"lastClose":11.38,"lastPrice":11.38,"lastSettlementPrice":11.38,"low":11.27,"open":11.4,"openInt":14,"pvolume":20026648,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":200266},"000700.SZ":{"amount":305448900,"askPrice":[10.94,10.95,10.96,10.97,10.98],"askVol":[158,118,51,10,40],"bidPrice":[10.93,10.92,10.91,10.9,10.89],"bidVol":[12,70,276,1158,194],"high":11.16,"lastClose":11.52,"lastPrice":10.93,"lastSettlementPrice":11.52,"low":10.53,"open":10.99,"openInt":14,"pvolume":27816337,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":278163},"000807.SZ":{"amount":973199300,"askPrice":[32.73,32.74,32.75,32.76,32.77],"askVol":[11,170,55,38,48],"bidPrice":[32.72,32.71,32.7,32.69,32.68],"bidVol":[93,109,128,210,51],"high":33.2,"lastClose":31.99,"lastPrice":32.72,"lastSettlementPrice":31.99,"low":31.9,"open":32.19,"openInt":14,"pvolume":29782542,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":297825},"000933.SZ":{"amount":855696700,"askPrice":[31.6,31.61,31.62,31.63,31.64],"askVol":[44,4,1,77,3],"bidPrice":[31.59,31.58,31.57,31.56,31.55],"bidVol":[204,143,303,75,601],"high":32.18,"lastClose":30.76,"lastPrice":31.59,"lastSettlementPrice":30.76,"low":30.79,"open":30.95,"openInt":14,"pvolume":27146708,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":271467},"000960.SZ":{"amount":560359500,"askPrice":[30.77,30.79,30.8,30.81,30.82],"askVol":[3,3,17,127,1],"bidPrice":[30.76,30.74,30.73,30.72,30.71],"bidVol":[3,39,233,80,90],"high":31.22,"lastClose":30.29,"lastPrice":30.76,"lastSettlementPrice":30.29,"low":30.47,"open":30.7,"openInt":14,"pvolume":18155888,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":181559},"002039.SZ":{"amount":181879300,"askPrice":[18.74,18.75,18.76,18.77,18.78],"askVol":[419,448,167,2,11],"bidPrice":[18.73,18.72,18.71,18.7,18.69],"bidVol":[17,132,90,394,107],"high":19.05,"lastClose":18.38,"lastPrice":18.74,"lastSettlementPrice":18.38,"low":18.36,"open":18.46,"openInt":14,"pvolume":9748775,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":97488},"002050.SZ":{"amount":1033077700,"askPrice":[41.6,41.61,41.62,41.63,41.64],"askVol":[78,19,329,6,13],"bidPrice":[41.59,41.58,41.57,41.56,41.55],"bidVol":[3,322,117,126,353],"high":42.25,"lastClose":41.9,"lastPrice":41.58,"lastSettlementPrice":41.9,"low":41.58,"open":41.9,"openInt":14,"pvolume":24649840,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":246498},"002074.SZ":{"amount":463804100,"askPrice":[33.65,33.66,33.67,33.68,33.69],"askVol":[83,22,30,91,15],"bidPrice":[33.64,33.63,33.62,33.61,33.6],"bidVol":[282,16,42,74,1352],"high":34.24,"lastClose":33.46,"lastPrice":33.64,"lastSettlementPrice":33.46,"low":33.57,"open":33.8,"openInt":14,"pvolume":13659662,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":136597},"002130.SZ":{"amount":288606400,"askPrice":[22.87,22.88,22.89,22.9,22.91],"askVol":[118,5,38,185,28],"bidPrice":[22.85,22.84,22.83,22.82,22.81],"bidVol":[228,170,133,181,548],"high":23.18,"lastClose":22.89,"lastPrice":22.87,"lastSettlementPrice":22.89,"low":22.84,"open":22.95,"openInt":14,"pvolume":12525400,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":125254},"002131.SZ":{"amount":1206895200,"askPrice":[7.21,7.22,7.23,7.24,7.25],"askVol":[7315,4162,3539,4360,5655],"bidPrice":[7.2,7.19,7.18,7.17,7.16],"bidVol":[9182,8803,11109,4993,5928],"high":7.350000000000001,"lastClose":7.12,"lastPrice":7.2,"lastSettlementPrice":7.12,"low":7.13,"open":7.19,"openInt":14,"pvolume":166674452,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":1666745},"002155.SZ":{"amount":689847000,"askPrice":[29.07,29.08,29.09,29.1,29.11],"askVol":[9,89,73,96,38],"bidPrice":[29.05,29.04,29.03,29.02,29.01],"bidVol":[65,68,73,24,243],"high":29.23,"lastClose":29.05,"lastPrice":29.07,"lastSettlementPrice":29.05,"low":28.77,"open":29,"openInt":14,"pvolume":23776070,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":237761},"002270.SZ":{"amount":99261000,"askPrice":[26.11,26.12,26.13,26.14,26.15],"askVol":[7,4,1,3,9],"bidPrice":[26.1,26.09,26.08,26.07,26.06],"bidVol":[9,10,130,49,55],"high":26.51,"lastClose":26.37,"lastPrice":26.12,"lastSettlementPrice":26.37,"low":26.01,"open":26.28,"openInt":14,"pvolume":3777397,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":37774},"002315.SZ":{"amount":134078700,"askPrice":[38.51,38.54,38.55,38.57,38.6],"askVol":[21,17,3,35,9],"bidPrice":[38.5,38.49,38.48,38.47,38.46],"bidVol":[3,3,34,9,8],"high":39.18,"lastClose":38.26,"lastPrice":38.49,"lastSettlementPrice":38.26,"low":38.49,"open":38.51,"openInt":14,"pvolume":3456200,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":34562},"002517.SZ":{"amount":335243300,"askPrice":[18.24,18.25,18.26,18.27,18.28],"askVol":[66,134,40,85,36],"bidPrice":[18.23,18.22,18.21,18.2,18.19],"bidVol":[39,244,803,1657,182],"high":18.58,"lastClose":18.39,"lastPrice":18.23,"lastSettlementPrice":18.39,"low":18.21,"open":18.39,"openInt":14,"pvolume":18223850,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":182239},"002555.SZ":{"amount":304914800,"askPrice":[21.57,21.58,21.6,21.62,21.63],"askVol":[2,226,82,12,4],"bidPrice":[21.55,21.54,21.53,21.52,21.51],"bidVol":[20,7,208,159,146],"high":21.97,"lastClose":21.72,"lastPrice":21.58,"lastSettlementPrice":21.72,"low":21.53,"open":21.82,"openInt":14,"pvolume":13996507,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":139965},"002594.SZ":{"amount":2411651100,"askPrice":[97.8,97.81,97.82000000000001,97.84,97.85000000000001],"askVol":[19,63,57,4,13],"bidPrice":[97.79,97.78,97.77,97.76,97.75],"bidVol":[23,209,317,44,40],"high":99.77,"lastClose":99.01,"lastPrice":97.8,"lastSettlementPrice":99.01,"low":97.77,"open":99.5,"openInt":14,"pvolume":24474814,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":244748},"002602.SZ":{"amount":924013900,"askPrice":[17.31,17.32,17.33,17.34,17.35],"askVol":[95,243,461,259,133],"bidPrice":[17.3,17.29,17.28,17.27,17.26],"bidVol":[158,662,1061,380,315],"high":17.56,"lastClose":17.04,"lastPrice":17.31,"lastSettlementPrice":17.04,"low":17.19,"open":17.4,"openInt":14,"pvolume":53154641,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":531546},"002801.SZ":{"amount":34222900,"askPrice":[29.61,29.62,29.63,29.64,29.65],"askVol":[14,11,8,35,9],"bidPrice":[29.6,29.59,29.58,29.56,29.55],"bidVol":[158,6,91,43,24],"high":30.2,"lastClose":29.93,"lastPrice":29.6,"lastSettlementPrice":29.93,"low":29.58,"open":30.2,"openInt":14,"pvolume":1147950,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":11480},"002947.SZ":{"amount":568669800,"askPrice":[54.18,54.19,54.2,54.21,54.24],"askVol":[2,3,15,35,6],"bidPrice":[54.16,54.15,54.14,54.11,54.1],"bidVol":[69,16,2,15,27],"high":55.8,"lastClose":53.91,"lastPrice":54.16,"lastSettlementPrice":53.91,"low":53.59,"open":53.79,"openInt":14,"pvolume":10375320,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":103753},"300476.SZ":{"amount":3999769400,"askPrice":[263.71,263.73,263.75,263.77,263.81],"askVol":[20,2,3,1,15],"bidPrice":[263.7,263.69,263.68,263.66,263.65],"bidVol":[29,42,41,26,61],"high":271.57,"lastClose":263.68,"lastPrice":263.7,"lastSettlementPrice":263.68,"low":263.01,"open":266.19,"openInt":14,"pvolume":14972179,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":149722},"300604.SZ":{"amount":848761200,"askPrice":[117.33,117.34,117.37,117.38,117.39],"askVol":[10,15,1,3,1],"bidPrice":[117.32,117.3,117.29,117.26,117.24],"bidVol":[2,9,2,1,13],"high":119,"lastClose":115,"lastPrice":117.32,"lastSettlementPrice":115,"low":115.5,"open":116.5,"openInt":14,"pvolume":7213309,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":72133},"300750.SZ":{"amount":4450304500,"askPrice":[382.29,382.34,382.37,382.38,382.39],"askVol":[6,5,1,1,18],"bidPrice":[382.07,382.05,382.03,382.02,382.01],"bidVol":[5,2,1,4,12],"high":393.2,"lastClose":386.46,"lastPrice":382.29,"lastSettlementPrice":386.46,"low":381.49,"open":389.98,"openInt":14,"pvolume":11471746,"settlementPrice":0,"stockStatus":4,"time":1775532600000,"timetag":"20260407 11:30:00","volume":114717},"600062.SH":{"amount":101814000,"askPrice":[18.41,18.43,18.44,18.45,18.46],"askVol":[39,19,4,24,4],"bidPrice":[18.4,18.39,18.38,18.37,18.36],"bidVol":[16,98,43,66,166],"high":18.57,"lastClose":18.57,"lastPrice":18.41,"lastSettlementPrice":18.57,"low":18.2,"open":18.56,"openInt":13,"pvolume":5534500,"settlementPrice":0,"stockStatus":3,"time":1775532586000,"timetag":"20260407 11:29:46","volume":55345},"600183.SH":{"amount":2314230500,"askPrice":[54.68,54.69,54.7,54.71,54.73],"askVol":[7,3,27,43,4],"bidPrice":[54.66,54.65,54.64,54.63,54.62],"bidVol":[6,47,57,25,40],"high":55.96,"lastClose":52.5,"lastPrice":54.67,"lastSettlementPrice":52.5,"low":53,"open":53.36,"openInt":13,"pvolume":42178121,"settlementPrice":0,"stockStatus":3,"time":1775532602000,"timetag":"20260407 11:30:02","volume":421781},"600489.SH":{"amount":597499600,"askPrice":[26.16,26.17,26.18,26.19,26.2],"askVol":[35,55,76,13,134],"bidPrice":[26.15,26.14,26.13,26.12,26.11],"bidVol":[919,320,374,418,455],"high":26.84,"lastClose":26.68,"lastPrice":26.16,"lastSettlementPrice":26.68,"low":26.14,"open":26.63,"openInt":13,"pvolume":22673457,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":226735},"600531.SH":{"amount":198146700,"askPrice":[12.73,12.74,12.75,12.76,12.77],"askVol":[129,8,81,24,235],"bidPrice":[12.72,12.71,12.7,12.69,12.68],"bidVol":[220,511,849,112,235],"high":12.85,"lastClose":12.75,"lastPrice":12.72,"lastSettlementPrice":12.75,"low":12.62,"open":12.7,"openInt":13,"pvolume":15527496,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":155275},"600711.SH":{"amount":1028545800,"askPrice":[13.44,13.45,13.46,13.47,13.48],"askVol":[156,115,2,79,206],"bidPrice":[13.43,13.42,13.41,13.4,13.39],"bidVol":[426,1007,1419,4069,600],"high":13.71,"lastClose":13.2,"lastPrice":13.44,"lastSettlementPrice":13.2,"low":13.2,"open":13.2,"openInt":13,"pvolume":76325249,"settlementPrice":0,"stockStatus":3,"time":1775532602000,"timetag":"20260407 11:30:02","volume":763252},"600961.SH":{"amount":103144500,"askPrice":[16.87,16.88,16.89,16.9,16.91],"askVol":[5,135,19,212,102],"bidPrice":[16.86,16.85,16.84,16.83,16.82],"bidVol":[163,110,31,166,69],"high":17.17,"lastClose":16.82,"lastPrice":16.87,"lastSettlementPrice":16.82,"low":16.83,"open":16.88,"openInt":13,"pvolume":6065522,"settlementPrice":0,"stockStatus":3,"time":1775532600000,"timetag":"20260407 11:30:00","volume":60655},"601138.SH":{"amount":4578003800,"askPrice":[52.26,52.27,52.28,52.29,52.3],"askVol":[1,25,42,183,49],"bidPrice":[52.25,52.24,52.23,52.22,52.21],"bidVol":[74,66,68,122,188],"high":54,"lastClose":52.18,"lastPrice":52.26,"lastSettlementPrice":52.18,"low":52.2,"open":52.98,"openInt":13,"pvolume":86039741,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":860397},"601168.SH":{"amount":401519500,"askPrice":[25.38,25.39,25.4,25.41,25.42],"askVol":[49,85,77,64,51],"bidPrice":[25.37,25.36,25.35,25.34,25.33],"bidVol":[593,919,937,20,587],"high":25.85,"lastClose":25.54,"lastPrice":25.37,"lastSettlementPrice":25.54,"low":25.35,"open":25.55,"openInt":13,"pvolume":15720965,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":157210},"601799.SH":{"amount":103168300,"askPrice":[119.59,119.6,119.62,119.63,119.64],"askVol":[1,1,2,4,4],"bidPrice":[119.47,119.46,119.45,119.44,119.42],"bidVol":[1,1,2,3,1],"high":120.5,"lastClose":119.77,"lastPrice":119.5,"lastSettlementPrice":119.77,"low":119.27,"open":120.2,"openInt":13,"pvolume":860500,"settlementPrice":0,"stockStatus":3,"time":1775532600000,"timetag":"20260407 11:30:00","volume":8605},"601899.SH":{"amount":2921148500,"askPrice":[32.39,32.4,32.41,32.42,32.43],"askVol":[1,168,168,140,174],"bidPrice":[32.38,32.37,32.36,32.35,32.34],"bidVol":[497,43,112,425,169],"high":33.1,"lastClose":32.91,"lastPrice":32.38,"lastSettlementPrice":32.91,"low":32.33,"open":32.8,"openInt":13,"pvolume":89561039,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":895610},"603067.SH":{"amount":877595400,"askPrice":[34.78,34.79,34.8,34.81,34.84],"askVol":[5,12,174,2,10],"bidPrice":[34.77,34.76,34.75,34.74,34.73],"bidVol":[26,52,54,51,18],"high":35.69,"lastClose":34.68,"lastPrice":34.77,"lastSettlementPrice":34.68,"low":33.71,"open":33.92,"openInt":13,"pvolume":25102696,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":251027},"603202.SH":{"amount":31184800,"askPrice":[94.38,94.39,94.4,94.41,94.42],"askVol":[28,31,10,20,28],"bidPrice":[94.26,94.23,94.21000000000001,94.2,94.15],"bidVol":[1,1,4,11,8],"high":94.69,"lastClose":94.35,"lastPrice":94.38,"lastSettlementPrice":94.35,"low":93.91,"open":94.35000000000001,"openInt":13,"pvolume":330700,"settlementPrice":0,"stockStatus":3,"time":1775532597000,"timetag":"20260407 11:29:57","volume":3307},"603259.SH":{"amount":2608535000,"askPrice":[101.28,101.29,101.3,101.31,101.32],"askVol":[5,11,122,2,5],"bidPrice":[101.27,101.26,101.25,101.24,101.23],"bidVol":[18,15,6,2,5],"high":103.26,"lastClose":99.99,"lastPrice":101.28,"lastSettlementPrice":99.99,"low":98.42,"open":100.02,"openInt":13,"pvolume":25790532,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":257905},"603296.SH":{"amount":1623269700,"askPrice":[94.94,94.95,94.96000000000001,94.97,94.98],"askVol":[1,2,6,4,11],"bidPrice":[94.9,94.88,94.86,94.85000000000001,94.83],"bidVol":[7,18,20,6,23],"high":97.5,"lastClose":90.9,"lastPrice":94.9,"lastSettlementPrice":90.9,"low":90.96000000000001,"open":90.96000000000001,"openInt":13,"pvolume":16985511,"settlementPrice":0,"stockStatus":3,"time":1775532599000,"timetag":"20260407 11:29:59","volume":169855},"603379.SH":{"amount":184999400,"askPrice":[61.41,61.44,61.5,61.51,61.52],"askVol":[6,4,9,3,2],"bidPrice":[61.4,61.39,61.37,61.35,61.34],"bidVol":[1,1,4,12,1],"high":62.74,"lastClose":61.78,"lastPrice":61.41,"lastSettlementPrice":61.78,"low":61.21,"open":61.78,"openInt":13,"pvolume":2986640,"settlementPrice":0,"stockStatus":3,"time":1775532600000,"timetag":"20260407 11:30:00","volume":29866},"603993.SH":{"amount":1459300100,"askPrice":[17.42,17.43,17.44,17.45,17.46],"askVol":[88,32,355,652,109],"bidPrice":[17.41,17.4,17.39,17.38,17.37],"bidVol":[867,7227,787,2154,304],"high":17.69,"lastClose":17.52,"lastPrice":17.42,"lastSettlementPrice":17.52,"low":17.41,"open":17.56,"openInt":13,"pvolume":83231404,"settlementPrice":0,"stockStatus":3,"time":1775532600000,"timetag":"20260407 11:30:00","volume":832314},"605090.SH":{"amount":404327100,"askPrice":[38.83,38.86,38.88,38.89,38.9],"askVol":[368,8,10,605,24],"bidPrice":[38.82,38.81,38.8,38.79,38.78],"bidVol":[116,261,231,20,415],"high":39.78,"lastClose":38.13,"lastPrice":38.82,"lastSettlementPrice":38.13,"low":37.8,"open":38.18,"openInt":13,"pvolume":10363045,"settlementPrice":0,"stockStatus":3,"time":1775532602000,"timetag":"20260407 11:30:02","volume":103630},"688183.SH":{"amount":574081700,"askPrice":[80.92,80.94,80.95,80.96000000000001,80.97],"askVol":[13,6,2,4,6],"bidPrice":[80.89,80.88,80.86,80.85000000000001,80.84],"bidVol":[18,18,28,24,78],"high":83.5,"lastClose":82.59,"lastPrice":80.89,"lastSettlementPrice":82.59,"low":80.81,"open":82.41,"openInt":13,"pvolume":6980780,"settlementPrice":0,"stockStatus":3,"time":1775532601000,"timetag":"20260407 11:30:01","volume":69808},"688313.SH":{"amount":2630091400,"askPrice":[90.12,90.13,90.14,90.15,90.16],"askVol":[21,91,33,4,36],"bidPrice":[90.11,90.10000000000001,90.09,90.08,90.07000000000001],"bidVol":[45,244,92,54,35],"high":96.55,"lastClose":96.4,"lastPrice":90.12,"lastSettlementPrice":96.4,"low":90.11,"open":94.5,"openInt":13,"pvolume":28277842,"settlementPrice":0,"stockStatus":3,"time":1775532602000,"timetag":"20260407 11:30:02","volume":282778}}},"status":{"config_key":"liao","home_name":"t8zznqs49f1ju7q","project_root":"D:\\work\\qmt","qmt_status":"connected","start_time":1775522874118621100}} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..55cb122 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9928e85 --- /dev/null +++ b/go.sum @@ -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= diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..c062164 --- /dev/null +++ b/models/models.go @@ -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"` +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..40bc14e --- /dev/null +++ b/scripts/build.sh @@ -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 \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..36d498a --- /dev/null +++ b/scripts/deploy.sh @@ -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 "=== 部署完成 ===" \ No newline at end of file diff --git a/scripts/schema.sql b/scripts/schema.sql new file mode 100644 index 0000000..88e92d7 --- /dev/null +++ b/scripts/schema.sql @@ -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); diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100644 index 0000000..f6ec2de --- /dev/null +++ b/scripts/update.sh @@ -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 \ No newline at end of file diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..3416dda --- /dev/null +++ b/start.bat @@ -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 diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..0dc76cb --- /dev/null +++ b/start.sh @@ -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 diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..b3bb9e7 --- /dev/null +++ b/storage/storage.go @@ -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 +}