Compare commits

...

7 Commits

10 changed files with 322 additions and 69 deletions

View File

@@ -422,7 +422,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 +439,36 @@ 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) (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
for ; start.Before(time.Now()); start = start.Add(time.Hour * 24) {
res, err = this.GetHistoryTradeDay(start.Format("20060102"), code)
if err != nil {
return nil, err
}
ls = append(ls, res.List...)
}
return ls, nil
}
// GetHistoryMinuteTradeAll 获取历史分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
func (this *Client) GetHistoryMinuteTradeAll(date, code string) (*protocol.TradeResp, error) {
// 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 {

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

@@ -8,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetHistoryMinuteTradeAll("20241025", "sz000001")
resp, err := c.GetHistoryMinuteTradeDay("20251010", "sh000001")
logs.PanicErr(err)
for _, v := range resp.List {

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

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

@@ -0,0 +1,66 @@
package extend
import (
"encoding/json"
"fmt"
"github.com/injoyai/conv"
"github.com/injoyai/tdx"
"io"
"net/http"
"path/filepath"
)
func ListenCodesHTTP(port int, filename ...string) error {
code, err := tdx.DialCodes(conv.Default(filepath.Join(tdx.DefaultDatabaseDir, "codes.db"), filename...))
if err != nil {
return nil
}
return http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/stocks":
ls := code.GetStocks()
w.WriteHeader(http.StatusOK)
w.Write(conv.Bytes(ls))
case "/etfs":
ls := code.GetETFs()
w.WriteHeader(http.StatusOK)
w.Write(conv.Bytes(ls))
default:
http.NotFound(w, r)
}
}))
}
func DialCodesHTTP(address string) *CodesHTTP {
return &CodesHTTP{address: address}
}
type CodesHTTP struct {
address string
}
func (this *CodesHTTP) getList(path string) ([]string, 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 := []string(nil)
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

@@ -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)...)

View File

@@ -233,6 +233,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 +257,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 +274,87 @@ func (this Klines) Kline() *Kline {
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
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.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())
} else {
ks = append(ks, this[i:i+n].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 {
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,108 @@ 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:
k.Close = v.Price
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.High = conv.Select(v.Price > k.High, v.Price, k.High)
k.Low = conv.Select(v.Price < k.Low, v.Price, k.Low)
k.Close = v.Price
k.Volume += int64(v.Volume)
k.Amount += v.Amount()
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)
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())
}
//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)
// 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{}
}
ls := Klines(nil)
for _, v := range m {
k, err := v.Kline()
if err != nil {
return nil, err
//下午
for i := 1; i <= 120; i++ {
keys = append(keys, _1300+i)
m[_1300+i] = []*Trade{}
}
//获取开盘价,有可能前几分钟没有数据,先遍历一遍
var open Price
for _, v := range this {
if v.Price > 0 {
open = v.Price
break
}
}
//分组,按
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

@@ -262,7 +262,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 {
@@ -317,3 +317,7 @@ func AddPrefix(code string) string {
}
return code
}
func minutes(t time.Time) int {
return t.Hour()*60 + t.Minute()
}