Compare commits

...

41 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
injoyai
530de4fa5a Merge remote-tracking branch 'origin/master' 2025-11-13 16:58:10 +08:00
injoyai
ffe0cb2c92 准备增加流通股总股本 2025-11-13 16:57:53 +08:00
钱纯净
4d1487f0b6 Merge remote-tracking branch 'origin/master' 2025-11-02 21:59:20 +08:00
钱纯净
bd3863d3c5 优化etf的判断,sh51xxxx,sh56xxxx,sh68xxxx,sz15xxxx,sz16xxxx 2025-11-02 21:58:44 +08:00
injoyai
5fb1a212c6 增加tdx.DialWorkday 2025-10-31 10:45:22 +08:00
injoyai
f381d72ec4 优化Workday.Range 2025-10-30 10:45:26 +08:00
injoyai
5fa572f298 把集合竞价合并到931里面 2025-10-28 08:41:04 +08:00
injoyai
61b2e737b3 把集合竞价从931剥离出来到930 2025-10-27 09:09:19 +08:00
injoyai
5654065954 把集合竞价从931剥离出来到930 2025-10-27 08:49:22 +08:00
injoyai
d7dd7fe0bf 优化extend.GetBjCodes,直接输出代码 2025-10-22 14:25:30 +08:00
injoyai
9fb1a3b651 增加SetTimeout 设置超时时间 2025-10-21 09:57:03 +08:00
injoyai
f8a24c0cf1 优化GetHistoryTradeBefore,增加失败重试,共尝试3次,都失败则返回错误 2025-10-21 09:39:16 +08:00
injoyai
ad0abbe6ba 优化GetHistoryTradeFull,需要传入Workday,减少非工作日的查询操作 2025-10-20 16:00:09 +08:00
injoyai
0b35006323 优化GetHistoryTradeFull,需要传入Workday,减少非工作日的查询操作 2025-10-20 15:37:04 +08:00
injoyai
3b823e2e54 优化GetHistoryTradeFull,需要传入Workday,减少非工作日的查询操作 2025-10-20 15:35:20 +08:00
injoyai
5c8091ac26 修复深圳指数历史分时成交价格小10倍的问题 2025-10-20 09:49:01 +08:00
32 changed files with 1657 additions and 213 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,10 +14,8 @@ import (
"github.com/injoyai/ios/client"
"github.com/injoyai/ios/module/common"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/lib/bse"
"github.com/injoyai/tdx/protocol"
"runtime/debug"
"sync/atomic"
"time"
)
const (
@@ -181,6 +183,11 @@ func (this *Client) handlerDealMessage(c *client.Client, msg ios.Acker) {
}
// SetTimeout 设置超时时间
func (this *Client) SetTimeout(t time.Duration) {
this.Wait.SetTimeout(t)
}
// SendFrame 发送数据,并等待响应
func (this *Client) SendFrame(f *protocol.Frame, cache ...any) (any, error) {
f.MsgID = atomic.AddUint32(&this.msgID, 1)
@@ -225,7 +232,7 @@ func (this *Client) GetCodeAll(exchange protocol.Exchange) (*protocol.CodeResp,
//不放在extend包时防止循环引用
//todo 这是临时方案,等通达信有北交所代码列表时再改
if exchange == protocol.ExchangeBJ {
codes, err := GetBjCodes()
codes, err := bse.GetCodes()
if err != nil {
return nil, err
}
@@ -299,7 +306,8 @@ func (this *Client) GetQuote(codes ...string) (protocol.QuotesResp, error) {
return nil, errors.New("DefaultCodes未初始化")
}
//不是股票代码的话根据codes的信息加上前缀
codes[i] = DefaultCodes.AddExchange(codes[i])
//codes[i] = DefaultCodes.AddExchange(codes[i])
codes[i] = protocol.AddPrefix(codes[i])
}
}
@@ -440,7 +448,12 @@ func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16
}
// GetHistoryTradeFull 获取上市至今的分时成交
func (this *Client) GetHistoryTradeFull(code string) (protocol.Trades, error) {
func (this *Client) GetHistoryTradeFull(code string, w *Workday) (protocol.Trades, error) {
return this.GetHistoryTradeBefore(code, w, time.Now())
}
// GetHistoryTradeBefore 获取上市至今的分时成交
func (this *Client) GetHistoryTradeBefore(code string, w *Workday, before time.Time) (protocol.Trades, error) {
ls := protocol.Trades(nil)
resp, err := this.GetKlineMonthAll(code)
if err != nil {
@@ -451,14 +464,20 @@ func (this *Client) GetHistoryTradeFull(code string) (protocol.Trades, error) {
}
start := time.Date(resp.List[0].Time.Year(), resp.List[0].Time.Month(), 1, 0, 0, 0, 0, resp.List[0].Time.Location())
var res *protocol.TradeResp
for ; start.Before(time.Now()); start = start.Add(time.Hour * 24) {
res, err = this.GetHistoryTradeDay(start.Format("20060102"), code)
w.Range(start, before, func(t time.Time) bool {
for i := 0; i < 3; i++ {
res, err = this.GetHistoryTradeDay(t.Format("20060102"), code)
if err == nil {
break
}
}
if err != nil {
return nil, err
return false
}
ls = append(ls, res.List...)
}
return ls, nil
return true
})
return ls, err
}
// GetHistoryTradeDay 获取历史某天分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
@@ -542,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)
}

116
codes.go
View File

@@ -2,19 +2,33 @@ 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) CodeModels
GetStockCodes(limit ...int) []string
GetETFs(limit ...int) CodeModels
GetETFCodes(limit ...int) []string
GetIndexes(limits ...int) CodeModels
GetIndexCodes(limits ...int) []string
}
// DefaultCodes 增加单例,部分数据需要通过Codes里面的信息计算
var DefaultCodes *Codes
@@ -123,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 //数据库实例
@@ -131,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 {
@@ -140,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
@@ -155,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
@@ -171,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 {
@@ -312,28 +367,35 @@ func (*UpdateModel) TableName() string {
}
type CodeModel struct {
ID int64 `json:"id"` //主键
Name string `json:"name"` //名称,有时候名称会变,例STxxx
Code string `json:"code" xorm:"index"` //代码
Exchange string `json:"exchange" xorm:"index"` //交易所
Multiple uint16 `json:"multiple"` //倍数
Decimal int8 `json:"decimal"` //小数位
LastPrice float64 `json:"lastPrice"` //昨收价格
EditDate int64 `json:"editDate" xorm:"updated"` //修改时间
InDate int64 `json:"inDate" xorm:"created"` //创建时间
ID int64 `json:"id"` //主键
Name string `json:"name"` //名称,有时候名称会变,例STxxx
Code string `json:"code" xorm:"index"` //代码
Exchange string `json:"exchange" xorm:"index"` //交易所
Multiple uint16 `json:"multiple"` //倍数
Decimal int8 `json:"decimal"` //小数位
LastPrice float64 `json:"lastPrice"` //昨收价格
FloatStock float64 `json:"floatStock"` //流通股
TotalStock float64 `json:"totalStock"` //总股本
EditDate int64 `json:"editDate" xorm:"updated"` //修改时间
InDate int64 `json:"inDate" xorm:"created"` //创建时间
}
func (*CodeModel) TableName() string {
return "codes"
}
// FullCode 获取完整代码 sz000001
func (this *CodeModel) FullCode() string {
return this.Exchange + this.Code
}
// Turnover 换手率
func (this *CodeModel) Turnover(volume float64) float64 {
return volume / (this.FloatStock * 100)
}
func (this *CodeModel) Price(p protocol.Price) protocol.Price {
return protocol.Price(float64(p) * math.Pow10(int(2-this.Decimal)))
//return p * protocol.Price(math.Pow10(int(2-this.Decimal)))
}
func NewSessionFunc(db *xorm.Engine, fn func(session *xorm.Session) error) error {
@@ -353,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
}

380
codes_v2.go Normal file
View File

@@ -0,0 +1,380 @@
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/lib/gbbq"
"github.com/injoyai/tdx/lib/xorms"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"xorm.io/xorm"
)
type Codes2Option func(*Codes2)
func WithCodes2Database(filename string) Codes2Option {
return func(c *Codes2) {
c.dbFilename = filename
}
}
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 WithCodes2UpdateKey(key string) Codes2Option {
return func(c *Codes2) {
c.updateKey = key
}
}
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{
dbFilename: filepath.Join(DefaultDatabaseDir, "codes2.db"),
tempDir: filepath.Join(DefaultDataDir, "temp"),
spec: "10 0 9 * * *",
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
// 初始化连接
if cs.c == nil {
cs.c, err = DialWith(cs.dial, cs.dialOption...)
if err != nil {
return nil, err
}
}
// 初始化数据库
cs.db, err = xorms.NewSqlite(cs.dbFilename)
if err != nil {
return nil, err
}
if err = cs.db.Sync2(new(CodeModel), new(UpdateModel)); err != nil {
return nil, err
}
// 立即更新
err = cs.Update()
if err != nil {
return nil, err
}
// 定时更新
cr := cron.New(cron.WithSeconds())
_, err = cr.AddFunc(cs.spec, func() {
for i := 0; i == 0 || i < cs.retry; i++ {
if err := cs.Update(); err != nil {
logs.Err(err)
<-time.After(time.Minute * 5)
} else {
break
}
}
})
if err != nil {
return nil, err
}
cr.Start()
return cs, nil
}
var _ ICodes = &Codes2{}
type Codes2 struct {
dbFilename string //数据库文件
tempDir string //临时目录
spec string //定时规则
updateKey string //标识
retry int //重试次数
dial ios.DialFunc //连接
dialOption []client.Option //
/*
内部字段
*/
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 {
v, _ := this.m.Get(code)
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 {
return "未知"
}
return v.Name
}
func (this *Codes2) GetStocks(limit ...int) CodeModels {
size := conv.Default(this.stocks.Len(), limit...)
return CodeModels(this.stocks.Limit(size))
}
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 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.updateKey).Get(update)
if err != nil {
return true, err
} else if !has {
update.Key = this.updateKey
if _, err = this.db.Insert(update); err != nil {
return true, err
}
return false, nil
}
}
{ //判断是否更新过,更新过则不更新
now := time.Now()
node := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, time.Local)
updateTime := time.Unix(update.Time, 0)
if now.Sub(node) > 0 {
//当前时间在9点之后,且更新时间在9点之前,需要更新
if updateTime.Sub(node) < 0 {
return false, nil
}
} else {
//当前时间在9点之前,且更新时间在上个节点之前
if updateTime.Sub(node.Add(time.Hour*24)) < 0 {
return false, nil
}
}
}
return true, nil
}
func (this *Codes2) Update() error {
codes, err := this.update()
if err != nil {
return err
}
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, v)
case protocol.IsETF(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
}
// GetCodes 更新股票并返回结果
func (this *Codes2) update() ([]*CodeModel, error) {
if this.c == nil {
return nil, errors.New("client is nil")
}
//2. 查询数据库所有股票
list := []*CodeModel(nil)
if err := this.db.Find(&list); err != nil {
return nil, err
}
//如果更新过,则不更新
updated, err := this.updated()
if err == nil && updated {
return list, nil
}
mCode := make(map[string]*CodeModel, len(list))
for _, v := range list {
mCode[v.FullCode()] = v
}
//3. 从服务器获取所有股票代码
insert := []*CodeModel(nil)
update := []*CodeModel(nil)
for _, exchange := range []protocol.Exchange{protocol.ExchangeSH, protocol.ExchangeSZ, protocol.ExchangeBJ} {
resp, err := this.c.GetCodeAll(exchange)
if err != nil {
return nil, err
}
for _, v := range resp.List {
code := &CodeModel{
Name: v.Name,
Code: v.Code,
Exchange: exchange.String(),
Multiple: v.Multiple,
Decimal: v.Decimal,
LastPrice: v.LastPrice,
}
if val, ok := mCode[exchange.String()+v.Code]; ok {
if val.Name != v.Name {
update = append(update, code)
}
delete(mCode, exchange.String()+v.Code)
} else {
insert = append(insert, code)
list = append(list, code)
}
}
}
//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 {
return err
}
}
for _, v := range insert {
if _, err := session.Insert(v); err != nil {
return err
}
}
for _, v := range update {
if _, err = session.Where("Exchange=? and Code=? ", v.Exchange, v.Code).Cols("Name,LastPrice").Update(v); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
//更新时间
_, 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,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetHistoryMinuteTradeDay("20251010", "sh000001")
resp, err := c.GetHistoryMinuteTradeDay("20170704", "sh000001")
logs.PanicErr(err)
for _, v := range resp.List {

View File

@@ -8,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetKlineDay("838971", 0, 20)
resp, err := c.GetKlineDay("920992", 0, 20)
logs.PanicErr(err)
for _, v := range resp.List {

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,9 +1,17 @@
package extend
import (
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/lib/bse"
)
func GetBjCodes() ([]*tdx.BjCode, error) {
return tdx.GetBjCodes()
func GetBjCodes() ([]string, error) {
cs, err := bse.GetCodes()
if err != nil {
return nil, err
}
ls := []string(nil)
for _, v := range cs {
ls = append(ls, "bj"+v.Code)
}
return ls, nil
}

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

@@ -1,4 +1,4 @@
package tdx
package bse
import (
"bytes"
@@ -13,15 +13,15 @@ import (
)
const (
// UrlBjCodes 最后跟的是时间戳(ms),但是随便什么时间戳都能请求成功
UrlBjCodes = "https://www.bse.cn/nqhqController/nqhq_en.do?callback=jQuery3710848510589806625_%d"
// UrlCodes 最后跟的是时间戳(ms),但是随便什么时间戳都能请求成功
UrlCodes = "https://www.bse.cn/nqhqController/nqhq_en.do?callback=jQuery3710848510589806625_%d"
)
func GetBjCodes() ([]*BjCode, error) {
list := []*BjCode(nil)
func GetCodes() ([]*Code, error) {
list := []*Code(nil)
//这个200预防下bug,除非北京上市公司有4000个
for page := 0; page < 200; page++ {
ls, done, err := getBjCodes(page)
ls, done, err := getCodes(page)
if err != nil {
return nil, err
}
@@ -35,9 +35,9 @@ func GetBjCodes() ([]*BjCode, error) {
return list, nil
}
func getBjCodes(page int) (_ []*BjCode, last bool, err error) {
func getCodes(page int) (_ []*Code, last bool, err error) {
url := fmt.Sprintf(UrlBjCodes, time.Now().UnixMilli())
url := fmt.Sprintf(UrlCodes, time.Now().UnixMilli())
bodyStr := "page=" + conv.String(page) + "&type_en=%5B%22B%22%5D&sortfield=hqcjsl&sorttype=desc&xxfcbj_en=%5B2%5D&zqdm="
@@ -68,7 +68,7 @@ func getBjCodes(page int) (_ []*BjCode, last bool, err error) {
bs = bs[i+1 : len(bs)-1]
ls := []*BjCodes(nil)
ls := []*Codes(nil)
err = json.Unmarshal(bs, &ls)
if err != nil {
return nil, false, err
@@ -81,14 +81,14 @@ func getBjCodes(page int) (_ []*BjCode, last bool, err error) {
return ls[0].Data, ls[0].LastPage, nil
}
type BjCodes struct {
Data []*BjCode `json:"content"`
TotalNumber int `json:"totalElements"`
TotalPage int `json:"totalPages"`
LastPage bool `json:"lastPage"`
type Codes struct {
Data []*Code `json:"content"`
TotalNumber int `json:"totalElements"`
TotalPage int `json:"totalPages"`
LastPage bool `json:"lastPage"`
}
type BjCode struct {
type Code struct {
Date string `json:"hqjsrq"` //日期
Code string `json:"hqzqdm"` //代码
Name string `json:"hqzqjc"` //名称

197
lib/gbbq/gbbq.go Normal file

File diff suppressed because one or more lines are too long

118
lib/xorms/engine.go Normal file
View File

@@ -0,0 +1,118 @@
package xorms
import (
_ "github.com/glebarez/go-sqlite"
_ "github.com/go-sql-driver/mysql"
"os"
"path/filepath"
"time"
"xorm.io/core"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func NewMysql(dsn string, options ...Option) (*Engine, error) {
return New("mysql", dsn, options...)
}
func NewSqlite(filename string, options ...Option) (*Engine, error) {
dir, _ := filepath.Split(filename)
_ = os.MkdirAll(dir, 0777)
//sqlite是文件数据库,只能打开一次(即一个连接)
options = append(options, WithMaxOpenConns(1))
return New("sqlite", filename, options...)
}
/*
New 需要手动引用驱动
mysql _ "github.com/go-sql-driver/mysql"
sqlite _ "github.com/glebarez/go-sqlite"
sqlserver _ "github.com/denisenkom/go-mssqldb"
*/
func New(Type, dsn string, options ...Option) (*Engine, error) {
db, err := xorm.NewEngine(Type, dsn)
if err != nil {
return nil, err
}
//默认同步字段
WithSyncField(true)(db)
for _, v := range options {
v(db)
}
return &Engine{Engine: db}, nil
}
type Engine struct {
*xorm.Engine
}
func (this *Engine) TableName(v any) string {
return this.Engine.TableName(v)
}
func (this *Engine) Tables() []*schemas.Table {
list, _ := this.DBMetas()
return list
}
// SetTablePrefix 前缀
func (this *Engine) SetTablePrefix(s string) *Engine {
this.SetTableMapper(core.NewPrefixMapper(core.SameMapper{}, s))
return this
}
// SetSyncField 字段同步
func (this *Engine) SetSyncField() *Engine {
this.SetMapper(core.SameMapper{})
return this
}
// SetConnMaxLifetime 设置连接超时时间(超时会断开连接)
func (this *Engine) SetConnMaxLifetime(d time.Duration) *Engine {
this.DB().SetConnMaxLifetime(d)
return this
}
// SetMaxIdleConns 设置空闲数(一直连接不断开)
func (this *Engine) SetMaxIdleConns(n int) *Engine {
this.DB().SetMaxIdleConns(n)
return this
}
// SetMaxOpenConns 设置连接数(超出最大数量会等待)
func (this *Engine) SetMaxOpenConns(n int) *Engine {
this.DB().SetMaxOpenConns(n)
return this
}
// NewSession 新建自动关闭事务
func (this *Engine) NewSession() *Session {
return newSession(this.Engine.Where(""))
}
func (this *Engine) SessionFunc(fn func(session *xorm.Session) error) error {
return NewSessionFunc(this.Engine, fn)
}
func (this *Engine) Like(param, arg string) *Session {
return newSession(this.Engine.Where(param+" like ?", "%"+arg+"%"))
}
func (this *Engine) Desc(colNames ...string) *Session {
return newSession(this.Engine.Desc(colNames...))
}
func (this *Engine) Asc(colNames ...string) *Session {
return newSession(this.Engine.Asc(colNames...))
}
func (this *Engine) Limit(limit int, start ...int) *Session {
if limit > 0 {
return newSession(this.Engine.Limit(limit, start...))
}
return newSession(this.Engine.Where(""))
}
func (this *Engine) Where(query any, args ...any) *Session {
return newSession(this.Engine.Where(query, args...))
}

70
lib/xorms/option.go Normal file
View File

@@ -0,0 +1,70 @@
package xorms
import (
"github.com/injoyai/conv"
"github.com/injoyai/conv/cfg"
"time"
"xorm.io/core"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
type Option func(*xorm.Engine)
func WithCfg(path ...string) Option {
return WithDMap(cfg.Default.GetDMap(conv.Default[string]("database", path...)))
}
func WithDMap(m *conv.Map) Option {
return func(e *xorm.Engine) {
if v := m.GetVar("fieldSync"); !v.IsNil() {
WithSyncField(v.Bool())(e)
}
if v := m.GetVar("tablePrefix"); !v.IsNil() {
WithTablePrefix(v.String())(e)
}
if v := m.GetVar("connMaxLifetime"); !v.IsNil() {
WithConnMaxLifetime(v.Duration())(e)
}
if v := m.GetVar("maxIdleConns"); !v.IsNil() {
WithMaxIdleConns(v.Int())(e)
}
if v := m.GetVar("maxOpenConns"); !v.IsNil() {
WithMaxOpenConns(v.Int())(e)
}
}
}
func WithTablePrefix(prefix string) Option {
return func(e *xorm.Engine) {
e.SetTableMapper(core.NewPrefixMapper(core.SameMapper{}, prefix))
}
}
func WithSyncField(b bool) Option {
return func(e *xorm.Engine) {
if b {
e.SetMapper(core.SameMapper{})
} else {
e.SetMapper(names.NewCacheMapper(new(names.SnakeMapper)))
}
}
}
func WithConnMaxLifetime(d time.Duration) Option {
return func(e *xorm.Engine) {
e.DB().SetConnMaxLifetime(d)
}
}
func WithMaxIdleConns(n int) Option {
return func(e *xorm.Engine) {
e.DB().SetMaxIdleConns(n)
}
}
func WithMaxOpenConns(n int) Option {
return func(e *xorm.Engine) {
e.DB().SetMaxOpenConns(n)
}
}

61
lib/xorms/session.go Normal file
View File

@@ -0,0 +1,61 @@
package xorms
import "xorm.io/xorm"
type Session struct {
*xorm.Session
}
func newSession(session *xorm.Session) *Session {
return &Session{session}
}
func (this *Session) Like(param, arg string) *Session {
this.Session.Where(param+" like ?", "%"+arg+"%")
return this
}
func (this *Session) Desc(colNames ...string) *Session {
this.Session.Desc(colNames...)
return this
}
func (this *Session) Asc(colNames ...string) *Session {
this.Session.Asc(colNames...)
return this
}
func (this *Session) Limit(limit int, start ...int) *Session {
if limit > 0 {
this.Session.Limit(limit, start...)
}
return this
}
func (this *Session) Where(query any, args ...any) *Session {
this.Session.Where(query, args...)
return this
}
func (this *Session) And(query any, args ...any) *Session {
this.Session.And(query, args...)
return this
}
func NewSessionFunc(db *xorm.Engine, fn func(session *xorm.Session) error) error {
session := db.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
session.Rollback()
return err
}
if err := fn(session); err != nil {
session.Rollback()
return err
}
if err := session.Commit(); err != nil {
session.Rollback()
return err
}
return nil
}

119
lib/zip/zip_func.go Normal file
View File

@@ -0,0 +1,119 @@
package zip
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
)
// Encode 压缩文件
// @filePath,文件路径
// @zipName,压缩名称
func Encode(filePath, zipName string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
os.MkdirAll(filepath.Dir(zipName), os.ModePerm)
zipFile, err := os.Create(zipName)
if err != nil {
return err
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
return compareZip(file, zipWriter, "", !strings.HasSuffix(filePath, "/"))
}
// 压缩文件
func compareZip(file *os.File, zipWriter *zip.Writer, prefix string, join bool) error {
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
if join {
header.Name = filepath.Join(prefix, header.Name)
prefix = filepath.Join(prefix, fileInfo.Name())
header.Name = strings.ReplaceAll(header.Name, "\\", "/")
if fileInfo.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate //压缩的关键
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if !fileInfo.IsDir() {
_, err = io.Copy(writer, file)
return err
}
}
fileInfoChildList, err := file.Readdir(-1)
if err != nil {
return err
}
for _, fileInfoChild := range fileInfoChildList {
fileChild, err := os.Open(filepath.Join(file.Name(), fileInfoChild.Name()))
if err != nil {
return err
}
if err := compareZip(fileChild, zipWriter, prefix, true); err != nil {
return err
}
}
return nil
}
// Decode 解压zip
func Decode(zipName, filePath string) error {
r, err := zip.OpenReader(zipName)
if err != nil {
return err
}
defer r.Close()
for _, k := range r.Reader.File {
if k.FileInfo().IsDir() {
if err := os.MkdirAll(filepath.Join(filePath, k.Name), os.ModePerm); err != nil {
return err
}
} else {
err := func() error {
f, err := k.Open()
if err != nil {
return err
}
defer f.Close()
w, err := os.Create(filepath.Join(filePath, k.Name))
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, f)
return err
}()
if err != nil {
return err
}
}
}
return nil
}

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 //时间
@@ -293,6 +294,7 @@ func (this Klines) Merge(n int) Klines {
if n <= 1 {
return this
}
ks := Klines(nil)
ls := Klines(nil)
for i := 0; ; i++ {

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++
}
@@ -210,7 +211,7 @@ func (this Trades) klinesForDay(date time.Time) Klines {
//分组,按
for _, v := range this {
ms := minutes(v.Time)
t := conv.Select(ms <= _930, _930, ms)
t := conv.Select(ms < _930, _930, ms)
t++
t = conv.Select(t > _1130 && t <= _1300, _1130, t)
t = conv.Select(t > _1500, _1500, t)

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 字节先转小端,再转字符
@@ -135,10 +136,10 @@ func basePrice(code string) Price {
return 1
}
switch code[:2] {
case "60", "30", "68", "00", "92", "43":
case "60", "30", "68", "00", "92", "43", "39":
return 1
default:
return 10
return 1
}
}
@@ -281,11 +282,28 @@ func IsETF(code string) bool {
code = strings.ToLower(code)
switch {
case code[0:2] == ExchangeSH.String() &&
(code[2:5] == "510" || code[2:5] == "511" || code[2:5] == "512" || code[2:5] == "513" || code[2:5] == "515"):
(code[2:4] == "51" || code[2:4] == "56" || code[2:4] == "58"):
return true
case code[0:2] == ExchangeSZ.String() &&
(code[2:5] == "159"):
(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,20 +2,31 @@ 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"
"github.com/injoyai/conv"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"os"
"path/filepath"
"time"
"xorm.io/core"
"xorm.io/xorm"
)
func DialWorkday(op ...client.Option) (*Workday, error) {
c, err := DialDefault(op...)
if err != nil {
return nil, err
}
return NewWorkdaySqlite(c)
}
func NewWorkdayMysql(c *Client, dsn string) (*Workday, error) {
//连接数据库
@@ -145,16 +156,14 @@ 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,
)
}
// Range 遍历指定范围的工作日
// 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) {
@@ -164,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
}
}
}
}