review code.

This commit is contained in:
2026-04-17 14:50:34 +08:00
parent a0a6d6adaa
commit 592c8e31dd
15 changed files with 487 additions and 338 deletions

View File

@@ -11,6 +11,7 @@ QMT数据采集器是一个专门用于从QMT量化交易平台采集实时交
- **订单状态跟踪**:持续监控股票订单的状态变化,包括已成交、未成交、部分成交等各种状态
- **持仓信息管理**:记录每只股票的持仓数量、可用数量、成本价、当前价及盈亏情况
- **行情数据捕获**收集股票的实时Tick数据包括买卖五档价格、成交量、成交额等市场深度信息
- **订单簿追踪**:自动追踪完整的交易周期,从买入到卖出形成闭合的交易记录,计算实际盈亏
### 2. 智能变化检测
- 采用SHA256哈希算法对采集的数据进行指纹计算

View File

@@ -1,139 +1,12 @@
package main
import (
"fmt"
"log"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"git.apinb.com/quant/collector/collector"
"git.apinb.com/quant/collector/storage"
"github.com/robfig/cron/v3"
"git.apinb.com/quant/collector/internal/impl"
"git.apinb.com/quant/collector/internal/logic"
)
func main() {
log.Println("=== QMT数据采集器启动 ===")
impl.NewImpl()
// 从环境变量获取配置
collectorURL := getEnv("COLLECTOR_URL", "http://localhost:5000/status")
dbConnStr := getEnv("DATABASE_URL", "host=139.224.247.176 user=postgres password=Stock0310~! dbname=stock_prod port=19432 sslmode=disable TimeZone=Asia/Shanghai")
interval := getEnvAsInt("COLLECTION_INTERVAL", 30) // 默认60秒
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("定时任务已启动")
// 等待退出信号
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) {
// 检查是否为开市时间
now := time.Now()
if !collector.IsTradingTime(now) {
return
}
log.Println("开始采集...")
// 采集数据并检查变化
status, dataHash, changed, err := coll.CollectAndCheck()
if err != nil {
log.Printf("采集失败: %v", err)
return
}
log.Printf("数据哈希: %s", dataHash)
log.Printf("数据是否变化: %v", changed)
// 如果数据没有变化,只记录日志
if !changed {
log.Println("数据未变化,跳过存储")
return
}
// 数据有变化,保存到数据库
log.Println("数据已变化,开始存储到数据库...")
if err := store.SaveData(status); err != nil {
log.Printf("保存数据失败: %v", err)
return
}
// 记录成功的日志
// if err := store.SaveCollectionLog(dataHash, ymd, 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
logic.Boot()
}

34
go.mod
View File

@@ -9,13 +9,45 @@ require (
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/allegro/bigcache/v3 v3.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
go.etcd.io/etcd/api/v3 v3.6.10 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.10 // indirect
go.etcd.io/etcd/client/v3 v3.6.10 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
)
require (
git.apinb.com/bsm-sdk/core v0.1.9
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect
)

88
go.sum
View File

@@ -1,6 +1,31 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.apinb.com/bsm-sdk/core v0.1.9 h1:Pp0zpSeX7OnFb3JQdXdJHR74EvU02HnMauk4wI0/gVg=
git.apinb.com/bsm-sdk/core v0.1.9/go.mod h1:R5Rm/Ep4D3mJ97dL6sLRi3jtfQxi2ftekp2E3frts/8=
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
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=
@@ -17,34 +42,97 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
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/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
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=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.6.10 h1:jlwjtELjA8yi2VWpOFH+0w0lGr3K6mVDyn0RDB9aaAY=
go.etcd.io/etcd/api/v3 v3.6.10/go.mod h1:pdV4VeFmvhdNjB4LWRkC8ReLyRBAxUOze3GarMhE2sk=
go.etcd.io/etcd/client/pkg/v3 v3.6.10 h1:tBT7podcPhuVbCVkAEzx8bC5I+aqxfLwBN8/As1arrA=
go.etcd.io/etcd/client/pkg/v3 v3.6.10/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y=
go.etcd.io/etcd/client/v3 v3.6.10 h1:J598zJ+C/ZPvImypmq5waj84+bovePrlZERHklf34y0=
go.etcd.io/etcd/client/v3 v3.6.10/go.mod h1:iHhUDUcEwaKs1YFq3MgmI9U4zhTVasp/vgdVbFf1RS8=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
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/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=

24
internal/impl/impl.go Normal file
View File

@@ -0,0 +1,24 @@
package impl
import (
"git.apinb.com/bsm-sdk/core/cache/redis"
"git.apinb.com/bsm-sdk/core/conf"
"git.apinb.com/bsm-sdk/core/with"
"gorm.io/gorm"
)
var (
CacheUrl = "redis://null:Weidong2023~!@139.224.247.176:19379/0"
DBSources = []string{"host=139.224.247.176 user=postgres password=Stock0310~! dbname=stock_prod port=19432 sslmode=disable TimeZone=Asia/Shanghai"}
RedisService *redis.RedisClient
DBService *gorm.DB
)
func NewImpl() {
RedisService = with.RedisCache(CacheUrl) // redis cache
// model
DBService = with.Databases(&conf.DBConf{
Driver: "postgres",
Source: DBSources,
}, nil)
}

100
internal/logic/boot.go Normal file
View File

@@ -0,0 +1,100 @@
package logic
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/robfig/cron/v3"
)
var (
COLLECTOR_URL = "http://localhost:5000/status"
COLLECTION_INTERVAL = 30
)
func Boot() {
log.Println("=== QMT数据采集器启动 ===")
log.Printf("采集地址: %s", COLLECTOR_URL)
log.Printf("采集间隔: %d秒", COLLECTION_INTERVAL)
// 创建采集器
coll := NewCollector(COLLECTOR_URL)
// 创建cron调度器
c := cron.New(cron.WithSeconds())
// 构建cron表达式 (每N秒执行一次)
cronSpec := fmt.Sprintf("@every %ds", COLLECTION_INTERVAL)
log.Printf("定时任务表达式: %s", cronSpec)
// 添加定时任务
_, err := c.AddFunc(cronSpec, func() {
runCollection(coll)
})
if err != nil {
log.Fatalf("添加定时任务失败: %v", err)
}
// 启动调度器
c.Start()
log.Println("定时任务已启动")
// 等待退出信号
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) {
// 检查是否为开市时间
now := time.Now()
if !IsTradingTime(now) {
return
}
log.Println("开始采集...")
// 采集数据并检查变化
status, dataHash, changed, err := coll.CollectAndCheck()
if err != nil {
log.Printf("采集失败: %v", err)
return
}
log.Printf("数据哈希: %s", dataHash)
log.Printf("数据是否变化: %v", changed)
// 如果数据没有变化,只记录日志
if !changed {
log.Println("数据未变化,跳过存储")
return
}
// 数据有变化,保存到数据库
log.Println("数据已变化,开始存储到数据库...")
if err := SaveData(status); err != nil {
log.Printf("保存数据失败: %v", err)
return
}
// 记录成功的日志
// if err := store.SaveCollectionLog(dataHash, ymd, 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))
}

View File

@@ -1,4 +1,4 @@
package collector
package logic
import (
"crypto/sha256"
@@ -9,7 +9,7 @@ import (
"net/http"
"time"
"git.apinb.com/quant/collector/types"
"git.apinb.com/quant/collector/internal/types"
)
// Collector 数据采集器

View File

@@ -1,92 +1,18 @@
package storage
package logic
import (
"encoding/json"
"fmt"
"log"
"time"
"git.apinb.com/quant/collector/models"
"git.apinb.com/quant/collector/types"
"gorm.io/driver/postgres"
"git.apinb.com/quant/collector/internal/impl"
"git.apinb.com/quant/collector/internal/models"
"git.apinb.com/quant/collector/internal/types"
"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.CollectorAssets{},
&models.CollectorOrder{},
&models.CollectorPosition{},
&models.CollectorTick{},
&models.CollectorLog{},
)
if err != nil {
return fmt.Errorf("自动迁移失败: %w", err)
}
log.Println("数据库表结构迁移完成")
return nil
}
// SaveStatus 保存完整状态数据(使用事务)
func (s *Storage) SaveData(status *types.StatusData) error {
func SaveData(status *types.StatusData) error {
// 验证必要字段 - AccountID是所有Upsert的共同条件
if status.Data.Assets.AccountID == "" {
return fmt.Errorf("账户ID不能为空")
@@ -98,7 +24,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
// 保存资产快照 (先查询后更新/插入)
var existingAsset models.CollectorAssets
err := s.db.Where("account_id = ? AND ymd = ?", status.Data.Assets.AccountID, ymd).First(&existingAsset).Error
err := impl.DBService.Where("account_id = ? AND ymd = ?", status.Data.Assets.AccountID, ymd).First(&existingAsset).Error
asset := models.CollectorAssets{
AccountID: status.Data.Assets.AccountID,
@@ -113,7 +39,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
if err == gorm.ErrRecordNotFound {
// 记录不存在,插入新记录
if err := s.db.Create(&asset).Error; err != nil {
if err := impl.DBService.Create(&asset).Error; err != nil {
return fmt.Errorf("插入资产快照失败: %w", err)
}
} else if err != nil {
@@ -123,7 +49,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
// 记录存在,更新现有记录
if asset.Cash != existingAsset.Cash || asset.FrozenCash != existingAsset.FrozenCash || asset.MarketValue != existingAsset.MarketValue || asset.Profit != existingAsset.Profit || asset.TotalAsset != existingAsset.TotalAsset {
asset.ID = existingAsset.ID
if err := s.db.Save(&asset).Error; err != nil {
if err := impl.DBService.Save(&asset).Error; err != nil {
return fmt.Errorf("更新资产快照失败: %w", err)
}
}
@@ -152,7 +78,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
// 查询是否存在
var cnt int64
s.db.Where("account_id = ? AND order_id = ? AND ymd = ?", status.Data.Assets.AccountID, order.OrderID, ymd).Count(&cnt)
impl.DBService.Where("account_id = ? AND order_id = ? AND ymd = ?", status.Data.Assets.AccountID, order.OrderID, ymd).Count(&cnt)
if cnt > 0 {
continue
}
@@ -176,9 +102,15 @@ func (s *Storage) SaveData(status *types.StatusData) error {
}
// 记录不存在,插入新记录
if err := s.db.Create(&orderRecord).Error; err != nil {
if err := impl.DBService.Create(&orderRecord).Error; err != nil {
return fmt.Errorf("插入订单失败: %w", err)
}
// 处理订单簿逻辑
if err := processOrderBook(status.Data.Assets.AccountID, order, ymd, now, open_price); err != nil {
fmt.Printf("处理订单簿失败: %v\n", err)
// 不返回错误,避免影响主流程
}
}
}
@@ -192,7 +124,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
// 查询是否存在
var existingPosition models.CollectorPosition
err := s.db.Where("account_id = ? AND code = ? AND ymd = ?",
err := impl.DBService.Where("account_id = ? AND code = ? AND ymd = ?",
status.Data.Assets.AccountID, pos.Code, ymd).First(&existingPosition).Error
positionRecord := models.CollectorPosition{
@@ -214,7 +146,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
if err == gorm.ErrRecordNotFound {
// 记录不存在,插入新记录
if err := s.db.Create(&positionRecord).Error; err != nil {
if err := impl.DBService.Create(&positionRecord).Error; err != nil {
return fmt.Errorf("插入持仓失败: %w", err)
}
} else if err != nil {
@@ -232,7 +164,7 @@ func (s *Storage) SaveData(status *types.StatusData) error {
positionRecord.ProfitRate != existingPosition.ProfitRate {
// 记录存在,更新现有记录
positionRecord.ID = existingPosition.ID
if err := s.db.Save(&positionRecord).Error; err != nil {
if err := impl.DBService.Save(&positionRecord).Error; err != nil {
return fmt.Errorf("更新持仓失败: %w", err)
}
}
@@ -245,24 +177,68 @@ func (s *Storage) SaveData(status *types.StatusData) error {
}
// SaveCollectionLog 保存采集日志
func (s *Storage) SaveCollectionLog(dataHash string, ymd int, hasChanged bool, statusMessage string) error {
log := models.CollectorLog{
DataHash: dataHash,
Ymd: ymd,
HasChanged: hasChanged,
StatusMessage: statusMessage,
CollectedAt: time.Now(),
}
// processOrderBook 处理订单簿逻辑
func processOrderBook(accountID string, order types.Order, ymd int, now time.Time, openPrice float64) error {
// OffsetFlag: 通常 0=买入, 1=卖出 (需要根据实际系统确认)
// 这里假设: OffsetFlag == 0 表示买入, OffsetFlag == 1 表示卖出
if err := s.db.Create(&log).Error; err != nil {
return fmt.Errorf("保存采集日志失败: %w", err)
if order.OffsetFlag == 0 {
// 买入订单 - 创建新的订单簿记录
orderBook := models.OrderBook{
AccountID: accountID,
StockCode: order.StockCode,
Ymd: ymd,
BuyOrderID: order.OrderID,
BuyPrice: order.TradedPrice,
BuyVolume: order.TradedVolume,
BuyTime: order.OrderTime,
BuyCollectedAt: now,
IsClosed: false,
}
if err := impl.DBService.Create(&orderBook).Error; err != nil {
return fmt.Errorf("创建订单簿记录失败: %w", err)
}
} else if order.OffsetFlag == 1 {
// 卖出订单 - 查找对应的买入记录并闭合
var orderBook models.OrderBook
// 查找同一账户、同一股票、未闭合的订单簿记录
err := impl.DBService.Where(
"account_id = ? AND stock_code = ? AND is_closed = ?",
accountID, order.StockCode, false,
).Order("buy_time ASC").First(&orderBook).Error
if err == gorm.ErrRecordNotFound {
// 没有找到对应的买入记录,可能是之前就已经持有的仓位
fmt.Printf("未找到对应的买入记录: account=%s, stock=%s\n", accountID, order.StockCode)
return nil
} else if err != nil {
return fmt.Errorf("查询订单簿记录失败: %w", err)
}
// 计算盈亏
buyAmount := float64(orderBook.BuyVolume) * orderBook.BuyPrice
sellAmount := float64(order.TradedVolume) * order.TradedPrice
profit := sellAmount - buyAmount
var profitRate float64
if buyAmount > 0 {
profitRate = (profit / buyAmount) * 100
}
// 更新订单簿记录
orderBook.SellOrderID = &order.OrderID
orderBook.SellPrice = &order.TradedPrice
orderBook.SellVolume = &order.TradedVolume
orderBook.SellTime = &order.OrderTime
orderBook.SellCollectedAt = &now
orderBook.IsClosed = true
orderBook.Profit = &profit
orderBook.ProfitRate = &profitRate
if err := impl.DBService.Save(&orderBook).Error; err != nil {
return fmt.Errorf("更新订单簿记录失败: %w", err)
}
}
return nil
}
// GetDB 获取GORM DB实例(用于高级查询)
func (s *Storage) GetDB() *gorm.DB {
return s.db
}

View File

@@ -1,4 +1,4 @@
package collector
package logic
import (
"time"

View File

@@ -0,0 +1,32 @@
package models
import (
"time"
"git.apinb.com/bsm-sdk/core/database"
"gorm.io/gorm"
)
// AssetSnapshot 资产快照数据库模型
type CollectorAssets struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
Cash float64 `json:"cash" gorm:"type:decimal(15,2);not null;default:0;comment:可用资金"`
FrozenCash float64 `json:"frozen_cash" gorm:"type:decimal(15,2);not null;default:0;column:frozen_cash;comment:冻结资金"`
MarketValue float64 `json:"market_value" gorm:"type:decimal(15,2);not null;default:0;column:market_value;comment:持仓市值"`
Profit float64 `json:"profit" gorm:"type:decimal(15,2);not null;default:0;comment:当日盈亏"`
TotalAsset float64 `json:"total_asset" gorm:"type:decimal(15,2);not null;default:0;column:total_asset;comment:总资产"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
func init() {
database.AppendMigrate(&CollectorAssets{})
}
// TableName 设置表名
func (CollectorAssets) TableName() string {
return "collector_assets"
}

View File

@@ -0,0 +1,37 @@
package models
import (
"time"
"git.apinb.com/bsm-sdk/core/database"
"gorm.io/gorm"
)
// OrderRecord 订单数据库模型
type CollectorOrder struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
OrderID int64 `json:"order_id" gorm:"not null;index;comment:订单ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
StockCode string `json:"stock_code" gorm:"type:varchar(20);not null;index;comment:股票代码"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
Price float64 `json:"price" gorm:"type:decimal(10,4);not null;default:0;comment:委托价格"`
Volume int `json:"volume" gorm:"not null;default:0;comment:委托数量"`
OpenPrice float64 `json:"open_price" gorm:"type:decimal(10,4);not null;default:0;column:open_price;comment:开仓价格"`
TradedPrice float64 `json:"traded_price" gorm:"type:decimal(10,4);not null;default:0;column:traded_price;comment:成交均价"`
TradedVolume int `json:"traded_volume" gorm:"not null;default:0;column:traded_volume;comment:成交数量"`
OrderStatus int `json:"order_status" gorm:"not null;default:0;column:order_status;comment:订单状态"`
OffsetFlag int `json:"offset_flag" gorm:"not null;default:0;column:offset_flag;comment:开平标志"`
OrderTime int64 `json:"order_time" gorm:"not null;column:order_time;comment:下单时间戳"`
OrderRemark string `json:"order_remark" gorm:"type:text;column:order_remark;comment:订单备注"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
func init() {
database.AppendMigrate(&CollectorOrder{})
}
func (CollectorOrder) TableName() string {
return "collector_orders"
}

View File

@@ -0,0 +1,38 @@
package models
import (
"time"
"git.apinb.com/bsm-sdk/core/database"
"gorm.io/gorm"
)
// PositionRecord 持仓数据库模型
type CollectorPosition struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
Code string `json:"code" gorm:"type:varchar(20);not null;index;comment:股票代码"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
Volume int `json:"volume" gorm:"not null;default:0;comment:持仓数量"`
CanUseVolume int `json:"can_use_volume" gorm:"not null;default:0;column:can_use_volume;comment:可用数量"`
FrozenVolume int `json:"frozen_volume" gorm:"not null;default:0;column:frozen_volume;comment:冻结数量"`
AvgPrice float64 `json:"avg_price" gorm:"type:decimal(10,4);not null;default:0;column:avg_price;comment:持仓成本价"`
OpenPrice float64 `json:"open_price" gorm:"type:decimal(10,4);not null;default:0;column:open_price;comment:开仓价格"`
CurrentPrice float64 `json:"current_price" gorm:"type:decimal(10,4);not null;default:0;column:current_price;comment:当前价格"`
MarketValue float64 `json:"market_value" gorm:"type:decimal(15,2);not null;default:0;column:market_value;comment:持仓市值"`
Profit float64 `json:"profit" gorm:"type:decimal(15,2);not null;default:0;comment:持仓盈亏"`
ProfitRate float64 `json:"profit_rate" gorm:"type:decimal(10,4);not null;default:0;column:profit_rate;comment:盈亏比例"`
MinProfitRate float64 `json:"min_profit_rate" gorm:"type:decimal(10,4);not null;default:0;column:min_profit_rate;comment:最低盈亏比例"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
func init() {
database.AppendMigrate(&CollectorPosition{})
}
// TableName 设置表名
func (CollectorPosition) TableName() string {
return "collector_positions"
}

View File

@@ -0,0 +1,48 @@
package models
import (
"time"
"git.apinb.com/bsm-sdk/core/database"
"gorm.io/gorm"
)
// OrderBook 订单簿数据库模型 - 记录完整的买卖交易周期
type OrderBook struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
StockCode string `json:"stock_code" gorm:"type:varchar(20);not null;index;comment:股票代码"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
// 买入信息
BuyOrderID int64 `json:"buy_order_id" gorm:"not null;index;comment:买入订单ID"`
BuyPrice float64 `json:"buy_price" gorm:"type:decimal(10,4);not null;default:0;column:buy_price;comment:买入价格"`
BuyVolume int `json:"buy_volume" gorm:"not null;default:0;column:buy_volume;comment:买入数量"`
BuyTime int64 `json:"buy_time" gorm:"not null;column:buy_time;comment:买入时间戳"`
BuyCollectedAt time.Time `json:"buy_collected_at" gorm:"not null;column:buy_collected_at;comment:买入数据采集时间"`
// 卖出信息 (初始为空,卖出时填充)
SellOrderID *int64 `json:"sell_order_id" gorm:"index;comment:卖出订单ID"`
SellPrice *float64 `json:"sell_price" gorm:"type:decimal(10,4);column:sell_price;comment:卖出价格"`
SellVolume *int `json:"sell_volume" gorm:"column:sell_volume;comment:卖出数量"`
SellTime *int64 `json:"sell_time" gorm:"column:sell_time;comment:卖出时间戳"`
SellCollectedAt *time.Time `json:"sell_collected_at" gorm:"column:sell_collected_at;comment:卖出数据采集时间"`
// 交易结果
IsClosed bool `json:"is_closed" gorm:"not null;default:false;column:is_closed;comment:是否已闭合(卖出)"`
Profit *float64 `json:"profit" gorm:"type:decimal(15,2);column:profit;comment:盈亏金额"`
ProfitRate *float64 `json:"profit_rate" gorm:"type:decimal(10,4);column:profit_rate;comment:盈亏比例"`
// 系统字段
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;comment:记录更新时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
func init() {
database.AppendMigrate(&OrderBook{})
}
func (OrderBook) TableName() string {
return "order_books"
}

View File

@@ -1,100 +0,0 @@
package models
import (
"time"
"gorm.io/gorm"
)
// AssetSnapshot 资产快照数据库模型
type CollectorAssets struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
Cash float64 `json:"cash" gorm:"type:decimal(15,2);not null;default:0;comment:可用资金"`
FrozenCash float64 `json:"frozen_cash" gorm:"type:decimal(15,2);not null;default:0;column:frozen_cash;comment:冻结资金"`
MarketValue float64 `json:"market_value" gorm:"type:decimal(15,2);not null;default:0;column:market_value;comment:持仓市值"`
Profit float64 `json:"profit" gorm:"type:decimal(15,2);not null;default:0;comment:当日盈亏"`
TotalAsset float64 `json:"total_asset" gorm:"type:decimal(15,2);not null;default:0;column:total_asset;comment:总资产"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
// OrderRecord 订单数据库模型
type CollectorOrder struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
OrderID int64 `json:"order_id" gorm:"not null;index;comment:订单ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
StockCode string `json:"stock_code" gorm:"type:varchar(20);not null;index;comment:股票代码"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
Price float64 `json:"price" gorm:"type:decimal(10,4);not null;default:0;comment:委托价格"`
Volume int `json:"volume" gorm:"not null;default:0;comment:委托数量"`
OpenPrice float64 `json:"open_price" gorm:"type:decimal(10,4);not null;default:0;column:open_price;comment:开仓价格"`
TradedPrice float64 `json:"traded_price" gorm:"type:decimal(10,4);not null;default:0;column:traded_price;comment:成交均价"`
TradedVolume int `json:"traded_volume" gorm:"not null;default:0;column:traded_volume;comment:成交数量"`
OrderStatus int `json:"order_status" gorm:"not null;default:0;column:order_status;comment:订单状态"`
OffsetFlag int `json:"offset_flag" gorm:"not null;default:0;column:offset_flag;comment:开平标志"`
OrderTime int64 `json:"order_time" gorm:"not null;column:order_time;comment:下单时间戳"`
OrderRemark string `json:"order_remark" gorm:"type:text;column:order_remark;comment:订单备注"`
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null;comment:数据哈希值(用于变化检测)"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
// PositionRecord 持仓数据库模型
type CollectorPosition struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
AccountID string `json:"account_id" gorm:"type:varchar(50);not null;index;comment:账户ID"`
Code string `json:"code" gorm:"type:varchar(20);not null;index;comment:股票代码"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
Volume int `json:"volume" gorm:"not null;default:0;comment:持仓数量"`
CanUseVolume int `json:"can_use_volume" gorm:"not null;default:0;column:can_use_volume;comment:可用数量"`
FrozenVolume int `json:"frozen_volume" gorm:"not null;default:0;column:frozen_volume;comment:冻结数量"`
AvgPrice float64 `json:"avg_price" gorm:"type:decimal(10,4);not null;default:0;column:avg_price;comment:持仓成本价"`
OpenPrice float64 `json:"open_price" gorm:"type:decimal(10,4);not null;default:0;column:open_price;comment:开仓价格"`
CurrentPrice float64 `json:"current_price" gorm:"type:decimal(10,4);not null;default:0;column:current_price;comment:当前价格"`
MarketValue float64 `json:"market_value" gorm:"type:decimal(15,2);not null;default:0;column:market_value;comment:持仓市值"`
Profit float64 `json:"profit" gorm:"type:decimal(15,2);not null;default:0;comment:持仓盈亏"`
ProfitRate float64 `json:"profit_rate" gorm:"type:decimal(10,4);not null;default:0;column:profit_rate;comment:盈亏比例"`
MinProfitRate float64 `json:"min_profit_rate" gorm:"type:decimal(10,4);not null;default:0;column:min_profit_rate;comment:最低盈亏比例"`
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null;comment:数据哈希值(用于变化检测)"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
// TickRecord 行情数据库模型
type CollectorTick struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
StockCode string `json:"stock_code" gorm:"type:varchar(20);not null;index;comment:股票代码"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
LastPrice float64 `json:"last_price" gorm:"type:decimal(10,4);not null;default:0;column:last_price;comment:最新成交价"`
Open float64 `json:"open" gorm:"type:decimal(10,4);not null;default:0;comment:开盘价"`
High float64 `json:"high" gorm:"type:decimal(10,4);not null;default:0;comment:最高价"`
Low float64 `json:"low" gorm:"type:decimal(10,4);not null;default:0;comment:最低价"`
LastClose float64 `json:"last_close" gorm:"type:decimal(10,4);not null;default:0;column:last_close;comment:昨收盘价"`
Volume int64 `json:"volume" gorm:"not null;default:0;comment:成交量(股)"`
Amount float64 `json:"amount" gorm:"type:decimal(15,2);not null;default:0;comment:成交额(元)"`
PVolume int64 `json:"pvolume" gorm:"not null;default:0;column:pvolume;comment:累积成交量"`
Time int64 `json:"time" gorm:"not null;index;comment:行情时间戳"`
TimeTag string `json:"timetag" gorm:"type:varchar(50);column:timetag;comment:时间标签(格式化时间字符串)"`
StockStatus int `json:"stock_status" gorm:"not null;default:0;column:stock_status;comment:股票状态"`
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null;comment:数据哈希值(用于变化检测)"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
// CollectionLog 采集日志数据库模型
type CollectorLog struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
DataHash string `json:"data_hash" gorm:"type:varchar(64);not null;index;comment:数据哈希值(用于变化检测)"`
Ymd int `json:"ymd" gorm:"not null;index;comment:采集日期年月日数字格式如20260407"`
HasChanged bool `json:"has_changed" gorm:"not null;default:false;column:has_changed;comment:数据是否发生变化"`
StatusMessage string `json:"status_message" gorm:"type:text;column:status_message;comment:状态消息(成功/失败信息)"`
CollectedAt time.Time `json:"collected_at" gorm:"not null;index;comment:数据采集时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:记录创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}