review code.
This commit is contained in:
@@ -11,6 +11,7 @@ QMT数据采集器是一个专门用于从QMT量化交易平台采集实时交
|
||||
- **订单状态跟踪**:持续监控股票订单的状态变化,包括已成交、未成交、部分成交等各种状态
|
||||
- **持仓信息管理**:记录每只股票的持仓数量、可用数量、成本价、当前价及盈亏情况
|
||||
- **行情数据捕获**:收集股票的实时Tick数据,包括买卖五档价格、成交量、成交额等市场深度信息
|
||||
- **订单簿追踪**:自动追踪完整的交易周期,从买入到卖出形成闭合的交易记录,计算实际盈亏
|
||||
|
||||
### 2. 智能变化检测
|
||||
- 采用SHA256哈希算法对采集的数据进行指纹计算
|
||||
|
||||
135
cmd/main.go
135
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()
|
||||
}
|
||||
|
||||
34
go.mod
34
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
|
||||
)
|
||||
|
||||
88
go.sum
88
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=
|
||||
|
||||
24
internal/impl/impl.go
Normal file
24
internal/impl/impl.go
Normal 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
100
internal/logic/boot.go
Normal 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))
|
||||
}
|
||||
@@ -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 数据采集器
|
||||
@@ -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,
|
||||
// processOrderBook 处理订单簿逻辑
|
||||
func processOrderBook(accountID string, order types.Order, ymd int, now time.Time, openPrice float64) error {
|
||||
// OffsetFlag: 通常 0=买入, 1=卖出 (需要根据实际系统确认)
|
||||
// 这里假设: OffsetFlag == 0 表示买入, OffsetFlag == 1 表示卖出
|
||||
|
||||
if order.OffsetFlag == 0 {
|
||||
// 买入订单 - 创建新的订单簿记录
|
||||
orderBook := models.OrderBook{
|
||||
AccountID: accountID,
|
||||
StockCode: order.StockCode,
|
||||
Ymd: ymd,
|
||||
HasChanged: hasChanged,
|
||||
StatusMessage: statusMessage,
|
||||
CollectedAt: time.Now(),
|
||||
BuyOrderID: order.OrderID,
|
||||
BuyPrice: order.TradedPrice,
|
||||
BuyVolume: order.TradedVolume,
|
||||
BuyTime: order.OrderTime,
|
||||
BuyCollectedAt: now,
|
||||
IsClosed: false,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&log).Error; err != nil {
|
||||
return fmt.Errorf("保存采集日志失败: %w", err)
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package collector
|
||||
package logic
|
||||
|
||||
import (
|
||||
"time"
|
||||
32
internal/models/collect_assets.go
Normal file
32
internal/models/collect_assets.go
Normal 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"
|
||||
}
|
||||
37
internal/models/collect_orders.go
Normal file
37
internal/models/collect_orders.go
Normal 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"
|
||||
}
|
||||
38
internal/models/collect_positions.go
Normal file
38
internal/models/collect_positions.go
Normal 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"
|
||||
}
|
||||
48
internal/models/order_book.go
Normal file
48
internal/models/order_book.go
Normal 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"
|
||||
}
|
||||
100
models/models.go
100
models/models.go
@@ -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:软删除时间"`
|
||||
}
|
||||
Reference in New Issue
Block a user