Compare commits

..

7 Commits

Author SHA1 Message Date
injoyai
8eeab6f533 增加Trades生成Kline的单数字段,当天数据才有效 2025-11-17 11:05:35 +08:00
injoyai
5fd492e881 优化Codes2 2025-11-17 11:04:55 +08:00
injoyai
fcb6c995ad 优化Codes2 2025-11-17 09:48:20 +08:00
injoyai
e6411858e9 定义ICodes接口 2025-11-17 09:48:06 +08:00
injoyai
f4b2497e92 把manage中的Codes改成接口 2025-11-17 09:47:29 +08:00
injoyai
e250223e57 增加codes2的示例 2025-11-17 09:46:58 +08:00
injoyai
d19cfb4416 优化gbbq 2025-11-17 09:45:33 +08:00
8 changed files with 170 additions and 29 deletions

View File

@@ -20,7 +20,6 @@ type ICodes interface {
GetName(code string) string
GetStocks(limit ...int) []string
GetETFs(limit ...int) []string
Update() error
}
// DefaultCodes 增加单例,部分数据需要通过Codes里面的信息计算

View File

@@ -8,9 +8,11 @@ import (
"github.com/injoyai/ios"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/internal/gbbq"
"github.com/injoyai/tdx/internal/xorms"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"os"
"path/filepath"
"time"
"xorm.io/xorm"
@@ -18,9 +20,15 @@ import (
type Codes2Option func(*Codes2)
func WithFilename(filename string) Codes2Option {
func WithDBFilename(filename string) Codes2Option {
return func(c *Codes2) {
c.filename = filename
c.dbFilename = filename
}
}
func WithTempDir(dir string) Codes2Option {
return func(c *Codes2) {
c.tempDir = dir
}
}
@@ -30,9 +38,9 @@ func WithSpec(spec string) Codes2Option {
}
}
func WithKey(key string) Codes2Option {
func WithUpdateKey(key string) Codes2Option {
return func(c *Codes2) {
c.key = key
c.updateKey = key
}
}
@@ -42,30 +50,55 @@ func WithRetry(retry int) Codes2Option {
}
}
func WithClient(c *Client) Codes2Option {
return func(cs *Codes2) {
cs.c = c
}
}
func WithDial(dial ios.DialFunc, op ...client.Option) Codes2Option {
return func(c *Codes2) {
c.dial = dial
c.dialOption = op
}
}
func WithDialOption(op ...client.Option) Codes2Option {
return func(c *Codes2) {
c.dialOption = op
}
}
func NewCodes2(op ...Codes2Option) (*Codes2, error) {
cs := &Codes2{
filename: filepath.Join(DefaultDatabaseDir, "codes.db"),
dbFilename: filepath.Join(DefaultDatabaseDir, "codes2.db"),
tempDir: filepath.Join(DefaultDataDir, "temp"),
spec: "10 0 9 * * *",
key: "codes",
updateKey: "codes",
retry: 3,
dial: NewRangeDial(Hosts),
dialOption: nil,
m: maps.NewGeneric[string, *CodeModel](),
}
for _, o := range op {
o(cs)
}
os.MkdirAll(cs.tempDir, 0777)
var err error
// 初始化连接
cs.c, err = DialWith(cs.dial, cs.dialOption...)
if err != nil {
return nil, err
if cs.c == nil {
cs.c, err = DialWith(cs.dial, cs.dialOption...)
if err != nil {
return nil, err
}
}
// 初始化数据库
cs.db, err = xorms.NewSqlite(cs.filename)
cs.db, err = xorms.NewSqlite(cs.dbFilename)
if err != nil {
return nil, err
}
@@ -103,9 +136,10 @@ func NewCodes2(op ...Codes2Option) (*Codes2, error) {
var _ ICodes = &Codes2{}
type Codes2 struct {
filename string //数据库文件
dbFilename string //数据库文件
tempDir string //临时目录
spec string //定时规则
key string //标识
updateKey string //标识
retry int //重试次数
dial ios.DialFunc //连接
dialOption []client.Option //
@@ -147,11 +181,11 @@ func (this *Codes2) GetETFs(limit ...int) []string {
func (this *Codes2) updated() (bool, error) {
update := new(UpdateModel)
{ //查询或者插入一条数据
has, err := this.db.Where("`Key`=?", this.key).Get(update)
has, err := this.db.Where("`Key`=?", this.updateKey).Get(update)
if err != nil {
return true, err
} else if !has {
update.Key = this.key
update.Key = this.updateKey
if _, err = this.db.Insert(update); err != nil {
return true, err
}
@@ -188,6 +222,7 @@ func (this *Codes2) Update() error {
etfs := []string(nil)
for _, v := range codes {
fullCode := v.FullCode()
this.m.Set(fullCode, v)
switch {
case protocol.IsStock(fullCode):
stocks = append(stocks, fullCode)
@@ -255,7 +290,32 @@ func (this *Codes2) update() ([]*CodeModel, error) {
}
}
//4. 插入或者更新数据库
//4. 获取gbbq
ss, err := gbbq.DownloadAndDecode(this.tempDir)
if err != nil {
logs.Err(err)
return nil, err
}
mStock := map[string]gbbq.Stock{}
for _, v := range ss {
mStock[protocol.AddPrefix(v.Code)] = v
}
//5. 赋值流通股和总股本
for _, v := range insert {
if protocol.IsStock(v.FullCode()) {
v.FloatStock, v.TotalStock = ss.GetStock(v.Code)
}
}
for _, v := range update {
if stock, ok := mStock[v.FullCode()]; ok {
v.FloatStock = stock.Float
v.TotalStock = stock.Total
}
}
//6. 插入或者更新数据库
err = this.db.SessionFunc(func(session *xorm.Session) error {
for _, v := range mCode {
if _, err = session.Where("Exchange=? and Code=? ", v.Exchange, v.Code).Delete(v); err != nil {
@@ -279,6 +339,6 @@ func (this *Codes2) update() ([]*CodeModel, error) {
}
//更新时间
_, err = this.db.Where("`Key`=?", this.key).Update(&UpdateModel{Time: time.Now().Unix()})
_, err = this.db.Where("`Key`=?", this.updateKey).Update(&UpdateModel{Time: time.Now().Unix()})
return list, err
}

16
example/Codes2/main.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import (
"fmt"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
cs, err := tdx.NewCodes2()
logs.PanicErr(err)
c := cs.Get("sz000001")
fmt.Println(c.FloatStock, c.TotalStock)
}

View File

@@ -8,13 +8,17 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetTrade("sz000001", 0, 20)
resp, err := c.GetTrade("sz000001", 0, 200)
logs.PanicErr(err)
for _, v := range resp.List {
logs.Debug(v)
}
for _, v := range resp.List.Klines() {
logs.Debug(v, v.Order)
}
logs.Debug("总数:", resp.Count)
})
}

View File

@@ -26,7 +26,7 @@ var HexKeys = "38 A7 C2 1D E0 6A 17 E2 D1 39 A2 40 9C BA 46 AF 42 C6 FF 05 74 EA
const ZipURL = "http://www.tdx.com.cn/products/data/data/dbf/gbbq.zip"
func DownloadAndDecode(dir string) ([]Stock, error) {
func DownloadAndDecode(dir string) (Stocks, error) {
filename, err := Download(dir)
if err != nil {
return nil, err
@@ -57,11 +57,13 @@ func Download(dir string) (string, error) {
if err != nil {
return "", err
}
err = zip.Decode(zipFilename, dir)
decodeDir := filepath.Join(dir, "gbbq")
os.MkdirAll(decodeDir, 0777)
err = zip.Decode(zipFilename, decodeDir)
return filepath.Join(dir, "gbbq", "gbbq"), err
}
func Decode(content []byte) ([]Stock, error) {
func Decode(content []byte) (Stocks, error) {
hexStr := strings.ReplaceAll(HexKeys, " ", "")
keys, err := hex.DecodeString(hexStr)
if err != nil {
@@ -173,6 +175,7 @@ type Stock struct {
type Stocks []Stock
// GetStock 输入920000,返回流通股本
func (this Stocks) GetStock(code string) (float float64, total float64) {
ls := types.List[Stock](this)
ls = ls.Where(func(i int, v Stock) bool {
@@ -183,10 +186,12 @@ func (this Stocks) GetStock(code string) (float float64, total float64) {
return false
})
ls = ls.Sort(func(a, b Stock) bool {
return a.Date.Unix() < b.Date.Unix()
return a.Date.Unix() > b.Date.Unix()
})
if len(ls) > 0 {
return ls[0].Float, ls[0].Total
for _, v := range ls {
if v.Float > 0 && v.Total > 0 {
return v.Float, v.Total
}
}
return 0, 0
}

View File

@@ -4,10 +4,12 @@ import (
"errors"
"github.com/injoyai/ios/client"
"github.com/robfig/cron/v3"
"sync"
"time"
)
const (
DefaultDataDir = "./data"
DefaultDatabaseDir = "./data/database"
)
@@ -58,7 +60,6 @@ func NewManageMysql(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
Config: cfg,
Codes: codes,
Workday: workday,
Cron: cron.New(cron.WithSeconds()),
}, nil
}
@@ -109,16 +110,66 @@ func NewManage(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
Config: cfg,
Codes: codes,
Workday: workday,
Cron: cron.New(cron.WithSeconds()),
}, nil
}
func NewManage2(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
//初始化配置
if cfg == nil {
cfg = &ManageConfig{}
}
if cfg.CodesFilename == "" {
cfg.CodesFilename = DefaultDatabaseDir + "/codes2.db"
}
if cfg.WorkdayFileName == "" {
cfg.WorkdayFileName = DefaultDatabaseDir + "/workday.db"
}
if cfg.Dial == nil {
cfg.Dial = DialDefault
}
//通用客户端
commonClient, err := cfg.Dial(op...)
if err != nil {
return nil, err
}
commonClient.Wait.SetTimeout(time.Second * 5)
//代码管理
codes, err := NewCodes2(WithClient(commonClient), WithDBFilename(cfg.CodesFilename))
if err != nil {
return nil, err
}
//工作日管理
workday, err := NewWorkdaySqlite(commonClient, cfg.WorkdayFileName)
if err != nil {
return nil, err
}
//连接池
p, err := NewPool(func() (*Client, error) {
return cfg.Dial(op...)
}, cfg.Number)
if err != nil {
return nil, err
}
return &Manage{
Pool: p,
Config: cfg,
Codes: codes,
Workday: workday,
}, nil
}
type Manage struct {
*Pool
Config *ManageConfig
Codes *Codes
Codes ICodes
Workday *Workday
Cron *cron.Cron
cron *cron.Cron
once sync.Once
}
// RangeStocks 遍历所有股票
@@ -137,7 +188,11 @@ func (this *Manage) RangeETFs(f func(code string)) {
// AddWorkdayTask 添加工作日任务
func (this *Manage) AddWorkdayTask(spec string, f func(m *Manage)) {
this.Cron.AddFunc(spec, func() {
this.once.Do(func() {
this.cron = cron.New(cron.WithSeconds())
this.cron.Start()
})
this.cron.AddFunc(spec, func() {
if this.Workday.TodayIs() {
f(this)
}

View File

@@ -44,6 +44,7 @@ type Kline struct {
High Price //最高价
Low Price //最低价
Close Price //收盘价,如果是当天,则是最新价/实时价
Order int //成交单数,不一定有值
Volume int64 //成交量
Amount Price //成交额
Time time.Time //时间

View File

@@ -175,6 +175,7 @@ func (this Trades) Kline(t time.Time, last Price) *Kline {
}
k.Close = v.Price
k.Volume += int64(v.Volume)
k.Order += v.Number
k.Amount += v.Price * Price(v.Volume) * 100
first++
}