mirror of
https://github.com/injoyai/tdx.git
synced 2025-11-26 21:25:35 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1187e1dcdf | ||
|
|
2adcc9a322 | ||
|
|
b035cc7fbf | ||
|
|
e58130b3c3 | ||
|
|
d840e0d33d | ||
|
|
78843ed6b7 | ||
|
|
04378dadf0 | ||
|
|
e74aeba7b2 | ||
|
|
bea9762c50 |
50
client.go
50
client.go
@@ -157,10 +157,10 @@ func (this *Client) handlerDealMessage(c *client.Client, msg ios.Acker) {
|
||||
resp, err = protocol.MHistoryMinute.Decode(f.Data)
|
||||
|
||||
case protocol.TypeMinuteTrade:
|
||||
resp, err = protocol.MMinuteTrade.Decode(f.Data, conv.String(val))
|
||||
resp, err = protocol.MTrade.Decode(f.Data, val.(protocol.TradeCache))
|
||||
|
||||
case protocol.TypeHistoryMinuteTrade:
|
||||
resp, err = protocol.MHistoryMinuteTrade.Decode(f.Data, conv.String(val))
|
||||
resp, err = protocol.MHistoryTrade.Decode(f.Data, val.(protocol.TradeCache))
|
||||
|
||||
case protocol.TypeKline:
|
||||
resp, err = protocol.MKline.Decode(f.Data, val.(protocol.KlineCache))
|
||||
@@ -316,23 +316,34 @@ func (this *Client) GetHistoryMinute(date, code string) (*protocol.MinuteResp, e
|
||||
return result.(*protocol.MinuteResp), nil
|
||||
}
|
||||
|
||||
func (this *Client) GetTrade(code string, start, count uint16) (*protocol.TradeResp, error) {
|
||||
return this.GetMinuteTrade(code, start, count)
|
||||
}
|
||||
|
||||
// GetMinuteTrade 获取分时交易详情,服务器最多返回1800条,count-start<=1800
|
||||
func (this *Client) GetMinuteTrade(code string, start, count uint16) (*protocol.MinuteTradeResp, error) {
|
||||
func (this *Client) GetMinuteTrade(code string, start, count uint16) (*protocol.TradeResp, error) {
|
||||
code = protocol.AddPrefix(code)
|
||||
f, err := protocol.MMinuteTrade.Frame(code, start, count)
|
||||
f, err := protocol.MTrade.Frame(code, start, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := this.SendFrame(f, code)
|
||||
result, err := this.SendFrame(f, protocol.TradeCache{
|
||||
Date: time.Now().Format("20060102"),
|
||||
Code: code,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*protocol.MinuteTradeResp), nil
|
||||
return result.(*protocol.TradeResp), nil
|
||||
}
|
||||
|
||||
func (this *Client) GetTradeAll(code string) (*protocol.TradeResp, error) {
|
||||
return this.GetMinuteTradeAll(code)
|
||||
}
|
||||
|
||||
// GetMinuteTradeAll 获取分时全部交易详情,todo 只做参考 因为交易实时在进行,然后又是分页读取的,所以会出现读取间隔内产生的交易会丢失
|
||||
func (this *Client) GetMinuteTradeAll(code string) (*protocol.MinuteTradeResp, error) {
|
||||
resp := &protocol.MinuteTradeResp{}
|
||||
func (this *Client) GetMinuteTradeAll(code string) (*protocol.TradeResp, error) {
|
||||
resp := &protocol.TradeResp{}
|
||||
size := uint16(1800)
|
||||
for start := uint16(0); ; start += size {
|
||||
r, err := this.GetMinuteTrade(code, start, size)
|
||||
@@ -349,26 +360,37 @@ func (this *Client) GetMinuteTradeAll(code string) (*protocol.MinuteTradeResp, e
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (this *Client) GetHistoryTrade(date, code string, start, count uint16) (*protocol.HistoryTradeResp, error) {
|
||||
return this.GetHistoryMinuteTrade(date, code, start, count)
|
||||
}
|
||||
|
||||
// GetHistoryMinuteTrade 获取历史分时交易
|
||||
// 只能获取昨天及之前的数据,服务器最多返回2000条,count-start<=2000,如果日期输入错误,则返回0
|
||||
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
|
||||
func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16) (*protocol.HistoryMinuteTradeResp, error) {
|
||||
func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16) (*protocol.HistoryTradeResp, error) {
|
||||
code = protocol.AddPrefix(code)
|
||||
f, err := protocol.MHistoryMinuteTrade.Frame(date, code, start, count)
|
||||
f, err := protocol.MHistoryTrade.Frame(date, code, start, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := this.SendFrame(f, code)
|
||||
result, err := this.SendFrame(f, protocol.TradeCache{
|
||||
Date: date,
|
||||
Code: code,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*protocol.HistoryMinuteTradeResp), nil
|
||||
return result.(*protocol.HistoryTradeResp), nil
|
||||
}
|
||||
|
||||
func (this *Client) GetHistoryTradeAll(date, code string) (*protocol.HistoryTradeResp, error) {
|
||||
return this.GetHistoryMinuteTradeAll(date, code)
|
||||
}
|
||||
|
||||
// GetHistoryMinuteTradeAll 获取历史分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
|
||||
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
|
||||
func (this *Client) GetHistoryMinuteTradeAll(date, code string) (*protocol.HistoryMinuteTradeResp, error) {
|
||||
resp := &protocol.HistoryMinuteTradeResp{}
|
||||
func (this *Client) GetHistoryMinuteTradeAll(date, code string) (*protocol.HistoryTradeResp, error) {
|
||||
resp := &protocol.HistoryTradeResp{}
|
||||
size := uint16(2000)
|
||||
for start := uint16(0); ; start += size {
|
||||
r, err := this.GetHistoryMinuteTrade(date, code, start, size)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func main() {
|
||||
common.Test(func(c *tdx.Client) {
|
||||
resp, err := c.GetHistoryMinuteTrade("20241025", "sz000001", 0, 20)
|
||||
resp, err := c.GetHistoryMinuteTrade("20250609", "sz000001", 0, 20)
|
||||
logs.PanicErr(err)
|
||||
|
||||
for _, v := range resp.List {
|
||||
19
example/PullTrade/main.go
Normal file
19
example/PullTrade/main.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/injoyai/logs"
|
||||
"github.com/injoyai/tdx"
|
||||
"github.com/injoyai/tdx/extend"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
pt := extend.NewPullTrade("./data/trade")
|
||||
|
||||
m, err := tdx.NewManage(nil)
|
||||
logs.PanicErr(err)
|
||||
|
||||
err = pt.Pull(m, 2025, "sz000001")
|
||||
logs.Err(err)
|
||||
|
||||
}
|
||||
150
extend/pull-trade.go
Normal file
150
extend/pull-trade.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package extend
|
||||
|
||||
import (
|
||||
"github.com/injoyai/conv"
|
||||
"github.com/injoyai/logs"
|
||||
"github.com/injoyai/tdx"
|
||||
"github.com/injoyai/tdx/protocol"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewPullTrade(dir string) *PullTrade {
|
||||
return &PullTrade{
|
||||
Dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
type PullTrade struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (this *PullTrade) Pull(m *tdx.Manage, year int, code string) (err error) {
|
||||
|
||||
tss := protocol.Trades{}
|
||||
kss1 := protocol.Klines(nil)
|
||||
kss5 := protocol.Klines(nil)
|
||||
kss15 := protocol.Klines(nil)
|
||||
kss30 := protocol.Klines(nil)
|
||||
kss60 := protocol.Klines(nil)
|
||||
|
||||
m.Workday.RangeYear(year, func(t time.Time) bool {
|
||||
date := t.Format("20060102")
|
||||
|
||||
var resp *protocol.HistoryTradeResp
|
||||
err = m.Do(func(c *tdx.Client) error {
|
||||
resp, err = c.GetHistoryTradeAll(date, code)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
logs.Err(err)
|
||||
return false
|
||||
}
|
||||
|
||||
tss = append(tss, resp.List...)
|
||||
|
||||
//转成分时K线
|
||||
ks, err := resp.List.Klines1()
|
||||
if err != nil {
|
||||
logs.Err(err)
|
||||
return false
|
||||
}
|
||||
|
||||
kss1 = append(kss1, ks...)
|
||||
kss5 = append(kss5, ks.Merge(5)...)
|
||||
kss15 = append(kss5, ks.Merge(15)...)
|
||||
kss30 = append(kss5, ks.Merge(30)...)
|
||||
kss60 = append(kss5, ks.Merge(60)...)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
_ = kss5
|
||||
_ = kss15
|
||||
_ = kss30
|
||||
_ = kss60
|
||||
|
||||
filename := filepath.Join(this.Dir, conv.String(year), "分时成交", code+".csv")
|
||||
filename1 := filepath.Join(this.Dir, conv.String(year), "1分钟", code+".csv")
|
||||
filename5 := filepath.Join(this.Dir, conv.String(year), "5分钟", code+".csv")
|
||||
filename15 := filepath.Join(this.Dir, conv.String(year), "15分钟", code+".csv")
|
||||
filename30 := filepath.Join(this.Dir, conv.String(year), "30分钟", code+".csv")
|
||||
filename60 := filepath.Join(this.Dir, conv.String(year), "60分钟", code+".csv")
|
||||
name := m.Codes.GetName(code)
|
||||
|
||||
err = TradeToCsv(filename, tss)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = KlinesToCsv(filename1, code, name, kss1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = KlinesToCsv(filename5, code, name, kss5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = KlinesToCsv(filename15, code, name, kss15)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = KlinesToCsv(filename30, code, name, kss30)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = KlinesToCsv(filename60, code, name, kss60)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func KlinesToCsv(filename string, code, name string, ks protocol.Klines) error {
|
||||
data := [][]any{{"日期", "时间", "代码", "名称", "开盘", "最高", "最低", "收盘", "总手", "金额"}}
|
||||
for _, v := range ks {
|
||||
data = append(data, []any{
|
||||
v.Time.Format("20060102"),
|
||||
v.Time.Format("15:04"),
|
||||
code,
|
||||
name,
|
||||
v.Open.Float64(),
|
||||
v.High.Float64(),
|
||||
v.Low.Float64(),
|
||||
v.Close.Float64(),
|
||||
v.Volume,
|
||||
v.Amount.Float64(),
|
||||
})
|
||||
}
|
||||
|
||||
buf, err := toCsv(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return newFile(filename, buf)
|
||||
}
|
||||
|
||||
func TradeToCsv(filename string, ts protocol.Trades) error {
|
||||
data := [][]any{{"日期", "时间", "价格", "成交量(手)", "成交额", "方向(0买,1卖)"}}
|
||||
for _, v := range ts {
|
||||
data = append(data, []any{
|
||||
v.Time.Format(time.DateOnly),
|
||||
v.Time.Format("15:04"),
|
||||
v.Price.Float64(),
|
||||
v.Volume,
|
||||
v.Amount().Float64(),
|
||||
v.Status,
|
||||
})
|
||||
}
|
||||
buf, err := toCsv(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return newFile(filename, buf)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/injoyai/conv"
|
||||
"github.com/injoyai/tdx"
|
||||
"github.com/injoyai/tdx/protocol"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -18,6 +19,51 @@ const (
|
||||
THS_HFQ uint8 = 2 //后复权
|
||||
)
|
||||
|
||||
/*
|
||||
GetTHSDayKlineFull
|
||||
获取[不复权,前复权,后复权]数据,并补充成交金额数据
|
||||
前复权,和通达信对的上,和东方财富对不上
|
||||
后复权,和通达信,东方财富都对不上
|
||||
*/
|
||||
func GetTHSDayKlineFull(code string, c *tdx.Client) ([3][]*Kline, error) {
|
||||
resp, err := c.GetKlineDayAll(code)
|
||||
if err != nil {
|
||||
return [3][]*Kline{}, err
|
||||
}
|
||||
mAmount := make(map[int64]protocol.Price)
|
||||
bfq := []*Kline(nil)
|
||||
for _, v := range resp.List {
|
||||
mAmount[v.Time.Unix()] = v.Amount
|
||||
bfq = append(bfq, &Kline{
|
||||
Code: code,
|
||||
Date: v.Time.Unix(),
|
||||
Open: v.Open,
|
||||
High: v.High,
|
||||
Low: v.Low,
|
||||
Close: v.Close,
|
||||
Volume: v.Volume,
|
||||
Amount: v.Amount,
|
||||
})
|
||||
}
|
||||
//前复权
|
||||
qfq, err := GetTHSDayKline(code, THS_QFQ)
|
||||
if err != nil {
|
||||
return [3][]*Kline{}, err
|
||||
}
|
||||
for i := range qfq {
|
||||
qfq[i].Amount = mAmount[qfq[i].Date]
|
||||
}
|
||||
//后复权
|
||||
hfq, err := GetTHSDayKline(code, THS_HFQ)
|
||||
if err != nil {
|
||||
return [3][]*Kline{}, err
|
||||
}
|
||||
for i := range hfq {
|
||||
hfq[i].Amount = mAmount[hfq[i].Date]
|
||||
}
|
||||
return [3][]*Kline{bfq, qfq, hfq}, nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetTHSDayKline
|
||||
前复权,和通达信对的上,和东方财富对不上
|
||||
58
extend/util.go
Normal file
58
extend/util.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package extend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"github.com/injoyai/conv"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func toCsv(data [][]interface{}) (*bytes.Buffer, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.WriteString("\xEF\xBB\xBF")
|
||||
w := csv.NewWriter(buf)
|
||||
for _, rows := range data {
|
||||
if err := w.Write(conv.Strings(rows)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// newFile 新建文件,会覆盖
|
||||
func newFile(filename string, v ...interface{}) error {
|
||||
if len(v) == 0 {
|
||||
return os.MkdirAll(filename, 0777)
|
||||
}
|
||||
dir, name := filepath.Split(filename)
|
||||
if len(dir) > 0 {
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
for _, k := range v {
|
||||
switch r := k.(type) {
|
||||
case nil:
|
||||
case io.Reader:
|
||||
if _, err = io.Copy(f, r); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if _, err = f.Write(conv.Bytes(r)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,16 +5,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
MConnect = connect{}
|
||||
MHeart = heart{}
|
||||
MCount = count{}
|
||||
MQuote = quote{}
|
||||
MCode = code{}
|
||||
MMinute = minute{}
|
||||
MHistoryMinute = historyMinute{}
|
||||
MMinuteTrade = minuteTrade{}
|
||||
MHistoryMinuteTrade = historyMinuteTrade{}
|
||||
MKline = kline{}
|
||||
MConnect = connect{}
|
||||
MHeart = heart{}
|
||||
MCount = count{}
|
||||
MQuote = quote{}
|
||||
MCode = code{}
|
||||
MMinute = minute{}
|
||||
MHistoryMinute = historyMinute{}
|
||||
MTrade = trade{}
|
||||
MHistoryTrade = historyTrade{}
|
||||
MKline = kline{}
|
||||
)
|
||||
|
||||
type ConnectResp struct {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/injoyai/conv"
|
||||
)
|
||||
|
||||
// HistoryMinuteTradeResp 历史分时交易比实时少了单量
|
||||
type HistoryMinuteTradeResp struct {
|
||||
Count uint16
|
||||
List []*HistoryMinuteTrade
|
||||
}
|
||||
|
||||
type HistoryMinuteTrade struct {
|
||||
Time string //时间
|
||||
Price Price //价格
|
||||
Volume int //成交量
|
||||
Status int //0是买,1是卖,2无效(汇总出现)中途也可能出现2,例20241115(sz000001)的14:56
|
||||
}
|
||||
|
||||
func (this *HistoryMinuteTrade) String() string {
|
||||
return fmt.Sprintf("%s \t%s \t%-6s \t%-6d(手) \t%-4s", this.Time, this.Price, this.Amount(), this.Volume, this.StatusString())
|
||||
}
|
||||
|
||||
// Amount 成交额
|
||||
func (this *HistoryMinuteTrade) Amount() Price {
|
||||
return this.Price * Price(this.Volume*100)
|
||||
}
|
||||
|
||||
func (this *HistoryMinuteTrade) StatusString() string {
|
||||
switch this.Status {
|
||||
case 0:
|
||||
return "买入"
|
||||
case 1:
|
||||
return "卖出"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type historyMinuteTrade struct{}
|
||||
|
||||
func (historyMinuteTrade) Frame(date, code string, start, count uint16) (*Frame, error) {
|
||||
exchange, number, err := DecodeCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataBs := Bytes(conv.Uint32(date)) //req.Time.Format("20060102"))
|
||||
dataBs = append(dataBs, exchange.Uint8(), 0x0)
|
||||
dataBs = append(dataBs, []byte(number)...)
|
||||
dataBs = append(dataBs, Bytes(start)...)
|
||||
dataBs = append(dataBs, Bytes(count)...)
|
||||
return &Frame{
|
||||
Control: Control01,
|
||||
Type: TypeHistoryMinuteTrade,
|
||||
Data: dataBs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (historyMinuteTrade) Decode(bs []byte, code string) (*HistoryMinuteTradeResp, error) {
|
||||
if len(bs) < 2 {
|
||||
return nil, errors.New("数据长度不足")
|
||||
}
|
||||
|
||||
_, number, err := DecodeCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &HistoryMinuteTradeResp{
|
||||
Count: Uint16(bs[:2]),
|
||||
}
|
||||
|
||||
//第2-6字节不知道是啥
|
||||
bs = bs[2+4:]
|
||||
|
||||
lastPrice := Price(0)
|
||||
for i := uint16(0); i < resp.Count; i++ {
|
||||
mt := &HistoryMinuteTrade{
|
||||
Time: GetHourMinute([2]byte(bs[:2])),
|
||||
}
|
||||
var sub Price
|
||||
bs, sub = GetPrice(bs[2:])
|
||||
lastPrice += sub * 10 //把分转成厘
|
||||
mt.Price = lastPrice / basePrice(number)
|
||||
bs, mt.Volume = CutInt(bs)
|
||||
bs, mt.Status = CutInt(bs)
|
||||
bs, _ = CutInt(bs) //这个得到的是0,不知道是啥
|
||||
resp.List = append(resp.List, mt)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
70
protocol/model_history_trade.go
Normal file
70
protocol/model_history_trade.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/injoyai/conv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HistoryTradeResp 历史分时交易比实时少了单量
|
||||
type HistoryTradeResp struct {
|
||||
Count uint16
|
||||
List Trades
|
||||
}
|
||||
|
||||
type historyTrade struct{}
|
||||
|
||||
func (historyTrade) Frame(date, code string, start, count uint16) (*Frame, error) {
|
||||
exchange, number, err := DecodeCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataBs := Bytes(conv.Uint32(date)) //req.Time.Format("20060102"))
|
||||
dataBs = append(dataBs, exchange.Uint8(), 0x0)
|
||||
dataBs = append(dataBs, []byte(number)...)
|
||||
dataBs = append(dataBs, Bytes(start)...)
|
||||
dataBs = append(dataBs, Bytes(count)...)
|
||||
return &Frame{
|
||||
Control: Control01,
|
||||
Type: TypeHistoryMinuteTrade,
|
||||
Data: dataBs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (historyTrade) Decode(bs []byte, c TradeCache) (*HistoryTradeResp, error) {
|
||||
if len(bs) < 2 {
|
||||
return nil, errors.New("数据长度不足")
|
||||
}
|
||||
|
||||
_, number, err := DecodeCode(c.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &HistoryTradeResp{
|
||||
Count: Uint16(bs[:2]),
|
||||
}
|
||||
|
||||
//第2-6字节不知道是啥
|
||||
bs = bs[2+4:]
|
||||
|
||||
lastPrice := Price(0)
|
||||
for i := uint16(0); i < resp.Count; i++ {
|
||||
timeStr := GetHourMinute([2]byte(bs[:2]))
|
||||
t, err := time.Parse("2006010215:04", c.Date+timeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mt := &Trade{Time: t}
|
||||
var sub Price
|
||||
bs, sub = GetPrice(bs[2:])
|
||||
lastPrice += sub * 10 //把分转成厘
|
||||
mt.Price = lastPrice / basePrice(number)
|
||||
bs, mt.Volume = CutInt(bs)
|
||||
bs, mt.Status = CutInt(bs)
|
||||
bs, _ = CutInt(bs) //这个得到的是0,不知道是啥
|
||||
resp.List = append(resp.List, mt)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
func Test_stockHistoryMinuteTrade_Frame(t *testing.T) {
|
||||
// 预期 0c 02000000 00 1200 1200 b50f 84da3401 0000 30303030303100006400
|
||||
// 0c000000000112001200b50f84da3401000030303030303100006400
|
||||
f, err := MHistoryMinuteTrade.Frame("20241028", "sz000001", 0, 100)
|
||||
f, err := MHistoryTrade.Frame("20241028", "sz000001", 0, 100)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/injoyai/base/types"
|
||||
"github.com/injoyai/conv"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -209,3 +210,66 @@ func FixKlineTime(ks []*Kline) []*Kline {
|
||||
}
|
||||
return ks
|
||||
}
|
||||
|
||||
type Klines []*Kline
|
||||
|
||||
func (this Klines) Len() int {
|
||||
return len(this)
|
||||
}
|
||||
|
||||
func (this Klines) Swap(i, j int) {
|
||||
this[i], this[j] = this[j], this[i]
|
||||
}
|
||||
|
||||
func (this Klines) Less(i, j int) bool {
|
||||
return this[i].Time.Before(this[j].Time)
|
||||
}
|
||||
|
||||
func (this Klines) Sort() {
|
||||
sort.Sort(this)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type MinuteTradeResp struct {
|
||||
Count uint16
|
||||
List []*MinuteTrade
|
||||
}
|
||||
|
||||
// MinuteTrade 分时成交,todo 时间没有到秒,客户端上也没有,东方客户端能显示秒
|
||||
type MinuteTrade struct {
|
||||
Time string //时间
|
||||
Price Price //价格
|
||||
Volume int //成交量
|
||||
Number int //单数,历史数据该字段无效
|
||||
Status int //0是买,1是卖,2无效(汇总出现)
|
||||
}
|
||||
|
||||
func (this *MinuteTrade) String() string {
|
||||
return fmt.Sprintf("%s \t%-6s \t%-6s \t%-6d(手) \t%-4d(单) \t%-4s",
|
||||
this.Time, this.Price, this.Amount(), this.Volume, this.Number, this.StatusString())
|
||||
}
|
||||
|
||||
// Amount 成交额
|
||||
func (this *MinuteTrade) Amount() Price {
|
||||
return this.Price * Price(this.Volume) * 100
|
||||
}
|
||||
|
||||
func (this *MinuteTrade) StatusString() string {
|
||||
switch this.Status {
|
||||
case 0:
|
||||
return "买入"
|
||||
case 1:
|
||||
return "卖出"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// AvgVolume 平均每单成交量
|
||||
func (this *MinuteTrade) AvgVolume() float64 {
|
||||
return float64(this.Volume) / float64(this.Number)
|
||||
}
|
||||
|
||||
// AvgPrice 平均每单成交金额
|
||||
func (this *MinuteTrade) AvgPrice() Price {
|
||||
return Price(this.AvgVolume() * float64(this.Price) * 100)
|
||||
}
|
||||
|
||||
// IsBuy 是否是买单
|
||||
func (this *MinuteTrade) IsBuy() bool {
|
||||
return this.Status == 0
|
||||
}
|
||||
|
||||
// IsSell 是否是卖单
|
||||
func (this *MinuteTrade) IsSell() bool {
|
||||
return this.Status == 1
|
||||
}
|
||||
|
||||
type minuteTrade struct{}
|
||||
|
||||
func (minuteTrade) Frame(code string, start, count uint16) (*Frame, error) {
|
||||
exchange, number, err := DecodeCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codeBs := []byte(number)
|
||||
codeBs = append(codeBs, Bytes(start)...)
|
||||
codeBs = append(codeBs, Bytes(count)...)
|
||||
return &Frame{
|
||||
Control: Control01,
|
||||
Type: TypeMinuteTrade,
|
||||
Data: append([]byte{exchange.Uint8(), 0x0}, codeBs...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (minuteTrade) Decode(bs []byte, code string) (*MinuteTradeResp, error) {
|
||||
|
||||
var err error
|
||||
_, code, err = DecodeCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(bs) < 2 {
|
||||
return nil, errors.New("数据长度不足")
|
||||
}
|
||||
|
||||
resp := &MinuteTradeResp{
|
||||
Count: Uint16(bs[:2]),
|
||||
}
|
||||
|
||||
bs = bs[2:]
|
||||
|
||||
lastPrice := Price(0)
|
||||
for i := uint16(0); i < resp.Count; i++ {
|
||||
mt := &MinuteTrade{
|
||||
Time: GetHourMinute([2]byte(bs[:2])),
|
||||
}
|
||||
var sub Price
|
||||
bs, sub = GetPrice(bs[2:])
|
||||
lastPrice += sub * 10 //把分转换成厘
|
||||
mt.Price = lastPrice / basePrice(code)
|
||||
bs, mt.Volume = CutInt(bs)
|
||||
bs, mt.Number = CutInt(bs)
|
||||
bs, mt.Status = CutInt(bs)
|
||||
bs, _ = CutInt(bs) //这个得到的是0,不知道是啥
|
||||
resp.List = append(resp.List, mt)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
175
protocol/model_trade.go
Normal file
175
protocol/model_trade.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/injoyai/conv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TradeResp struct {
|
||||
Count uint16
|
||||
List Trades
|
||||
}
|
||||
|
||||
// Trade 分时成交,todo 时间没有到秒,客户端上也没有,东方客户端能显示秒
|
||||
type Trade struct {
|
||||
Time time.Time //时间, 09:30
|
||||
Price Price //价格
|
||||
Volume int //成交量,手
|
||||
Status int //0是买,1是卖,2中性/汇总 中途也可能出现2,例20241115(sz000001)的14:56
|
||||
Number int //单数,历史数据该字段无效
|
||||
}
|
||||
|
||||
func (this *Trade) String() string {
|
||||
return fmt.Sprintf("%s \t%-6s \t%-6s \t%-6d(手) \t%-4d(单) \t%-4s",
|
||||
this.Time, this.Price, this.Amount(), this.Volume, this.Number, this.StatusString())
|
||||
}
|
||||
|
||||
// Amount 成交额
|
||||
func (this *Trade) Amount() Price {
|
||||
return this.Price * Price(this.Volume*100)
|
||||
}
|
||||
|
||||
func (this *Trade) StatusString() string {
|
||||
switch this.Status {
|
||||
case 0:
|
||||
return "买入"
|
||||
case 1:
|
||||
return "卖出"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// AvgVolume 平均每单成交量
|
||||
func (this *Trade) AvgVolume() float64 {
|
||||
return float64(this.Volume) / float64(this.Number)
|
||||
}
|
||||
|
||||
// AvgPrice 平均每单成交金额
|
||||
func (this *Trade) AvgPrice() Price {
|
||||
return Price(this.AvgVolume() * float64(this.Price) * 100)
|
||||
}
|
||||
|
||||
// IsBuy 是否是买单
|
||||
func (this *Trade) IsBuy() bool {
|
||||
return this.Status == 0
|
||||
}
|
||||
|
||||
// IsSell 是否是卖单
|
||||
func (this *Trade) IsSell() bool {
|
||||
return this.Status == 1
|
||||
}
|
||||
|
||||
type trade struct{}
|
||||
|
||||
func (trade) Frame(code string, start, count uint16) (*Frame, error) {
|
||||
exchange, number, err := DecodeCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codeBs := []byte(number)
|
||||
codeBs = append(codeBs, Bytes(start)...)
|
||||
codeBs = append(codeBs, Bytes(count)...)
|
||||
return &Frame{
|
||||
Control: Control01,
|
||||
Type: TypeMinuteTrade,
|
||||
Data: append([]byte{exchange.Uint8(), 0x0}, codeBs...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (trade) Decode(bs []byte, c TradeCache) (*TradeResp, error) {
|
||||
|
||||
_, code, err := DecodeCode(c.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(bs) < 2 {
|
||||
return nil, errors.New("数据长度不足")
|
||||
}
|
||||
|
||||
resp := &TradeResp{
|
||||
Count: Uint16(bs[:2]),
|
||||
}
|
||||
|
||||
bs = bs[2:]
|
||||
|
||||
lastPrice := Price(0)
|
||||
for i := uint16(0); i < resp.Count; i++ {
|
||||
timeStr := GetHourMinute([2]byte(bs[:2]))
|
||||
t, err := time.Parse("2006010215:04", c.Date+timeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mt := &Trade{Time: t}
|
||||
var sub Price
|
||||
bs, sub = GetPrice(bs[2:])
|
||||
lastPrice += sub * 10 //把分转换成厘
|
||||
mt.Price = lastPrice / basePrice(code)
|
||||
bs, mt.Volume = CutInt(bs)
|
||||
bs, mt.Number = CutInt(bs)
|
||||
bs, mt.Status = CutInt(bs)
|
||||
bs, _ = CutInt(bs) //这个得到的是0,不知道是啥
|
||||
resp.List = append(resp.List, mt)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type Trades []*Trade
|
||||
|
||||
func (this Trades) Kline() (k *Kline, err error) {
|
||||
k = &Kline{}
|
||||
for i, v := range this {
|
||||
switch i {
|
||||
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
|
||||
}
|
||||
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()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
ls := Klines(nil)
|
||||
for _, v := range m {
|
||||
k, err := v.Kline()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls = append(ls, k)
|
||||
}
|
||||
ls.Sort()
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
type TradeCache struct {
|
||||
Date string //日期
|
||||
Code string //计算倍数
|
||||
}
|
||||
@@ -271,15 +271,24 @@ func IsETF(code string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// AddPrefix 添加股票代码前缀,针对股票生效,例如000001,会增加前缀sz000001(平安银行),而不是sh000001(上证指数)
|
||||
// AddPrefix 添加股票/基金代码前缀,针对股票/基金生效,例如000001,会增加前缀sz000001(平安银行),而不是sh000001(上证指数)
|
||||
func AddPrefix(code string) string {
|
||||
if len(code) == 6 {
|
||||
switch {
|
||||
case code[:1] == "6":
|
||||
//上海股票
|
||||
code = ExchangeSH.String() + code
|
||||
case code[:1] == "0":
|
||||
//深圳股票
|
||||
code = ExchangeSZ.String() + code
|
||||
case code[:2] == "30":
|
||||
//深圳股票
|
||||
code = ExchangeSZ.String() + code
|
||||
case code[:3] == "510" || code[:3] == "511" || code[:3] == "512" || code[:3] == "513" || code[:3] == "515":
|
||||
//上海基金
|
||||
code = ExchangeSH.String() + code
|
||||
case code[:3] == "159":
|
||||
//深圳基金
|
||||
code = ExchangeSZ.String() + code
|
||||
}
|
||||
}
|
||||
|
||||
25
workday.go
25
workday.go
@@ -4,7 +4,9 @@ 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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -116,6 +118,29 @@ func (this *Workday) TodayIs() bool {
|
||||
return this.Is(time.Now())
|
||||
}
|
||||
|
||||
// RangeYear 遍历一年的所有工作日
|
||||
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),
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Range 遍历指定范围的工作日
|
||||
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) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RangeDesc 倒序遍历工作日,从今天-1990年12月19日(上海交易所成立时间)
|
||||
func (this *Workday) RangeDesc(f func(t time.Time) bool) {
|
||||
t := IntegerDay(time.Now())
|
||||
|
||||
Reference in New Issue
Block a user