Compare commits

...

54 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
钱纯净
d7b6963bd6 修复workday没有要更新时会报错的问题 "no element on slice when insert" 2025-10-19 22:46:06 +08:00
injoyai
456a0af9a5 增加Manage对mysql的支持 2025-10-16 14:00:31 +08:00
injoyai
84404bcb2c 增加Manage对mysql的支持 2025-10-16 11:00:58 +08:00
injoyai
716e35122f 增加Manage对mysql的支持 2025-10-16 10:54:44 +08:00
injoyai
c4866a2f2e 增加Manage对mysql的支持 2025-10-16 10:47:24 +08:00
injoyai
fa98199dae 增加Codes和Workday对mysql的支持 2025-10-16 10:33:04 +08:00
injoyai
29882ea5c0 修复IsSZStock的判断 2025-10-14 13:07:59 +08:00
injoyai
37eb34beaa 优化 2025-10-14 09:23:45 +08:00
钱纯净
7bf4839310 修改GetHistoryTradeAll为GetHistoryTradeDay,增加GetHistoryTradeFull,获取上市至今的分时成交 2025-10-13 20:04:47 +08:00
钱纯净
cbf56d936d 优化Klines的Kline和Merge 2025-10-13 19:53:19 +08:00
injoyai
80ecdec737 增加extend.ListenCodesHTTP,因为北交所代码是网页爬虫,爬被封ip,感觉好像用处也不大 2025-10-13 15:05:21 +08:00
injoyai
ecf0365879 优化protocol.Trades,能直接生成完整的分时k线,和实际误差很小 2025-10-13 15:01:44 +08:00
injoyai
f1da1182ce 优化protocol.Trades,能直接生成完整的分时k线,和实际误差很小 2025-10-13 15:01:27 +08:00
38 changed files with 2069 additions and 286 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])
}
}
@@ -422,7 +430,7 @@ func (this *Client) GetHistoryTrade(date, code string, start, count uint16) (*pr
// GetHistoryMinuteTrade 获取历史分时交易
// 只能获取昨天及之前的数据,服务器最多返回2000条,count-start<=2000,如果日期输入错误,则返回0
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
// 历史数据只能查到20000609
func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16) (*protocol.TradeResp, error) {
code = protocol.AddPrefix(code)
f, err := protocol.MHistoryTrade.Frame(date, code, start, count)
@@ -439,13 +447,47 @@ func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16
return result.(*protocol.TradeResp), nil
}
func (this *Client) GetHistoryTradeAll(date, code string) (*protocol.TradeResp, error) {
return this.GetHistoryMinuteTradeAll(date, code)
// GetHistoryTradeFull 获取上市至今的分时成交
func (this *Client) GetHistoryTradeFull(code string, w *Workday) (protocol.Trades, error) {
return this.GetHistoryTradeBefore(code, w, time.Now())
}
// GetHistoryMinuteTradeAll 获取历史分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
func (this *Client) GetHistoryMinuteTradeAll(date, code string) (*protocol.TradeResp, error) {
// 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 {
return nil, err
}
if len(resp.List) == 0 {
return nil, nil
}
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
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 false
}
ls = append(ls, res.List...)
return true
})
return ls, err
}
// GetHistoryTradeDay 获取历史某天分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
func (this *Client) GetHistoryTradeDay(date, code string) (*protocol.TradeResp, error) {
return this.GetHistoryMinuteTradeDay(date, code)
}
// GetHistoryMinuteTradeDay 获取历史某天分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
// 历史数据只能查到20000609
func (this *Client) GetHistoryMinuteTradeDay(date, code string) (*protocol.TradeResp, error) {
resp := &protocol.TradeResp{}
size := uint16(2000)
for start := uint16(0); ; start += size {
@@ -519,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)
}

196
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
@@ -23,10 +37,22 @@ func DialCodes(filename string, op ...client.Option) (*Codes, error) {
if err != nil {
return nil, err
}
return NewCodes(c, filename)
return NewCodesSqlite(c, filename)
}
func NewCodes(c *Client, filenames ...string) (*Codes, error) {
func NewCodesMysql(c *Client, dsn string) (*Codes, error) {
//连接数据库
db, err := xorm.NewEngine("mysql", dsn)
if err != nil {
return nil, err
}
db.SetMapper(core.SameMapper{})
return NewCodes(c, db)
}
func NewCodesSqlite(c *Client, filenames ...string) (*Codes, error) {
//如果没有指定文件名,则使用默认
defaultFilename := filepath.Join(DefaultDatabaseDir, "codes.db")
@@ -44,6 +70,12 @@ func NewCodes(c *Client, filenames ...string) (*Codes, error) {
}
db.SetMapper(core.SameMapper{})
db.DB().SetMaxOpenConns(1)
return NewCodes(c, db)
}
func NewCodes(c *Client, db *xorm.Engine) (*Codes, error) {
if err := db.Sync2(new(CodeModel)); err != nil {
return nil, err
}
@@ -53,10 +85,11 @@ func NewCodes(c *Client, filenames ...string) (*Codes, error) {
update := new(UpdateModel)
{ //查询或者插入一条数据
has, err := db.Get(update)
has, err := db.Where("`Key`=?", "codes").Get(update)
if err != nil {
return nil, err
} else if !has {
update.Key = "codes"
if _, err := db.Insert(update); err != nil {
return nil, err
}
@@ -72,7 +105,8 @@ func NewCodes(c *Client, filenames ...string) (*Codes, error) {
task := cron.New(cron.WithSeconds())
task.AddFunc("10 0 9 * * *", func() {
for i := 0; i < 3; i++ {
if err := cc.Update(); err == nil {
err := cc.Update()
if err == nil {
return
}
logs.Err(err)
@@ -103,6 +137,8 @@ func NewCodes(c *Client, filenames ...string) (*Codes, error) {
return cc, cc.Update(true)
}
var _ ICodes = &Codes{}
type Codes struct {
*Client //客户端
db *xorm.Engine //数据库实例
@@ -111,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 {
@@ -120,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
@@ -135,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
@@ -151,57 +205,33 @@ 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()
}
//// GetExchange 获取股票交易所,这里的参数不需要带前缀
//func (this *Codes) GetExchange(code string) protocol.Exchange {
// if len(code) == 6 {
// switch {
// case code[:1] == "6":
// return protocol.ExchangeSH
// case code[:1] == "0":
// return protocol.ExchangeSZ
// case code[:2] == "30":
// return protocol.ExchangeSZ
// }
// }
// var exchange string
// exchanges := this.exchanges[code]
// if len(exchanges) >= 1 {
// exchange = exchanges[0]
// }
// if len(code) == 8 {
// exchange = code[0:2]
// }
// switch exchange {
// case protocol.ExchangeSH.String():
// return protocol.ExchangeSH
// case protocol.ExchangeSZ.String():
// return protocol.ExchangeSZ
// default:
// return protocol.ExchangeSH
// }
//}
// 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 {
return protocol.AddPrefix(code)
//if exchanges := this.exchanges[code]; len(exchanges) == 1 {
// return exchanges[0] + code
//}
//if len(code) == 6 {
// switch {
// case code[:1] == "6":
// return protocol.ExchangeSH.String() + code
// case code[:1] == "0":
// return protocol.ExchangeSZ.String() + code
// case code[:2] == "30":
// return protocol.ExchangeSZ.String() + code
// }
// return this.GetExchange(code).String() + code
//}
//return code
}
// Update 更新数据,从服务器或者数据库
@@ -220,7 +250,7 @@ func (this *Codes) Update(byDB ...bool) error {
this.list = codes
this.exchanges = exchanges
//更新时间
_, err = this.db.Update(&UpdateModel{Time: time.Now().Unix()})
_, err = this.db.Where("`Key`=?", "codes").Update(&UpdateModel{Time: time.Now().Unix()})
return err
}
@@ -283,6 +313,28 @@ func (this *Codes) GetCodes(byDatabase bool) ([]*CodeModel, error) {
}
}
switch this.db.Dialect().URI().DBType {
case "mysql":
// 1⃣ 清空
if _, err := this.db.Exec("TRUNCATE TABLE codes"); err != nil {
return nil, err
}
data := append(insert, update...)
// 2⃣ 直接批量插入
batchSize := 3000 // 8000(2m16s) 5000(43s) 3000(11s) 1000(59s)
for i := 0; i < len(data); i += batchSize {
end := i + batchSize
if end > len(data) {
end = len(data)
}
slice := conv.Array(data[i:end])
if _, err := this.db.Insert(slice); err != nil {
return nil, err
}
}
case "sqlite3":
//4. 插入或者更新数据库
err := NewSessionFunc(this.db, func(session *xorm.Session) error {
for _, v := range insert {
@@ -300,12 +352,13 @@ func (this *Codes) GetCodes(byDatabase bool) ([]*CodeModel, error) {
if err != nil {
return nil, err
}
}
return list, nil
}
type UpdateModel struct {
Key string
Time int64 //更新时间
}
@@ -321,6 +374,8 @@ type CodeModel struct {
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"` //创建时间
}
@@ -329,13 +384,18 @@ 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 {
@@ -355,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)
}
}
}

20
example/CodesHTTP/main.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx/extend"
"time"
)
func main() {
go extend.ListenCodesHTTP(10033)
<-time.After(time.Second * 3)
c := extend.DialCodesHTTP("http://localhost:10033")
stocks, err := c.GetStocks()
logs.PanicErr(err)
for _, v := range stocks {
println(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.GetHistoryMinuteTradeAll("20241025", "sz000001")
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

@@ -10,7 +10,7 @@ func main() {
c, err := tdx.Dial("124.71.187.122:7709", tdx.WithDebug())
logs.PanicErr(err)
tdx.DefaultCodes, err = tdx.NewCodes(c, "./codes.db")
tdx.DefaultCodes, err = tdx.NewCodesSqlite(c, "./codes.db")
logs.PanicErr(err)
_ = c

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

@@ -0,0 +1,16 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
_, 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

@@ -0,0 +1,22 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/example/common"
)
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetHistoryTradeDay("20251010", "sz000001")
logs.PanicErr(err)
ks := resp.List.Klines()
for _, v := range ks {
logs.Debug(v)
}
})
}

View File

@@ -9,10 +9,10 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
_, err := tdx.NewWorkday(c) //"./workday.db"
_, err := tdx.NewWorkdaySqlite(c) //"./workday.db"
logs.PanicErr(err)
_, err = tdx.NewCodes(c) //"./codes.db"
_, err = tdx.NewCodesSqlite(c) //"./codes.db"
logs.PanicErr(err)
c.Close()

17
extend/codes-bj.go Normal file
View File

@@ -0,0 +1,17 @@
package extend
import (
"github.com/injoyai/tdx/lib/bse"
)
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
}

162
extend/codes-server.go Normal file
View File

@@ -0,0 +1,162 @@
package extend
import (
"encoding/json"
"fmt"
"io"
"iter"
"net/http"
"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, 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":
succ(w, code.GetStocks())
case "/etfs":
succ(w, code.GetETFs())
case "/indexes":
succ(w, code.GetIndexes())
default:
http.NotFound(w, r)
}
}))
}
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) 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
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http code:%d", resp.StatusCode)
}
bs, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ls := tdx.CodeModels{}
err = json.Unmarshal(bs, &ls)
return ls, err
}

View File

@@ -1,9 +0,0 @@
package extend
import (
"github.com/injoyai/tdx"
)
func GetBjCodes() ([]*tdx.BjCode, error) {
return tdx.GetBjCodes()
}

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 {

View File

@@ -56,7 +56,7 @@ func (this *PullTrade) PullYear(ctx context.Context, m *tdx.Manage, year int, co
var resp *protocol.TradeResp
err = m.Do(func(c *tdx.Client) error {
resp, err = c.GetHistoryTradeAll(date, code)
resp, err = c.GetHistoryTradeDay(date, code)
return err
})
if err != nil {
@@ -67,11 +67,7 @@ func (this *PullTrade) PullYear(ctx context.Context, m *tdx.Manage, year int, co
tss = append(tss, resp.List...)
//转成分时K线
ks, err := resp.List.Klines1()
if err != nil {
logs.Err(err)
return false
}
ks := resp.List.Klines()
kss1 = append(kss1, ks...)
kss5 = append(kss5, ks.Merge(5)...)

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"`
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
}

242
manage.go
View File

@@ -1,91 +1,259 @@
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 NewManage(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
//初始化配置
if cfg == nil {
cfg = &ManageConfig{}
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的数据库")
}
if cfg.CodesFilename == "" {
cfg.CodesFilename = DefaultDatabaseDir + "/codes.db"
return NewCodesMysql(c, database)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
if database == "" {
return nil, errors.New("未配置Workday的数据库")
}
if cfg.WorkdayFileName == "" {
cfg.WorkdayFileName = DefaultDatabaseDir + "/workday.db"
}
if cfg.Dial == nil {
cfg.Dial = DialDefault
return NewWorkdayMysql(c, database)
}),
)
}
//通用客户端
commonClient, err := cfg.Dial(op...)
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{},
}
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 := NewCodes(commonClient, cfg.CodesFilename)
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 := NewWorkday(commonClient, cfg.WorkdayFileName)
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 &Manage{
Pool: p,
Config: cfg,
Codes: codes,
Workday: workday,
Cron: cron.New(cron.WithSeconds()),
}, nil
return
}
/*
*/
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 //时间
@@ -233,6 +234,14 @@ func FixKlineTime(ks []*Kline) []*Kline {
type Klines []*Kline
// LastPrice 获取最后一个K线的收盘价
func (this Klines) LastPrice() Price {
if len(this) == 0 {
return 0
}
return this[len(this)-1].Close
}
func (this Klines) Len() int {
return len(this)
}
@@ -249,12 +258,16 @@ func (this Klines) Sort() {
sort.Sort(this)
}
// Kline 计算多个K线,成一个K线
func (this Klines) Kline() *Kline {
if this == nil {
return new(Kline)
func (this Klines) Kline(t time.Time, last Price) *Kline {
k := &Kline{
Time: t,
Open: last,
High: last,
Low: last,
Close: last,
Volume: 0,
Amount: 0,
}
k := new(Kline)
for i, v := range this {
switch i {
case 0:
@@ -262,34 +275,88 @@ func (this Klines) Kline() *Kline {
k.High = v.High
k.Low = v.Low
k.Close = v.Close
case len(this) - 1:
default:
if k.Open == 0 {
k.Open = v.Open
}
k.High = conv.Select(k.High < v.High, v.High, k.High)
k.Low = conv.Select(k.Low > v.Low, v.Low, k.Low)
}
k.Close = v.Close
k.Time = v.Time
}
if v.High > k.High {
k.High = v.High
}
if v.Low < k.Low {
k.Low = v.Low
}
k.Volume += v.Volume
k.Amount += v.Amount
}
return k
}
// Merge 合并K线,1分钟转成5,15,30分钟等
// Merge 合并成其他类型的K线
func (this Klines) Merge(n int) Klines {
if this == nil {
return nil
if n <= 1 {
return this
}
ks := []*Kline(nil)
for i := 0; i < len(this); i += n {
if i+n > len(this) {
ks = append(ks, this[i:].Kline())
ks := Klines(nil)
ls := Klines(nil)
for i := 0; ; i++ {
if len(this) <= i*n {
break
}
if len(this) < (i+1)*n {
ls = this[i*n:]
} else {
ks = append(ks, this[i:i+n].Kline())
ls = this[i*n : (i+1)*n]
}
if len(ls) == 0 {
break
}
last := ls[len(ls)-1]
k := ls.Kline(last.Time, ls[0].Open)
ks = append(ks, k)
}
return ks
}
//// Kline 计算多个K线,成一个K线
//func (this Klines) Kline() *Kline {
// if this == nil {
// return new(Kline)
// }
// k := new(Kline)
// for i, v := range this {
// switch i {
// case 0:
// k.Open = v.Open
// k.High = v.High
// k.Low = v.Low
// k.Close = v.Close
// case len(this) - 1:
// k.Close = v.Close
// k.Time = v.Time
// }
// if v.High > k.High {
// k.High = v.High
// }
// if v.Low < k.Low {
// k.Low = v.Low
// }
// k.Volume += v.Volume
// k.Amount += v.Amount
// }
// return k
//}
//// Merge 合并K线,1分钟转成5,15,30分钟等
//func (this Klines) Merge(n int) Klines {
// if this == nil {
// return nil
// }
// ks := []*Kline(nil)
// for i := 0; i < len(this); i += n {
// if i+n > len(this) {
// ks = append(ks, this[i:].Kline())
// } else {
// ks = append(ks, this[i:i+n].Kline())
// }
// }
// return ks
//}

View File

@@ -3,6 +3,7 @@ package protocol
import (
"errors"
"fmt"
"github.com/injoyai/base/types"
"github.com/injoyai/conv"
"time"
)
@@ -121,52 +122,109 @@ func (trade) Decode(bs []byte, c TradeCache) (*TradeResp, error) {
type Trades []*Trade
func (this Trades) Kline() (k *Kline, err error) {
k = &Kline{}
for i, v := range this {
switch i {
// Klines 合并分时成交成k线
func (this Trades) Klines() Klines {
//按天分割
m := make(types.SortMap[int64, Trades])
for _, v := range this {
//获取当天零点的时间戳
unix := time.Date(v.Time.Year(), v.Time.Month(), v.Time.Day(), 0, 0, 0, 0, v.Time.Location()).Unix()
m[unix] = append(m[unix], v)
}
//按天排序
mKline := types.SortMap[int64, Klines]{}
for date, v := range m {
//生成一分钟k线
t := time.Unix(date, 0)
mKline[date] = v.klinesForDay(t)
}
//按时间排序
lss := mKline.Sort()
ls := Klines{}
for _, v := range lss {
ls = append(ls, v...)
}
return ls
}
// Kline 合并分时成交成1个k线,注意分时成交时间保持一致
func (this Trades) Kline(t time.Time, last Price) *Kline {
k := &Kline{
Time: t,
Last: last,
Open: last,
High: last,
Low: last,
Close: last,
}
first := 0
for _, v := range this {
if v.Price <= 0 {
continue
}
switch first {
case 0:
k.Time = v.Time
k.Open = v.Price
k.High = v.Price
k.Low = v.Price
k.Close = v.Price
case len(this) - 1:
default:
k.High = conv.Select(k.High < v.Price, v.Price, k.High)
k.Low = conv.Select(k.Low > v.Price, v.Price, k.Low)
}
k.Close = v.Price
}
k.High = conv.Select(v.Price > k.High, v.Price, k.High)
k.Low = conv.Select(v.Price < k.Low, v.Price, k.Low)
k.Volume += int64(v.Volume)
k.Amount += v.Amount()
k.Order += v.Number
k.Amount += v.Price * Price(v.Volume) * 100
first++
}
return
return k
}
// Klines1 1分K线
func (this Trades) Klines1() (Klines, error) {
m := make(map[int64]Trades)
// kline1 生成一分钟k线,一天
func (this Trades) klinesForDay(date time.Time) Klines {
_930 := 570 //9:30 的分钟
_1130 := 690 //11:30 的分钟
_1300 := 780 //13:00 的分钟
_1500 := 900 //15:00 的分钟
keys := []int(nil)
//早上
m := map[int]Trades{}
for i := 1; i <= 120; i++ {
keys = append(keys, _930+i)
m[_930+i] = []*Trade{}
}
//下午
for i := 1; i <= 120; i++ {
keys = append(keys, _1300+i)
m[_1300+i] = []*Trade{}
}
//获取开盘价,有可能前几分钟没有数据,先遍历一遍
var open Price
for _, v := range this {
//小于9点30的数据归类到9点30
if v.Time.Hour() == 9 && v.Time.Minute() < 30 {
v.Time = time.Date(v.Time.Year(), v.Time.Month(), v.Time.Day(), 9, 30, 0, 0, v.Time.Location())
if v.Price > 0 {
open = v.Price
break
}
//15:00之前和11:30之前+1
if (v.Time.Hour() >= 13 && v.Time.Hour() < 15) || (v.Time.Hour() == 11 && v.Time.Minute() < 30) || v.Time.Hour() < 11 {
v.Time = v.Time.Add(time.Minute)
}
m[v.Time.Unix()] = append(m[v.Time.Unix()], v)
}
ls := Klines(nil)
for _, v := range m {
k, err := v.Kline()
if err != nil {
return nil, err
//分组,按
for _, v := range this {
ms := minutes(v.Time)
t := conv.Select(ms < _930, _930, ms)
t++
t = conv.Select(t > _1130 && t <= _1300, _1130, t)
t = conv.Select(t > _1500, _1500, t)
m[t] = append(m[t], v)
}
//合并
ls := []*Kline(nil)
for _, v := range keys {
k := m[v].Kline(time.Date(date.Year(), date.Month(), date.Day(), v/60, v%60, 0, 0, date.Location()), open)
open = k.Close
ls = append(ls, k)
}
ls.Sort()
return ls, nil
return ls
}
type TradeCache struct {

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
}
}
@@ -262,7 +263,7 @@ func IsStock(code string) bool {
}
func IsSZStock(code string) bool {
return len(code) == 8 && strings.ToLower(code[0:2]) == ExchangeSZ.String() && code[2:3] == "0"
return len(code) == 8 && strings.ToLower(code[0:2]) == ExchangeSZ.String() && (code[2:3] == "0" || code[2:4] == "30")
}
func IsSHStock(code string) bool {
@@ -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
@@ -317,3 +335,7 @@ func AddPrefix(code string) string {
}
return code
}
func minutes(t time.Time) int {
return t.Hour()*60 + t.Minute()
}

View File

@@ -2,20 +2,44 @@ package tdx
import (
"errors"
_ "github.com/glebarez/go-sqlite"
"github.com/injoyai/base/maps"
"github.com/injoyai/conv"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"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"
"xorm.io/core"
"xorm.io/xorm"
)
func NewWorkday(c *Client, filenames ...string) (*Workday, error) {
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) {
//连接数据库
db, err := xorm.NewEngine("mysql", dsn)
if err != nil {
return nil, err
}
db.SetMapper(core.SameMapper{})
return NewWorkday(c, db)
}
func NewWorkdaySqlite(c *Client, filenames ...string) (*Workday, error) {
defaultFilename := filepath.Join(DefaultDatabaseDir, "workday.db")
filename := conv.Default(defaultFilename, filenames...)
@@ -31,6 +55,11 @@ func NewWorkday(c *Client, filenames ...string) (*Workday, error) {
}
db.SetMapper(core.SameMapper{})
db.DB().SetMaxOpenConns(1)
return NewWorkday(c, db)
}
func NewWorkday(c *Client, db *xorm.Engine) (*Workday, error) {
if err := db.Sync2(new(WorkdayModel)); err != nil {
return nil, err
}
@@ -40,12 +69,12 @@ func NewWorkday(c *Client, filenames ...string) (*Workday, error) {
db: db,
cache: maps.NewBit(),
}
//设置定时器,每天早上9点更新数据,8点多获取不到今天的数据
task := cron.New(cron.WithSeconds())
task.AddFunc("0 0 9 * * *", func() {
for i := 0; i < 3; i++ {
if err := w.Update(); err == nil {
err := w.Update()
if err == nil {
return
}
logs.Err(err)
@@ -53,7 +82,6 @@ func NewWorkday(c *Client, filenames ...string) (*Workday, error) {
}
})
task.Start()
return w, w.Update()
}
@@ -87,27 +115,30 @@ func (this *Workday) Update() error {
}
now := time.Now()
if lastWorkday == nil || lastWorkday.Unix < IntegerDay(now).Unix() {
if lastWorkday.Unix < IntegerDay(now).Unix() {
resp, err := this.Client.GetIndexDayAll("sh000001")
if err != nil {
logs.Err(err)
return err
}
return NewSessionFunc(this.db, func(session *xorm.Session) error {
inserts := []any(nil)
for _, v := range resp.List {
if unix := v.Time.Unix(); unix > lastWorkday.Unix {
_, err = session.Insert(&WorkdayModel{Unix: unix, Date: v.Time.Format("20060102"), Is: true})
if err != nil {
return err
}
inserts = append(inserts, &WorkdayModel{Unix: unix, Date: v.Time.Format("20060102")})
this.cache.Set(uint64(unix), true)
}
}
if len(inserts) == 0 {
return nil
})
}
_, err = this.db.Insert(inserts)
return err
}
return nil
}
@@ -125,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) {
@@ -144,24 +173,46 @@ 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) {
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
}
}
}
}
}
// WorkdayModel 工作日
type WorkdayModel struct {
ID int64 `json:"id"` //主键
Unix int64 `json:"unix"` //时间戳
Date string `json:"date"` //日期
Is bool `json:"is"` //是否是工作日
}
func (this *WorkdayModel) TableName() string {