|
|
|
|
|
package tushare
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"market-data-service/adapter"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Adapter Tushare数据源适配器
|
|
|
|
|
|
type Adapter struct {
|
|
|
|
|
|
client *Client
|
|
|
|
|
|
config map[string]string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewAdapter 创建Tushare适配器
|
|
|
|
|
|
func NewAdapter() *Adapter {
|
|
|
|
|
|
return &Adapter{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Connect 建立连接
|
|
|
|
|
|
func (a *Adapter) Connect(config map[string]string) error {
|
|
|
|
|
|
token, ok := config["token"]
|
|
|
|
|
|
if !ok || token == "" {
|
|
|
|
|
|
return fmt.Errorf("tushare token is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.client = NewClient(token)
|
|
|
|
|
|
if baseURL, ok := config["base_url"]; ok && baseURL != "" {
|
|
|
|
|
|
a.client.SetBaseURL(baseURL)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.config = config
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SubscribeTicks 订阅实时Tick(Tushare不支持实时推送,返回错误)
|
|
|
|
|
|
func (a *Adapter) SubscribeTicks(symbols []string, callback adapter.TickCallback) error {
|
|
|
|
|
|
return fmt.Errorf("tushare does not support real-time tick subscription")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FetchKLines 拉取历史K线
|
|
|
|
|
|
func (a *Adapter) FetchKLines(symbol, start, end, freq string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
// 判断是股票还是期货
|
|
|
|
|
|
if strings.Contains(symbol, ".SH") || strings.Contains(symbol, ".SZ") || strings.Contains(symbol, ".BJ") {
|
|
|
|
|
|
return a.fetchStockKLines(symbol, start, end, freq)
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.fetchFuturesKLines(symbol, start, end, freq)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchStockKLines 获取股票K线
|
|
|
|
|
|
func (a *Adapter) fetchStockKLines(symbol, start, end, freq string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
// 转换symbol格式: 000001.SZ -> 000001.SZ
|
|
|
|
|
|
tsCode := symbol
|
|
|
|
|
|
|
|
|
|
|
|
switch freq {
|
|
|
|
|
|
case "1d", "", "day":
|
|
|
|
|
|
return a.fetchStockDaily(tsCode, start, end)
|
|
|
|
|
|
case "1m", "5m", "15m", "30m", "60m":
|
|
|
|
|
|
return a.fetchStockMinute(tsCode, start, end, freq)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, fmt.Errorf("unsupported frequency: %s", freq)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchStockDaily 获取股票日线
|
|
|
|
|
|
func (a *Adapter) fetchStockDaily(tsCode, start, end string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
data, err := a.client.GetStockDaily(tsCode, start, end)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.KLineData, len(data))
|
|
|
|
|
|
for i, d := range data {
|
|
|
|
|
|
tradeDate, _ := time.Parse("20060102", d.TradeDate)
|
|
|
|
|
|
result[i] = adapter.KLineData{
|
|
|
|
|
|
Symbol: d.TSCode,
|
|
|
|
|
|
Time: tradeDate.Unix(),
|
|
|
|
|
|
Open: d.Open,
|
|
|
|
|
|
High: d.High,
|
|
|
|
|
|
Low: d.Low,
|
|
|
|
|
|
Close: d.Close,
|
|
|
|
|
|
Volume: int64(d.Volume * 100), // 手 -> 股
|
|
|
|
|
|
Amount: d.Amount * 1000, // 千元 -> 元
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchStockMinute 获取股票分钟线
|
|
|
|
|
|
func (a *Adapter) fetchStockMinute(tsCode, start, end, freq string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
// 去掉'm'后缀
|
|
|
|
|
|
freqNum := strings.TrimSuffix(freq, "m")
|
|
|
|
|
|
|
|
|
|
|
|
data, err := a.client.GetStockMinute(tsCode, start, end, freqNum)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.KLineData, len(data))
|
|
|
|
|
|
for i, d := range data {
|
|
|
|
|
|
// 解析时间格式: 2025-03-07 09:31:00
|
|
|
|
|
|
tradeTime, _ := time.Parse("2006-01-02 15:04:05", d.TradeTime)
|
|
|
|
|
|
result[i] = adapter.KLineData{
|
|
|
|
|
|
Symbol: d.TSCode,
|
|
|
|
|
|
Time: tradeTime.Unix(),
|
|
|
|
|
|
Open: d.Open,
|
|
|
|
|
|
High: d.High,
|
|
|
|
|
|
Low: d.Low,
|
|
|
|
|
|
Close: d.Close,
|
|
|
|
|
|
Volume: int64(d.Volume * 100),
|
|
|
|
|
|
Amount: d.Amount * 1000,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchFuturesKLines 获取期货K线
|
|
|
|
|
|
func (a *Adapter) fetchFuturesKLines(symbol, start, end, freq string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
switch freq {
|
|
|
|
|
|
case "1d", "", "day":
|
|
|
|
|
|
return a.fetchFuturesDaily(symbol, start, end)
|
|
|
|
|
|
case "1m", "5m", "15m", "30m", "60m":
|
|
|
|
|
|
return a.fetchFuturesMinute(symbol, start, end, freq)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, fmt.Errorf("unsupported frequency: %s", freq)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchFuturesDaily 获取期货日线
|
|
|
|
|
|
func (a *Adapter) fetchFuturesDaily(tsCode, start, end string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
data, err := a.client.GetFuturesDaily(tsCode, start, end)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.KLineData, len(data))
|
|
|
|
|
|
for i, d := range data {
|
|
|
|
|
|
tradeDate, _ := time.Parse("20060102", d.TradeDate)
|
|
|
|
|
|
result[i] = adapter.KLineData{
|
|
|
|
|
|
Symbol: d.TSCode,
|
|
|
|
|
|
Time: tradeDate.Unix(),
|
|
|
|
|
|
Open: d.Open,
|
|
|
|
|
|
High: d.High,
|
|
|
|
|
|
Low: d.Low,
|
|
|
|
|
|
Close: d.Close,
|
|
|
|
|
|
Volume: int64(d.Volume),
|
|
|
|
|
|
Amount: d.Amount * 10000, // 万元 -> 元
|
|
|
|
|
|
OpenInterest: int64(d.OpenInterest),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchFuturesMinute 获取期货分钟线
|
|
|
|
|
|
func (a *Adapter) fetchFuturesMinute(tsCode, start, end, freq string) ([]adapter.KLineData, error) {
|
|
|
|
|
|
freqNum := strings.TrimSuffix(freq, "m")
|
|
|
|
|
|
|
|
|
|
|
|
data, err := a.client.GetFuturesMinute(tsCode, start, end, freqNum)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.KLineData, len(data))
|
|
|
|
|
|
for i, d := range data {
|
|
|
|
|
|
tradeTime, _ := time.Parse("2006-01-02 15:04:05", d.TradeTime)
|
|
|
|
|
|
result[i] = adapter.KLineData{
|
|
|
|
|
|
Symbol: d.TSCode,
|
|
|
|
|
|
Time: tradeTime.Unix(),
|
|
|
|
|
|
Open: d.Open,
|
|
|
|
|
|
High: d.High,
|
|
|
|
|
|
Low: d.Low,
|
|
|
|
|
|
Close: d.Close,
|
|
|
|
|
|
Volume: int64(d.Volume),
|
|
|
|
|
|
Amount: d.Amount * 10000,
|
|
|
|
|
|
OpenInterest: int64(d.OpenInterest),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FetchSymbols 获取标的列表
|
|
|
|
|
|
func (a *Adapter) FetchSymbols(assetType string) ([]adapter.SymbolInfo, error) {
|
|
|
|
|
|
switch assetType {
|
|
|
|
|
|
case "stock":
|
|
|
|
|
|
return a.fetchStockSymbols()
|
|
|
|
|
|
case "futures":
|
|
|
|
|
|
return a.fetchFuturesSymbols()
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, fmt.Errorf("unsupported asset type: %s", assetType)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchStockSymbols 获取股票列表
|
|
|
|
|
|
func (a *Adapter) fetchStockSymbols() ([]adapter.SymbolInfo, error) {
|
|
|
|
|
|
data, err := a.client.GetStockBasic()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.SymbolInfo, 0, len(data))
|
|
|
|
|
|
for _, d := range data {
|
|
|
|
|
|
// 只返回上市状态的
|
|
|
|
|
|
if d.ListStatus != "L" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
result = append(result, adapter.SymbolInfo{
|
|
|
|
|
|
SymbolID: d.TSCode,
|
|
|
|
|
|
Name: d.Name,
|
|
|
|
|
|
Exchange: d.Exchange,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fetchFuturesSymbols 获取期货列表
|
|
|
|
|
|
func (a *Adapter) fetchFuturesSymbols() ([]adapter.SymbolInfo, error) {
|
|
|
|
|
|
data, err := a.client.GetFuturesBasic("")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.SymbolInfo, len(data))
|
|
|
|
|
|
for i, d := range data {
|
|
|
|
|
|
result[i] = adapter.SymbolInfo{
|
|
|
|
|
|
SymbolID: d.TSCode,
|
|
|
|
|
|
Name: d.Name,
|
|
|
|
|
|
Exchange: d.Exchange,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FetchTradingCalendar 获取交易日历
|
|
|
|
|
|
func (a *Adapter) FetchTradingCalendar(exchange, start, end string) ([]adapter.TradeCalData, error) {
|
|
|
|
|
|
// Tushare交易所代码映射
|
|
|
|
|
|
exchangeMap := map[string]string{
|
|
|
|
|
|
"SH": "SSE",
|
|
|
|
|
|
"SZ": "SZSE",
|
|
|
|
|
|
"SHFE": "SHFE",
|
|
|
|
|
|
"DCE": "DCE",
|
|
|
|
|
|
"CZCE": "CZCE",
|
|
|
|
|
|
"CFFEX": "CFFEX",
|
|
|
|
|
|
"INE": "INE",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tsExchange, ok := exchangeMap[exchange]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
tsExchange = "SSE"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data, err := a.client.GetTradeCal(tsExchange, start, end, -1)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]adapter.TradeCalData, len(data))
|
|
|
|
|
|
for i, d := range data {
|
|
|
|
|
|
calDate, _ := time.Parse("20060102", d.CalDate)
|
|
|
|
|
|
result[i] = adapter.TradeCalData{
|
|
|
|
|
|
Date: calDate,
|
|
|
|
|
|
IsTradingDay: d.IsOpen == 1,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HealthCheck 健康检查
|
|
|
|
|
|
func (a *Adapter) HealthCheck() error {
|
|
|
|
|
|
if a.client == nil {
|
|
|
|
|
|
return fmt.Errorf("client not initialized")
|
|
|
|
|
|
}
|
|
|
|
|
|
// 尝试获取交易日历作为健康检查
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
start := now.AddDate(0, 0, -7).Format("20060102")
|
|
|
|
|
|
end := now.Format("20060102")
|
|
|
|
|
|
|
|
|
|
|
|
_, err := a.client.GetTradeCal("SSE", start, end, 1)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Close 关闭连接
|
|
|
|
|
|
func (a *Adapter) Close() error {
|
|
|
|
|
|
// Tushare是HTTP接口,无需关闭
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetClient 获取底层客户端(用于直接调用Tushare API)
|
|
|
|
|
|
func (a *Adapter) GetClient() *Client {
|
|
|
|
|
|
return a.client
|
|
|
|
|
|
}
|