Compare commits

...

25 Commits

Author SHA1 Message Date
injoyai
36a8479aa4 优化codes-server,待完成 2025-11-26 17:00:05 +08:00
injoyai
0d1e6b6b51 修复指数判断不全的问题 2025-11-26 16:49:11 +08:00
injoyai
6fd178245a 增加LICENSE文件 2025-11-26 15:41:18 +08:00
injoyai
0efd0735e6 Merge pull request #27 from jingmian/patch-1
Update main.go
2025-11-25 19:08:44 +08:00
镜面王子
b01da71236 Update main.go
NewManageMysql函数不接受*ManageConfig类型参数,而是接受Option函数类型参数
2025-11-25 17:34:49 +08:00
injoyai
26e2479e2f 细节优化 2025-11-21 16:08:46 +08:00
injoyai
6d0125afef 优化manage,修改成Option的方式,老版本的名称从NewManage改成MewManageSqlite 2025-11-21 14:17:09 +08:00
injoyai
2d77d769fd 增加Codes2的Option名称前缀 2025-11-21 14:14:54 +08:00
injoyai
33627c3d6c 增加新版迭代器,Iter和IterYear 2025-11-21 14:13:40 +08:00
injoyai
1ff1ceb8d7 开放interval包,命名为lib 2025-11-21 08:44:37 +08:00
injoyai
233d1b689e 更新etf的判断 2025-11-20 09:31:08 +08:00
injoyai
2a27eea873 增加指数代码的判断 2025-11-20 09:08:22 +08:00
injoyai
fcfb329712 增加指数代码的判断 2025-11-20 08:56:53 +08:00
injoyai
ed2c814fab 增加指数分钟k线的方法 2025-11-19 15:57:02 +08:00
injoyai
fc04b5042a 重新定义接口 2025-11-17 15:48:57 +08:00
injoyai
b2a4c00253 重新定义接口 2025-11-17 15:48:35 +08:00
injoyai
c68c7582bc go版本升级到1.23 2025-11-17 15:40:41 +08:00
injoyai
4e62ee1c5e 更新文档 2025-11-17 14:44:25 +08:00
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
30 changed files with 767 additions and 218 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 injoyai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,9 +1,8 @@
### 说明
* 参考golang库 [`https://github.com/bensema/gotdx`](https://github.com/bensema/gotdx)
* 参考python库 [`https://github.com/mootdx/mootdx`](https://github.com/mootdx/mootdx)
* 数据入库示例(开发中) [`https://github.com/injoyai/stock`](https://github.com/injoyai/stock)
* 参考 [`https://github.com/bensema/gotdx`](https://github.com/bensema/gotdx)
* 参考 [`https://github.com/mootdx/mootdx`](https://github.com/mootdx/mootdx)
* 参考 [`https://github.com/jing2uo/tdx2db`](https://github.com/jing2uo/tdx2db)
### 如何使用

View File

@@ -3,6 +3,10 @@ package tdx
import (
"errors"
"fmt"
"runtime/debug"
"sync/atomic"
"time"
"github.com/injoyai/base/maps"
"github.com/injoyai/base/maps/wait"
"github.com/injoyai/conv"
@@ -10,11 +14,8 @@ import (
"github.com/injoyai/ios/client"
"github.com/injoyai/ios/module/common"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/internal/bse"
"github.com/injoyai/tdx/lib/bse"
"github.com/injoyai/tdx/protocol"
"runtime/debug"
"sync/atomic"
"time"
)
const (
@@ -560,6 +561,26 @@ func (this *Client) GetIndexAll(Type uint8, code string) (*protocol.KlineResp, e
return this.GetIndexUntil(Type, code, func(k *protocol.Kline) bool { return false })
}
func (this *Client) GetIndexMinute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKlineMinute, code, start, count)
}
func (this *Client) GetIndex5Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline5Minute, code, start, count)
}
func (this *Client) GetIndex15Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline15Minute, code, start, count)
}
func (this *Client) GetIndex30Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline30Minute, code, start, count)
}
func (this *Client) GetIndex60Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline60Minute, code, start, count)
}
func (this *Client) GetIndexDay(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKlineDay, code, start, count)
}

View File

@@ -2,25 +2,31 @@ package tdx
import (
"errors"
"iter"
"math"
"os"
"path/filepath"
"time"
"github.com/injoyai/conv"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"math"
"os"
"path/filepath"
"time"
"xorm.io/core"
"xorm.io/xorm"
)
type ICodes interface {
Iter() iter.Seq2[string, *CodeModel]
Get(code string) *CodeModel
GetName(code string) string
GetStocks(limit ...int) []string
GetETFs(limit ...int) []string
Update() error
GetStocks(limit ...int) CodeModels
GetStockCodes(limit ...int) []string
GetETFs(limit ...int) CodeModels
GetETFCodes(limit ...int) []string
GetIndexes(limits ...int) CodeModels
GetIndexCodes(limits ...int) []string
}
// DefaultCodes 增加单例,部分数据需要通过Codes里面的信息计算
@@ -131,6 +137,8 @@ func NewCodes(c *Client, db *xorm.Engine) (*Codes, error) {
return cc, cc.Update(true)
}
var _ ICodes = &Codes{}
type Codes struct {
*Client //客户端
db *xorm.Engine //数据库实例
@@ -139,6 +147,20 @@ type Codes struct {
exchanges map[string][]string //交易所缓存
}
func (this *Codes) Get(code string) *CodeModel {
return this.Map[code]
}
func (this *Codes) Iter() iter.Seq2[string, *CodeModel] {
return func(yield func(string, *CodeModel) bool) {
for _, code := range this.list {
if !yield(code.FullCode(), code) {
break
}
}
}
}
// GetName 获取股票名称
func (this *Codes) GetName(code string) string {
if v, ok := this.Map[code]; ok {
@@ -148,13 +170,13 @@ func (this *Codes) GetName(code string) string {
}
// GetStocks 获取股票代码,sh6xxx sz0xx sz30xx
func (this *Codes) GetStocks(limits ...int) []string {
func (this *Codes) GetStocks(limits ...int) CodeModels {
limit := conv.Default(-1, limits...)
ls := []string(nil)
ls := []*CodeModel(nil)
for _, m := range this.list {
code := m.FullCode()
if protocol.IsStock(code) {
ls = append(ls, code)
ls = append(ls, m)
}
if limit > 0 && len(ls) >= limit {
break
@@ -163,14 +185,18 @@ func (this *Codes) GetStocks(limits ...int) []string {
return ls
}
func (this *Codes) GetStockCodes(limits ...int) []string {
return this.GetStocks(limits...).Codes()
}
// GetETFs 获取基金代码,sz159xxx,sh510xxx,sh511xxx
func (this *Codes) GetETFs(limits ...int) []string {
func (this *Codes) GetETFs(limits ...int) CodeModels {
limit := conv.Default(-1, limits...)
ls := []string(nil)
ls := []*CodeModel(nil)
for _, m := range this.list {
code := m.FullCode()
if protocol.IsETF(code) {
ls = append(ls, code)
ls = append(ls, m)
}
if limit > 0 && len(ls) >= limit {
break
@@ -179,8 +205,29 @@ func (this *Codes) GetETFs(limits ...int) []string {
return ls
}
func (this *Codes) Get(code string) *CodeModel {
return this.Map[code]
// GetETFCodes 获取基金代码,sz159xxx,sh510xxx,sh511xxx
func (this *Codes) GetETFCodes(limits ...int) []string {
return this.GetETFs(limits...).Codes()
}
// GetIndexes 获取基金代码,sz159xxx,sh510xxx,sh511xxx
func (this *Codes) GetIndexes(limits ...int) CodeModels {
limit := conv.Default(-1, limits...)
ls := []*CodeModel(nil)
for _, m := range this.list {
code := m.FullCode()
if protocol.IsIndex(code) {
ls = append(ls, m)
}
if limit > 0 && len(ls) >= limit {
break
}
}
return ls
}
func (this *Codes) GetIndexCodes(limits ...int) []string {
return this.GetIndexes(limits...).Codes()
}
func (this *Codes) AddExchange(code string) string {
@@ -368,3 +415,13 @@ func NewSessionFunc(db *xorm.Engine, fn func(session *xorm.Session) error) error
}
return nil
}
type CodeModels []*CodeModel
func (this CodeModels) Codes() []string {
codes := make([]string, len(this))
for i, v := range this {
codes[i] = v.FullCode()
}
return codes
}

View File

@@ -2,70 +2,105 @@ package tdx
import (
"errors"
"iter"
"os"
"path/filepath"
"time"
"github.com/injoyai/base/maps"
"github.com/injoyai/base/types"
"github.com/injoyai/conv"
"github.com/injoyai/ios"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/internal/xorms"
"github.com/injoyai/tdx/lib/gbbq"
"github.com/injoyai/tdx/lib/xorms"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"path/filepath"
"time"
"xorm.io/xorm"
)
type Codes2Option func(*Codes2)
func WithFilename(filename string) Codes2Option {
func WithCodes2Database(filename string) Codes2Option {
return func(c *Codes2) {
c.filename = filename
c.dbFilename = filename
}
}
func WithSpec(spec string) Codes2Option {
func WithCodes2TempDir(dir string) Codes2Option {
return func(c *Codes2) {
c.tempDir = dir
}
}
func WithCodes2Spec(spec string) Codes2Option {
return func(c *Codes2) {
c.spec = spec
}
}
func WithKey(key string) Codes2Option {
func WithCodes2UpdateKey(key string) Codes2Option {
return func(c *Codes2) {
c.key = key
c.updateKey = key
}
}
func WithRetry(retry int) Codes2Option {
func WithCodes2Retry(retry int) Codes2Option {
return func(c *Codes2) {
c.retry = retry
}
}
func WithCodes2Client(c *Client) Codes2Option {
return func(cs *Codes2) {
cs.c = c
}
}
func WithCodes2Dial(dial ios.DialFunc, op ...client.Option) Codes2Option {
return func(c *Codes2) {
c.dial = dial
c.dialOption = op
}
}
func WithCodes2DialOption(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",
retry: 3,
updateKey: "codes",
retry: DefaultRetry,
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
}
@@ -82,7 +117,7 @@ func NewCodes2(op ...Codes2Option) (*Codes2, error) {
// 定时更新
cr := cron.New(cron.WithSeconds())
_, err = cr.AddFunc(cs.spec, func() {
for i := 0; i < 3; i++ {
for i := 0; i == 0 || i < cs.retry; i++ {
if err := cs.Update(); err != nil {
logs.Err(err)
<-time.After(time.Minute * 5)
@@ -103,9 +138,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 //
@@ -114,11 +150,13 @@ type Codes2 struct {
内部字段
*/
c *Client //
db *xorms.Engine //
stocks types.List[string] //缓存
etfs types.List[string] //缓存
m *maps.Generic[string, *CodeModel] //缓存
c *Client //
db *xorms.Engine //
stocks types.List[*CodeModel] //股票缓存
etfs types.List[*CodeModel] //etf缓存
indexes types.List[*CodeModel] //指数缓存
all types.List[*CodeModel] //全部缓存
m *maps.Generic[string, *CodeModel] //缓存
}
func (this *Codes2) Get(code string) *CodeModel {
@@ -126,6 +164,16 @@ func (this *Codes2) Get(code string) *CodeModel {
return v
}
func (this *Codes2) Iter() iter.Seq2[string, *CodeModel] {
return func(yield func(string, *CodeModel) bool) {
for _, code := range this.all {
if !yield(code.FullCode(), code) {
break
}
}
}
}
func (this *Codes2) GetName(code string) string {
v, _ := this.m.Get(code)
if v == nil {
@@ -134,24 +182,41 @@ func (this *Codes2) GetName(code string) string {
return v.Name
}
func (this *Codes2) GetStocks(limit ...int) []string {
func (this *Codes2) GetStocks(limit ...int) CodeModels {
size := conv.Default(this.stocks.Len(), limit...)
return this.stocks.Limit(size)
return CodeModels(this.stocks.Limit(size))
}
func (this *Codes2) GetETFs(limit ...int) []string {
func (this *Codes2) GetStockCodes(limit ...int) []string {
return this.GetStocks(limit...).Codes()
}
func (this *Codes2) GetETFs(limit ...int) CodeModels {
size := conv.Default(this.etfs.Len(), limit...)
return this.etfs.Limit(size)
return CodeModels(this.etfs.Limit(size))
}
func (this *Codes2) GetETFCodes(limit ...int) []string {
return this.GetETFs(limit...).Codes()
}
func (this *Codes2) GetIndexes(limit ...int) CodeModels {
size := conv.Default(this.etfs.Len(), limit...)
return CodeModels(this.indexes.Limit(size))
}
func (this *Codes2) GetIndexCodes(limit ...int) []string {
return this.GetIndexes(limit...).Codes()
}
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
}
@@ -184,20 +249,26 @@ func (this *Codes2) Update() error {
return err
}
stocks := []string(nil)
etfs := []string(nil)
stocks := []*CodeModel(nil)
etfs := []*CodeModel(nil)
indexes := []*CodeModel(nil)
for _, v := range codes {
fullCode := v.FullCode()
this.m.Set(fullCode, v)
switch {
case protocol.IsStock(fullCode):
stocks = append(stocks, fullCode)
stocks = append(stocks, v)
case protocol.IsETF(fullCode):
etfs = append(etfs, fullCode)
etfs = append(etfs, v)
case protocol.IsIndex(fullCode):
indexes = append(indexes, v)
}
}
this.stocks = stocks
this.etfs = etfs
this.indexes = indexes
this.all = codes
return nil
}
@@ -255,7 +326,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 +375,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
}

View File

@@ -1,16 +1,17 @@
package main
import (
"time"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"time"
)
func main() {
m, err := tdx.NewManage(nil)
m, err := tdx.NewManage()
logs.PanicErr(err)
codes := m.Codes.GetStocks()
codes := m.Codes.GetStocks().Codes()
//codes = []string{
// "sz000001",
// "sz000002",

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

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"strings"
"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)
for _, v := range cs.GetIndexes().Codes() {
if strings.HasPrefix(v, "sz") {
logs.Debug(v)
}
}
}

View File

@@ -0,0 +1,29 @@
package main
import (
"strings"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
cs, err := tdx.NewCodes2()
logs.PanicErr(err)
ls := cs.GetETFCodes()
shNumber := 0
szNumber := 0
for _, v := range ls {
switch {
case strings.HasPrefix(v, "sh"):
shNumber++
case strings.HasPrefix(v, "sz"):
szNumber++
}
}
logs.Debug("sh:", shNumber)
logs.Debug("sz:", szNumber)
}

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)
})
}

23
example/Manage/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
m, err := tdx.NewManage()
logs.PanicErr(err)
err = m.Do(func(c *tdx.Client) error {
resp, err := c.GetIndexDayAll("sh000001")
if err != nil {
return err
}
for _, v := range resp.List {
logs.Debug(v)
}
return nil
})
logs.PanicErr(err)
}

View File

@@ -6,12 +6,11 @@ import (
)
func main() {
_, err := tdx.NewManageMysql(&tdx.ManageConfig{
Number: 2,
CodesFilename: "root:root@tcp(192.168.1.105:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local",
WorkdayFileName: "root:root@tcp(192.168.1.105:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local",
Dial: nil,
})
_, err := tdx.NewManageMysql(
tdx.WithClients(2),
tdx.WithCodesDatabase("root:root@tcp(192.168.1.105:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"),
tdx.WithWorkdayDatabase("root:root@tcp(192.168.1.105:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"),
)
logs.PanicErr(err)
logs.Debug("done")
}

View File

@@ -2,16 +2,17 @@ package main
import (
"context"
"path/filepath"
"time"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/extend"
"path/filepath"
"time"
)
func main() {
m, err := tdx.NewManage(nil)
m, err := tdx.NewManage()
logs.PanicErr(err)
err = extend.NewPullKline(extend.PullKlineConfig{

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/extend"
@@ -11,7 +12,7 @@ func main() {
pt := extend.NewPullTrade("./data/trade")
m, err := tdx.NewManage(nil)
m, err := tdx.NewManage()
logs.PanicErr(err)
err = pt.PullYear(context.Background(), m, 2025, "sz000001")

View File

@@ -1,7 +1,7 @@
package extend
import (
"github.com/injoyai/tdx/internal/bse"
"github.com/injoyai/tdx/lib/bse"
)
func GetBjCodes() ([]string, error) {

View File

@@ -3,43 +3,147 @@ package extend
import (
"encoding/json"
"fmt"
"github.com/injoyai/conv"
"github.com/injoyai/tdx"
"io"
"iter"
"net/http"
"path/filepath"
"github.com/injoyai/base/maps"
"github.com/injoyai/conv"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/robfig/cron/v3"
)
func ListenCodesHTTP(port int, filename ...string) error {
code, err := tdx.DialCodes(conv.Default(filepath.Join(tdx.DefaultDatabaseDir, "codes.db"), filename...))
func ListenCodesHTTP(port int, op ...tdx.Codes2Option) error {
code, err := tdx.NewCodes2(op...)
if err != nil {
return nil
}
succ := func(w http.ResponseWriter, data any) {
w.WriteHeader(http.StatusOK)
w.Write(conv.Bytes(data))
}
return http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/all":
case "/stocks":
ls := code.GetStocks()
w.WriteHeader(http.StatusOK)
w.Write(conv.Bytes(ls))
succ(w, code.GetStocks())
case "/etfs":
ls := code.GetETFs()
w.WriteHeader(http.StatusOK)
w.Write(conv.Bytes(ls))
succ(w, code.GetETFs())
case "/indexes":
succ(w, code.GetIndexes())
default:
http.NotFound(w, r)
}
}))
}
func DialCodesHTTP(address string) *CodesHTTP {
return &CodesHTTP{address: address}
func DialCodesHTTP(address string) (c *CodesHTTP, err error) {
c = &CodesHTTP{address: address}
cr := cron.New(cron.WithSeconds())
_, err = cr.AddFunc("0 20 9 * * *", func() { logs.PrintErr(c.Update()) })
if err != nil {
return
}
err = c.Update()
if err != nil {
return
}
cr.Start()
return c, nil
}
type CodesHTTP struct {
address string
stocks tdx.CodeModels
etfs tdx.CodeModels
indexes tdx.CodeModels
m maps.Generic[string, *tdx.CodeModel]
}
func (this *CodesHTTP) getList(path string) ([]string, error) {
func (this *CodesHTTP) Iter() iter.Seq2[string, *tdx.CodeModel] {
return func(yield func(string, *tdx.CodeModel) bool) {
for _, v := range this.stocks {
if !yield(v.FullCode(), v) {
return
}
}
for _, v := range this.etfs {
if !yield(v.FullCode(), v) {
return
}
}
for _, v := range this.indexes {
if !yield(v.FullCode(), v) {
return
}
}
}
}
func (this *CodesHTTP) Get(code string) *tdx.CodeModel {
return this.m.MustGet(code)
}
func (this *CodesHTTP) GetName(code string) string {
v := this.m.MustGet(code)
if v != nil {
return v.Name
}
return ""
}
func (this *CodesHTTP) GetStocks(limit ...int) tdx.CodeModels {
return this.stocks
}
func (this *CodesHTTP) GetStockCodes(limit ...int) []string {
return this.stocks.Codes()
}
func (this *CodesHTTP) GetETFs(limit ...int) tdx.CodeModels {
return this.etfs
}
func (this *CodesHTTP) GetETFCodes(limit ...int) []string {
return this.etfs.Codes()
}
func (this *CodesHTTP) GetIndexes(limits ...int) tdx.CodeModels {
return this.indexes
}
func (this *CodesHTTP) GetIndexCodes(limits ...int) []string {
return this.indexes.Codes()
}
func (this *CodesHTTP) Update() (err error) {
this.stocks, err = this.getList("/stocks")
if err != nil {
return
}
for _, v := range this.stocks {
this.m.Set(v.FullCode(), v)
}
this.etfs, err = this.getList("/etfs")
if err != nil {
return
}
for _, v := range this.etfs {
this.m.Set(v.FullCode(), v)
}
this.indexes, err = this.getList("/indexes")
if err != nil {
return
}
for _, v := range this.indexes {
this.m.Set(v.FullCode(), v)
}
return
}
func (this *CodesHTTP) getList(path string) (tdx.CodeModels, error) {
resp, err := http.DefaultClient.Get(this.address + path)
if err != nil {
return nil, err
@@ -52,15 +156,7 @@ func (this *CodesHTTP) getList(path string) ([]string, error) {
if err != nil {
return nil, err
}
ls := []string(nil)
ls := tdx.CodeModels{}
err = json.Unmarshal(bs, &ls)
return ls, err
}
func (this *CodesHTTP) GetStocks() ([]string, error) {
return this.getList("/stocks")
}
func (this *CodesHTTP) GetETFs() ([]string, error) {
return this.getList("/etfs")
}

View File

@@ -48,7 +48,7 @@ func (this *PullKlineMysql) Run(ctx context.Context, m *tdx.Manage) error {
//1. 获取所有股票代码
codes := this.Config.Codes
if len(codes) == 0 {
codes = m.Codes.GetStocks()
codes = m.Codes.GetStockCodes()
}
for _, v := range codes {

View File

@@ -110,7 +110,7 @@ func (this *PullKline) Run(ctx context.Context, m *tdx.Manage) error {
//1. 获取所有股票代码
codes := this.Config.Codes
if len(codes) == 0 {
codes = m.Codes.GetStocks()
codes = m.Codes.GetStockCodes()
}
for _, v := range codes {

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/injoyai/tdx
go 1.20
go 1.23
require (
github.com/glebarez/go-sqlite v1.22.0

16
go.sum
View File

@@ -22,12 +22,11 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/injoyai/base v1.2.15 h1:K/ysPqZl7vgNUAz/jpG1IdDpzdSMWvUfoJL+1gPdM9g=
github.com/injoyai/base v1.2.15/go.mod h1:NfCQjml3z2pCvQ3J3YcOXtecqXD0xVPKjo4YTsMLhr8=
github.com/injoyai/base v1.2.17 h1:+qYeCSeEMWgmTla+LBC0Ozan9ysS4mV0ne5nfMt9opU=
github.com/injoyai/base v1.2.17/go.mod h1:NfCQjml3z2pCvQ3J3YcOXtecqXD0xVPKjo4YTsMLhr8=
github.com/injoyai/conv v1.2.5 h1:G4OCyF0NTZul5W1u9IgXDOhW4/zmIigdPKXFHQGmv1M=
@@ -39,6 +38,7 @@ github.com/injoyai/logs v1.0.12/go.mod h1:+dKEL6GvaFqqVRatqUBiCicJbZnAgtj7hVs824
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -50,6 +50,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -84,11 +85,14 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -98,9 +102,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
@@ -114,8 +120,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
@@ -123,10 +132,13 @@ modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWP
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0=

View File

@@ -12,7 +12,7 @@ import (
"encoding/hex"
"fmt"
"github.com/injoyai/base/types"
"github.com/injoyai/tdx/internal/zip"
"github.com/injoyai/tdx/lib/zip"
"io"
"math"
"net/http"
@@ -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
}

300
manage.go
View File

@@ -2,142 +2,258 @@ package tdx
import (
"errors"
"sync"
"github.com/injoyai/conv"
"github.com/injoyai/ios/client"
"github.com/robfig/cron/v3"
"time"
)
const (
DefaultClients = 1
DefaultRetry = 3
DefaultDataDir = "./data"
DefaultDatabaseDir = "./data/database"
)
func NewManageMysql(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
//初始化配置
if cfg == nil {
cfg = &ManageConfig{}
}
if cfg.CodesFilename == "" {
return nil, errors.New("未配置Codes的数据库")
}
if cfg.WorkdayFileName == "" {
return nil, errors.New("未配置Workday的数据库")
}
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 := NewCodesMysql(commonClient, cfg.CodesFilename)
if err != nil {
return nil, err
}
//工作日管理
workday, err := NewWorkdayMysql(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,
Cron: cron.New(cron.WithSeconds()),
}, nil
func NewManageMysql(op ...Option) (*Manage, error) {
return NewManage(
WithOptions(op...),
WithDialCodes(func(c *Client, database string) (ICodes, error) {
if database == "" {
return nil, errors.New("未配置Codes的数据库")
}
return NewCodesMysql(c, database)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
if database == "" {
return nil, errors.New("未配置Workday的数据库")
}
return NewWorkdayMysql(c, database)
}),
)
}
func NewManage(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
//初始化配置
if cfg == nil {
cfg = &ManageConfig{}
}
if cfg.CodesFilename == "" {
cfg.CodesFilename = DefaultDatabaseDir + "/codes.db"
}
if cfg.WorkdayFileName == "" {
cfg.WorkdayFileName = DefaultDatabaseDir + "/workday.db"
}
if cfg.Dial == nil {
cfg.Dial = DialDefault
func NewManageSqlite(op ...Option) (*Manage, error) {
return NewManage(
WithCodesDatabase(DefaultDatabaseDir+"/codes.db"),
WithWorkdayDatabase(DefaultDatabaseDir+"/workday.db"),
WithOptions(op...),
WithDialCodes(func(c *Client, database string) (ICodes, error) {
return NewCodesSqlite(c, database)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
return NewWorkdaySqlite(c, database)
}),
)
}
func NewManageSqlite2(op ...Option) (*Manage, error) {
return NewManage(
WithCodesDatabase(DefaultDatabaseDir+"/codes2.db"),
WithWorkdayDatabase(DefaultDatabaseDir+"/workday.db"),
WithOptions(op...),
WithDialCodes(func(c *Client, database string) (ICodes, error) {
return NewCodes2(
WithCodes2Client(c),
WithCodes2Database(database),
)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
return NewWorkdaySqlite(c, database)
}),
)
}
func NewManage(op ...Option) (m *Manage, err error) {
m = &Manage{
clients: DefaultClients,
dial: DialDefault,
dialOptions: nil,
dialCodes: nil,
codesDatabase: DefaultDatabaseDir + "/codes2.db",
dialWorkday: nil,
workdayDatabase: DefaultDatabaseDir + "/workday.db",
Pool: nil,
Codes: nil,
Workday: nil,
cron: nil,
once: sync.Once{},
}
//通用客户端
commonClient, err := cfg.Dial(op...)
for _, v := range op {
if v != nil {
v(m)
}
}
m.clients = conv.Select(m.clients <= 0, 1, m.clients)
m.dial = conv.Select(m.dial == nil, DialDefault, m.dial)
//连接池
m.Pool, err = NewPool(func() (*Client, error) { return m.dial(m.dialOptions...) }, m.clients)
if err != nil {
return nil, err
}
commonClient.Wait.SetTimeout(time.Second * 5)
//代码管理
codes, err := NewCodesSqlite(commonClient, cfg.CodesFilename)
if err != nil {
return nil, err
if m.Codes == nil {
if m.dialCodes == nil {
m.dialCodes = func(c *Client, database string) (ICodes, error) {
return NewCodes2(WithCodes2Client(c), WithCodes2Database(database))
}
}
err = m.Pool.Do(func(c *Client) error {
m.Codes, err = m.dialCodes(c, m.codesDatabase)
return err
})
if err != nil {
return nil, err
}
}
//工作日管理
workday, err := NewWorkdaySqlite(commonClient, cfg.WorkdayFileName)
if err != nil {
return nil, err
if m.Workday == nil {
if m.dialWorkday == nil {
m.dialWorkday = func(c *Client, database string) (*Workday, error) {
return NewWorkdaySqlite(c, database)
}
}
err = m.Pool.Do(func(c *Client) error {
m.Workday, err = m.dialWorkday(c, m.workdayDatabase)
return err
})
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
}
return &Manage{
Pool: p,
Config: cfg,
Codes: codes,
Workday: workday,
Cron: cron.New(cron.WithSeconds()),
}, nil
/*
*/
type Option func(m *Manage)
type DialWorkdayFunc func(c *Client, database string) (*Workday, error)
type DialCodesFunc func(c *Client, database string) (ICodes, error)
func WithClients(clients int) Option {
return func(m *Manage) {
m.clients = clients
}
}
func WithDial(dial func(op ...client.Option) (*Client, error), op ...client.Option) Option {
return func(m *Manage) {
m.dial = dial
m.dialOptions = op
}
}
func WithDialOptions(op ...client.Option) Option {
return func(m *Manage) {
m.dialOptions = op
}
}
func WithCodes(codes ICodes) Option {
return func(m *Manage) {
m.Codes = codes
}
}
func WithDialCodes(dial DialCodesFunc) Option {
return func(m *Manage) {
m.dialCodes = dial
}
}
func WithCodesDatabase(database string) Option {
return func(m *Manage) {
m.codesDatabase = database
}
}
func WithWorkday(w *Workday) Option {
return func(m *Manage) {
m.Workday = w
}
}
func WithDialWorkday(dial DialWorkdayFunc) Option {
return func(m *Manage) {
m.dialWorkday = dial
}
}
func WithWorkdayDatabase(database string) Option {
return func(m *Manage) {
m.workdayDatabase = database
}
}
func WithOptions(op ...Option) Option {
return func(m *Manage) {
for _, v := range op {
v(m)
}
}
}
type Manage struct {
clients int
dial func(op ...client.Option) (cli *Client, err error)
dialOptions []client.Option
dialCodes func(c *Client, database string) (ICodes, error)
codesDatabase string
dialWorkday DialWorkdayFunc
workdayDatabase string
/*
*/
*Pool
Config *ManageConfig
Codes *Codes
Codes ICodes
Workday *Workday
Cron *cron.Cron
cron *cron.Cron
once sync.Once
}
// RangeStocks 遍历所有股票
func (this *Manage) RangeStocks(f func(code string)) {
for _, v := range this.Codes.GetStocks() {
f(v)
f(v.FullCode())
}
}
// RangeETFs 遍历所有ETF
func (this *Manage) RangeETFs(f func(code string)) {
for _, v := range this.Codes.GetETFs() {
f(v)
f(v.FullCode())
}
}
// RangeIndexes 遍历所有指数
func (this *Manage) RangeIndexes(f func(code string)) {
for _, v := range this.Codes.GetETFs() {
f(v.FullCode())
}
}
// 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++
}

View File

@@ -3,13 +3,14 @@ package protocol
import (
"bytes"
"fmt"
"github.com/injoyai/conv"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io"
"math"
"strings"
"time"
"github.com/injoyai/conv"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// String 字节先转小端,再转字符
@@ -285,7 +286,24 @@ func IsETF(code string) bool {
return true
case code[0:2] == ExchangeSZ.String() &&
(code[2:4] == "15" || code[2:4] == "16"):
(code[2:4] == "15"):
return true
}
return false
}
// IsIndex 是否是指数,sh000001,sz399001,bj899100
func IsIndex(code string) bool {
if len(code) != 8 {
return false
}
code = strings.ToLower(code)
switch {
case code[0:2] == ExchangeSH.String() && code[2:5] == "000":
return true
case code[0:2] == ExchangeSZ.String() && code[2:5] == "399":
return true
case code[0:2] == ExchangeBJ.String() && code[2:5] == "899":
return true
}
return false

View File

@@ -2,6 +2,11 @@ package tdx
import (
"errors"
"iter"
"os"
"path/filepath"
"time"
_ "github.com/glebarez/go-sqlite"
_ "github.com/go-sql-driver/mysql"
"github.com/injoyai/base/maps"
@@ -10,9 +15,6 @@ import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"os"
"path/filepath"
"time"
"xorm.io/core"
"xorm.io/xorm"
)
@@ -154,7 +156,7 @@ func (this *Workday) TodayIs() bool {
func (this *Workday) RangeYear(year int, f func(t time.Time) bool) {
this.Range(
time.Date(year, 1, 1, 0, 0, 0, 0, time.Local),
time.Date(year, 12, 31, 0, 0, 0, 0, time.Local),
time.Date(year, 12, 31, 0, 0, 0, 1, time.Local),
f,
)
}
@@ -162,8 +164,6 @@ func (this *Workday) RangeYear(year int, f func(t time.Time) bool) {
// Range 遍历指定范围的工作日,推荐start带上时间15:00,这样当天小于15点不会触发
func (this *Workday) Range(start, end time.Time, f func(t time.Time) bool) {
start = conv.Select(start.Before(protocol.ExchangeEstablish), protocol.ExchangeEstablish, start)
//now := IntegerDay(time.Now())
//end = conv.Select(end.After(now), now, end).Add(1)
for ; start.Before(end); start = start.Add(time.Hour * 24) {
if this.Is(start) {
if !f(start) {
@@ -173,13 +173,36 @@ func (this *Workday) Range(start, end time.Time, f func(t time.Time) bool) {
}
}
// RangeDesc 倒序遍历工作日,从今天-1990年12月19日(上海交易所成立时间)
func (this *Workday) RangeDesc(f func(t time.Time) bool) {
t := IntegerDay(time.Now())
for ; t.After(time.Date(1990, 12, 18, 0, 0, 0, 0, time.Local)); t = t.Add(-time.Hour * 24) {
if this.Is(t) {
if !f(t) {
return
func (this *Workday) IterYear(year int, desc ...bool) iter.Seq[time.Time] {
return this.Iter(
time.Date(year, 1, 1, 0, 0, 0, 0, time.Local),
time.Date(year, 12, 31, 0, 0, 0, 1, time.Local),
desc...,
)
}
// Iter 遍历指定范围的工作日,推荐start带上时间15:00,这样当天小于15点不会触发
func (this *Workday) Iter(start, end time.Time, desc ...bool) iter.Seq[time.Time] {
start = conv.Select(start.Before(protocol.ExchangeEstablish), protocol.ExchangeEstablish, start)
if len(desc) > 0 && desc[0] {
//倒序遍历
return func(yield func(time.Time) bool) {
for ; end.After(start); end = end.Add(-time.Hour * 24) {
if this.Is(end) {
if !yield(end) {
return
}
}
}
}
}
//正序遍历
return func(yield func(time.Time) bool) {
for ; start.Before(end); start = start.Add(time.Hour * 24) {
if this.Is(start) {
if !yield(start) {
return
}
}
}
}