mirror of
https://github.com/injoyai/tdx.git
synced 2025-11-26 21:25:35 +08:00
Compare commits
7 Commits
530de4fa5a
...
v0.0.49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eeab6f533 | ||
|
|
5fd492e881 | ||
|
|
fcb6c995ad | ||
|
|
e6411858e9 | ||
|
|
f4b2497e92 | ||
|
|
e250223e57 | ||
|
|
d19cfb4416 |
1
codes.go
1
codes.go
@@ -20,7 +20,6 @@ type ICodes interface {
|
||||
GetName(code string) string
|
||||
GetStocks(limit ...int) []string
|
||||
GetETFs(limit ...int) []string
|
||||
Update() error
|
||||
}
|
||||
|
||||
// DefaultCodes 增加单例,部分数据需要通过Codes里面的信息计算
|
||||
|
||||
92
codes_v2.go
92
codes_v2.go
@@ -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
16
example/Codes2/main.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
65
manage.go
65
manage.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ type Kline struct {
|
||||
High Price //最高价
|
||||
Low Price //最低价
|
||||
Close Price //收盘价,如果是当天,则是最新价/实时价
|
||||
Order int //成交单数,不一定有值
|
||||
Volume int64 //成交量
|
||||
Amount Price //成交额
|
||||
Time time.Time //时间
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user