diff --git a/README.md b/README.md index 250c06e..73df721 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ QMT数据采集器是一个专门用于从QMT量化交易平台采集实时交 - **订单状态跟踪**:持续监控股票订单的状态变化,包括已成交、未成交、部分成交等各种状态 - **持仓信息管理**:记录每只股票的持仓数量、可用数量、成本价、当前价及盈亏情况 - **行情数据捕获**:收集股票的实时Tick数据,包括买卖五档价格、成交量、成交额等市场深度信息 +- **订单簿追踪**:自动追踪完整的交易周期,从买入到卖出形成闭合的交易记录,计算实际盈亏 ### 2. 智能变化检测 - 采用SHA256哈希算法对采集的数据进行指纹计算 diff --git a/cmd/main.go b/cmd/main.go index 29c590e..1cedfa4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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() } diff --git a/go.mod b/go.mod index 35aed3a..f8f5c2e 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 24744fe..85d1fda 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/impl/impl.go b/internal/impl/impl.go new file mode 100644 index 0000000..45a3337 --- /dev/null +++ b/internal/impl/impl.go @@ -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) +} diff --git a/internal/logic/boot.go b/internal/logic/boot.go new file mode 100644 index 0000000..94016d5 --- /dev/null +++ b/internal/logic/boot.go @@ -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)) +} diff --git a/collector/collector.go b/internal/logic/collector.go similarity index 97% rename from collector/collector.go rename to internal/logic/collector.go index 507a382..257f116 100644 --- a/collector/collector.go +++ b/internal/logic/collector.go @@ -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 数据采集器 diff --git a/storage/storage.go b/internal/logic/storage.go similarity index 57% rename from storage/storage.go rename to internal/logic/storage.go index 0205eeb..5428921 100644 --- a/storage/storage.go +++ b/internal/logic/storage.go @@ -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 -} diff --git a/collector/utils.go b/internal/logic/utils.go similarity index 98% rename from collector/utils.go rename to internal/logic/utils.go index 288efaf..738f0be 100644 --- a/collector/utils.go +++ b/internal/logic/utils.go @@ -1,4 +1,4 @@ -package collector +package logic import ( "time" diff --git a/internal/models/collect_assets.go b/internal/models/collect_assets.go new file mode 100644 index 0000000..3ae9b73 --- /dev/null +++ b/internal/models/collect_assets.go @@ -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" +} diff --git a/internal/models/collect_orders.go b/internal/models/collect_orders.go new file mode 100644 index 0000000..7c51064 --- /dev/null +++ b/internal/models/collect_orders.go @@ -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" +} diff --git a/internal/models/collect_positions.go b/internal/models/collect_positions.go new file mode 100644 index 0000000..518148e --- /dev/null +++ b/internal/models/collect_positions.go @@ -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" +} diff --git a/internal/models/order_book.go b/internal/models/order_book.go new file mode 100644 index 0000000..29982fa --- /dev/null +++ b/internal/models/order_book.go @@ -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" +} diff --git a/types/req.go b/internal/types/req.go similarity index 100% rename from types/req.go rename to internal/types/req.go diff --git a/models/models.go b/models/models.go deleted file mode 100644 index 8640d5e..0000000 --- a/models/models.go +++ /dev/null @@ -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:软删除时间"` -}