diff --git a/conv/conv.go b/conv/conv.go index 4b39517..cb9b960 100644 --- a/conv/conv.go +++ b/conv/conv.go @@ -99,14 +99,36 @@ func AnyToFloat64(v any) float64 { switch val := v.(type) { case float64: return val - case string: - return String2Float64(val) case float32: return float64(val) case int: return float64(val) + case int8: + return float64(val) + case int16: + return float64(val) + case int32: + return float64(val) + case int64: + return float64(val) case uint: return float64(val) + case uint8: + return float64(val) + case uint16: + return float64(val) + case uint32: + return float64(val) + case uint64: + return float64(val) + case string: + return String2Float64(val) + case json.Number: + f, err := val.Float64() + if err != nil { + return 0 + } + return f default: return 0 } diff --git a/markdown/builder.go b/markdown/builder.go index 351193c..b574d96 100644 --- a/markdown/builder.go +++ b/markdown/builder.go @@ -72,7 +72,7 @@ func (m *Builder) Build() string { } func (m *Builder) SaveToFile(filePath string) error { - rf, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + rf, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } diff --git a/schema/blocks_index.go b/schema/blocks_index.go deleted file mode 100644 index 85e3713..0000000 --- a/schema/blocks_index.go +++ /dev/null @@ -1,28 +0,0 @@ -package schema - -import "gorm.io/gorm" - -// BlocksIndex 板块索引(采用 dataset/stock 的 code 唯一约束)。 -type BlocksIndex struct { - gorm.Model - Code string `gorm:"type:varchar(50);not null;default:'';comment:板块代码;uniqueIndex:uq_blocks_index_code" json:"code"` - Name string `gorm:"type:varchar(50);not null;default:'';comment:板块名称" json:"name"` - IsRecommend bool `gorm:"type:bool;not null;default:false;comment:是否推荐" json:"is_recommend"` -} - -func (BlocksIndex) TableName() string { - return "blocks_index" -} - -// Key 板块业务主键:code。 -func (b *BlocksIndex) Key() string { - return b.Code -} - -// DisplayName 展示名称:非空 name,否则回退 code。 -func (b *BlocksIndex) DisplayName() string { - if b.Name != "" { - return b.Name - } - return b.Code -} diff --git a/schema/blocks_member.go b/schema/blocks_member.go deleted file mode 100644 index 23be117..0000000 --- a/schema/blocks_member.go +++ /dev/null @@ -1,28 +0,0 @@ -package schema - -import "gorm.io/gorm" - -// BlocksMember 板块成分股。 -type BlocksMember struct { - gorm.Model - TiCode string `gorm:"type:varchar(50);not null;default:'';comment:板块代码;index" json:"ti_code"` - StockCode string `gorm:"type:varchar(50);not null;default:'';comment:股票代码;index" json:"stock_code"` - Weight float64 `gorm:"type:float;not null;default:0;comment:权重" json:"weight"` -} - -func (BlocksMember) TableName() string { - return "blocks_member" -} - -// Key 成分在板块内的键:ti_code + stock_code。 -func (b *BlocksMember) Key() string { - if b.TiCode == "" && b.StockCode == "" { - return "" - } - return b.TiCode + "/" + b.StockCode -} - -// HasWeight 是否带权重量化成分。 -func (b *BlocksMember) HasWeight() bool { - return b.Weight > 0 -} diff --git a/schema/stock_basic.go b/schema/dataset_basic.go similarity index 80% rename from schema/stock_basic.go rename to schema/dataset_basic.go index 8e789d1..a6f4014 100644 --- a/schema/stock_basic.go +++ b/schema/dataset_basic.go @@ -2,8 +2,8 @@ package schema import "gorm.io/gorm" -// StockBasic 股票基本信息表(合并 dataset/stock 与 gostock 字段;gostock 独有 Level、Desc)。 -type StockBasic struct { +// DatasetStockBasic 股票基本信息表(合并 dataset/stock 与 gostock 字段;gostock 独有 Level、Desc)。 +type DatasetStockBasic struct { gorm.Model TsCode string `gorm:"type:varchar(50);not null;index;comment:TS代码"` Symbol string `gorm:"type:varchar(50);not null;comment:股票代码"` @@ -21,17 +21,17 @@ type StockBasic struct { ActEntType string `gorm:"type:varchar(50);not null;default:'';comment:实控人企业性质"` } -func (StockBasic) TableName() string { - return "stock_basic" +func (DatasetStockBasic) TableName() string { + return "dataset_stock_basic" } // Key 业务主键:TS 代码。 -func (s *StockBasic) Key() string { +func (s *DatasetStockBasic) Key() string { return s.TsCode } // DisplaySymbol 展示用代码:有 symbol 用 symbol,否则用 ts_code。 -func (s *StockBasic) DisplaySymbol() string { +func (s *DatasetStockBasic) DisplaySymbol() string { if s.Symbol != "" { return s.Symbol } @@ -39,6 +39,6 @@ func (s *StockBasic) DisplaySymbol() string { } // IsNorthbound 是否沪深港通标的(H 沪股通 / S 深股通)。 -func (s *StockBasic) IsNorthbound() bool { +func (s *DatasetStockBasic) IsNorthbound() bool { return s.IsHS == "H" || s.IsHS == "S" } diff --git a/schema/dataset_blocks_index.go b/schema/dataset_blocks_index.go new file mode 100644 index 0000000..8605d83 --- /dev/null +++ b/schema/dataset_blocks_index.go @@ -0,0 +1,28 @@ +package schema + +import "gorm.io/gorm" + +// DatasetBlocksIndex 板块索引(采用 dataset/stock 的 code 唯一约束)。 +type DatasetBlocksIndex struct { + gorm.Model + TiCode string `gorm:"type:varchar(50);not null;default:'';comment:指数代码;uniqueIndex:uq_blocks_index_code" json:"ti_code"` + Name string `gorm:"type:varchar(50);not null;default:'';comment:板块名称" json:"name"` + IsRecommend bool `gorm:"type:bool;not null;default:false;comment:是否推荐" json:"is_recommend"` +} + +func (DatasetBlocksIndex) TableName() string { + return "dataset_blocks_index" +} + +// Key 板块业务主键:code。 +func (b *DatasetBlocksIndex) Key() string { + return b.TiCode +} + +// DisplayName 展示名称:非空 name,否则回退 code。 +func (b *DatasetBlocksIndex) DisplayName() string { + if b.Name != "" { + return b.Name + } + return b.TiCode +} diff --git a/schema/dataset_blocks_member.go b/schema/dataset_blocks_member.go new file mode 100644 index 0000000..0a76ee9 --- /dev/null +++ b/schema/dataset_blocks_member.go @@ -0,0 +1,28 @@ +package schema + +import "gorm.io/gorm" + +// DatasetBlocksMember 板块成分股。 +type DatasetBlocksMember struct { + gorm.Model + TiCode string `gorm:"type:varchar(50);not null;default:'';comment:板块代码;index" json:"ti_code"` + TsCode string `gorm:"type:varchar(50);not null;default:'';comment:股票代码;index" json:"ts_code"` + Weight float64 `gorm:"type:float;not null;default:0;comment:权重" json:"weight"` +} + +func (DatasetBlocksMember) TableName() string { + return "dataset_blocks_member" +} + +// Key 成分在板块内的键:ti_code + ts_code。 +func (b *DatasetBlocksMember) Key() string { + if b.TiCode == "" && b.TsCode == "" { + return "" + } + return b.TiCode + "/" + b.TsCode +} + +// HasWeight 是否带权重量化成分。 +func (b *DatasetBlocksMember) HasWeight() bool { + return b.Weight > 0 +} diff --git a/schema/stock_daily.go b/schema/dataset_daily.go similarity index 81% rename from schema/stock_daily.go rename to schema/dataset_daily.go index eaa240a..1a4624e 100644 --- a/schema/stock_daily.go +++ b/schema/dataset_daily.go @@ -2,8 +2,8 @@ package schema import "strconv" -// StockDaily 股票日线数据(两仓库结构一致)。 -type StockDaily struct { +// DatasetStockDaily 股票日线数据(两仓库结构一致)。 +type DatasetStockDaily struct { ID uint `gorm:"primarykey;autoIncrement" json:"id"` TsCode string `gorm:"type:varchar(20);not null;index:idx_ts_code;uniqueIndex:un_code_date;comment:股票代码" json:"ts_code"` TradeDate int `gorm:"index:idx_trade_date;uniqueIndex:un_code_date;comment:交易日期" json:"trade_date"` @@ -19,12 +19,12 @@ type StockDaily struct { Amount float64 `gorm:"type:decimal(20,2);comment:成交额(千元)" json:"amount"` } -func (StockDaily) TableName() string { - return "stock_daily" +func (DatasetStockDaily) TableName() string { + return "dataset_stock_daily" } // Key 业务主键:ts_code + 交易日。 -func (d *StockDaily) Key() string { +func (d *DatasetStockDaily) Key() string { if d.TsCode == "" && d.TradeDate == 0 { return "" } @@ -32,17 +32,17 @@ func (d *StockDaily) Key() string { } // IsRising 是否收涨(昨收有效且收盘高于昨收)。 -func (d *StockDaily) IsRising() bool { +func (d *DatasetStockDaily) IsRising() bool { return d.PreClose > 0 && d.Close > d.PreClose } // IsFalling 是否收跌。 -func (d *StockDaily) IsFalling() bool { +func (d *DatasetStockDaily) IsFalling() bool { return d.PreClose > 0 && d.Close < d.PreClose } // PctChangeFromPre 由昨收计算的涨跌幅(%);昨收无效时返回 0。 -func (d *StockDaily) PctChangeFromPre() float64 { +func (d *DatasetStockDaily) PctChangeFromPre() float64 { if d.PreClose <= 0 { return 0 } @@ -50,7 +50,7 @@ func (d *StockDaily) PctChangeFromPre() float64 { } // AmplitudePct 振幅(相对昨收,%):(最高-最低)/昨收*100。 -func (d *StockDaily) AmplitudePct() float64 { +func (d *DatasetStockDaily) AmplitudePct() float64 { if d.PreClose <= 0 { return 0 } diff --git a/schema/stock_fina_indicator.go b/schema/dataset_fina_indicator.go similarity index 98% rename from schema/stock_fina_indicator.go rename to schema/dataset_fina_indicator.go index cfb57ea..1f39888 100644 --- a/schema/stock_fina_indicator.go +++ b/schema/dataset_fina_indicator.go @@ -6,8 +6,8 @@ import ( "gorm.io/gorm" ) -// StockFinaIndicator 财务指标模型 -type StockFinaIndicator struct { +// DatasetStockFinaIndicator 财务指标模型 +type DatasetStockFinaIndicator struct { gorm.Model TsCode string `gorm:"type:varchar(20);not null;index:fi_ts_code;uniqueIndex:un_fi_code_date;comment:TS代码"` Period int `gorm:"index:idx_period;uniqueIndex:un_fi_code_date;comment:报告期数"` @@ -205,12 +205,12 @@ type StockFinaIndicator struct { } // TableName 设置表名 -func (StockFinaIndicator) TableName() string { - return "fina_indicator" +func (DatasetStockFinaIndicator) TableName() string { + return "dataset_fina_indicator" } // Key 与表 uniqueIndex un_fi_code_date 一致:ts_code + period。 -func (f *StockFinaIndicator) Key() string { +func (f *DatasetStockFinaIndicator) Key() string { if f.TsCode == "" && f.Period == 0 { return "" } @@ -218,7 +218,7 @@ func (f *StockFinaIndicator) Key() string { } // RowLabel 便于日志/调试:ts_code + 报告期 end_date + 公告 ann_date。 -func (f *StockFinaIndicator) RowLabel() string { +func (f *DatasetStockFinaIndicator) RowLabel() string { if f.EndDate != "" || f.AnnDate != "" { return f.TsCode + " end=" + f.EndDate + " ann=" + f.AnnDate } diff --git a/schema/stock_indicator.go b/schema/dataset_indicator.go similarity index 83% rename from schema/stock_indicator.go rename to schema/dataset_indicator.go index 0675871..3cbf12d 100644 --- a/schema/stock_indicator.go +++ b/schema/dataset_indicator.go @@ -2,8 +2,8 @@ package schema import "strconv" -// StockIndicator 每日基本面指标(采用 dataset/stock 的 decimal 定义,与采集端迁移一致)。 -type StockIndicator struct { +// DatasetStockIndicator 每日基本面指标(采用 dataset/stock 的 decimal 定义,与采集端迁移一致)。 +type DatasetStockIndicator struct { ID uint `gorm:"primarykey;autoIncrement"` TsCode string `gorm:"type:varchar(20);not null;index:si_ts_code;uniqueIndex:un_si_code_date;comment:股票代码" json:"ts_code"` TradeDate int `gorm:"index:si_trade_date;uniqueIndex:un_si_code_date;comment:交易日期" json:"trade_date"` @@ -26,12 +26,12 @@ type StockIndicator struct { Roe float64 `gorm:"type:decimal(20,4);comment:ROE(%) 净利润/股本"` } -func (StockIndicator) TableName() string { - return "stock_indicator" +func (DatasetStockIndicator) TableName() string { + return "dataset_stock_indicator" } // Key 业务主键:ts_code + 交易日。 -func (s *StockIndicator) Key() string { +func (s *DatasetStockIndicator) Key() string { if s.TsCode == "" && s.TradeDate == 0 { return "" } @@ -39,11 +39,11 @@ func (s *StockIndicator) Key() string { } // HasTotalMV 总市值是否已填充(大于 0)。 -func (s *StockIndicator) HasTotalMV() bool { +func (s *DatasetStockIndicator) HasTotalMV() bool { return s.TotalMv > 0 } // HasCircMV 流通市值是否已填充。 -func (s *StockIndicator) HasCircMV() bool { +func (s *DatasetStockIndicator) HasCircMV() bool { return s.CircMv > 0 } diff --git a/schema/dataset_indicator_pro.go b/schema/dataset_indicator_pro.go new file mode 100644 index 0000000..6185e20 --- /dev/null +++ b/schema/dataset_indicator_pro.go @@ -0,0 +1,66 @@ +package schema + +import "strconv" + +// DatasetIndicatorPro 对应 Tushare stk_factor_pro 当前请求字段集(见 tushare/indicator.go 的 fields 列表)。 +type DatasetIndicatorPro struct { + ID uint `gorm:"primarykey;autoIncrement"` + TsCode string `gorm:"type:varchar(20);not null;index:dip_ts_code;uniqueIndex:un_dip_code_date;comment:股票代码" json:"ts_code"` + TradeDate int `gorm:"index:dip_trade_date;uniqueIndex:un_dip_code_date;comment:交易日期" json:"trade_date"` + Open float64 `gorm:"type:decimal(20,4);comment:开盘价"` + High float64 `gorm:"type:decimal(20,4);comment:最高价"` + Low float64 `gorm:"type:decimal(20,4);comment:最低价"` + Close float64 `gorm:"type:decimal(20,4);comment:收盘价"` + PreClose float64 `gorm:"type:decimal(20,4);comment:昨收价"` + Change float64 `gorm:"type:decimal(20,4);comment:涨跌额"` + PctChg float64 `gorm:"type:decimal(20,6);comment:涨跌幅%"` + Vol float64 `gorm:"type:decimal(20,2);comment:成交量(手)"` + Amount float64 `gorm:"type:decimal(20,2);comment:成交额(千元)"` + TurnoverRate float64 `gorm:"type:decimal(20,4);comment:换手率(%)"` + TurnoverRateF float64 `gorm:"type:decimal(20,4);comment:换手率(自由流通股)"` + VolumeRatio float64 `gorm:"type:decimal(20,4);comment:量比"` + Pe float64 `gorm:"type:decimal(20,4);comment:市盈率"` + PeTtm float64 `gorm:"type:decimal(20,4);comment:市盈率TTM"` + Pb float64 `gorm:"type:decimal(20,4);comment:市净率"` + Ps float64 `gorm:"type:decimal(20,4);comment:市销率"` + PsTtm float64 `gorm:"type:decimal(20,4);comment:市销率TTM"` + DvRatio float64 `gorm:"type:decimal(20,4);comment:股息率(%)"` + DvTtm float64 `gorm:"type:decimal(20,4);comment:股息率TTM(%)"` + TotalShare float64 `gorm:"type:decimal(20,4);comment:总股本(万股)"` + FloatShare float64 `gorm:"type:decimal(20,4);comment:流通股本(万股)"` + FreeShare float64 `gorm:"type:decimal(20,4);comment:自由流通股本(万股)"` + TotalMv float64 `gorm:"type:decimal(20,4);comment:总市值(万元)"` + CircMv float64 `gorm:"type:decimal(20,4);comment:流通市值(万元)"` + AdjFactor float64 `gorm:"type:decimal(20,6);comment:复权因子"` + MaBfq5 float64 `gorm:"type:decimal(20,6);comment:MA5不复权"` + MaBfq10 float64 `gorm:"type:decimal(20,6);comment:MA10不复权"` + MaBfq20 float64 `gorm:"type:decimal(20,6);comment:MA20不复权"` + MaBfq60 float64 `gorm:"type:decimal(20,6);comment:MA60不复权"` + EmaBfq5 float64 `gorm:"type:decimal(20,6);comment:EMA5不复权"` + EmaBfq10 float64 `gorm:"type:decimal(20,6);comment:EMA10不复权"` + EmaBfq20 float64 `gorm:"type:decimal(20,6);comment:EMA20不复权"` + MacdBfq float64 `gorm:"type:decimal(20,6);comment:MACD不复权"` + MacdDifBfq float64 `gorm:"type:decimal(20,6);comment:MACD DIF不复权"` + MacdDeaBfq float64 `gorm:"type:decimal(20,6);comment:MACD DEA不复权"` + RsiBfq6 float64 `gorm:"type:decimal(20,6);comment:RSI6不复权"` + RsiBfq12 float64 `gorm:"type:decimal(20,6);comment:RSI12不复权"` + RsiBfq24 float64 `gorm:"type:decimal(20,6);comment:RSI24不复权"` + KdjKBfq float64 `gorm:"type:decimal(20,6);comment:KDJ-K不复权"` + KdjDBfq float64 `gorm:"type:decimal(20,6);comment:KDJ-D不复权"` + KdjBfq float64 `gorm:"type:decimal(20,6);comment:KDJ-J不复权"` + BollUpperBfq float64 `gorm:"type:decimal(20,6);comment:BOLL上轨不复权"` + BollMidBfq float64 `gorm:"type:decimal(20,6);comment:BOLL中轨不复权"` + BollLowerBfq float64 `gorm:"type:decimal(20,6);comment:BOLL下轨不复权"` +} + +func (DatasetIndicatorPro) TableName() string { + return "dataset_indicator_pro" +} + +// Key 业务主键:ts_code + 交易日。 +func (s *DatasetIndicatorPro) Key() string { + if s.TsCode == "" && s.TradeDate == 0 { + return "" + } + return s.TsCode + "#" + strconv.Itoa(s.TradeDate) +} diff --git a/schema/dataset_money_total.go b/schema/dataset_money_total.go new file mode 100644 index 0000000..f2e553d --- /dev/null +++ b/schema/dataset_money_total.go @@ -0,0 +1,31 @@ +package schema + +// DatasetMoneyTotal 资金流汇总(采用 dataset/stock 的索引定义)。 +type DatasetMoneyTotal struct { + ID uint `gorm:"primarykey"` + TsCode string `gorm:"type:varchar(20);not null;uniqueIndex:uq_money_total_ts_code"` + Last1DayMfAmount float64 + Last3DayMfAmount float64 + Last1DayTotalAmount float64 + Last3DayTotalAmount float64 + IsGreaterPervious bool +} + +func (DatasetMoneyTotal) TableName() string { + return "dataset_money_total" +} + +// Key 与唯一索引 uq_money_total_ts_code 一致。 +func (m *DatasetMoneyTotal) Key() string { + return m.TsCode +} + +// NetFlow1Day 最近 1 日主力净流入(万元),与字段语义一致。 +func (m *DatasetMoneyTotal) NetFlow1Day() float64 { + return m.Last1DayMfAmount +} + +// NetFlow3Day 最近 3 日主力净流入(万元)。 +func (m *DatasetMoneyTotal) NetFlow3Day() float64 { + return m.Last3DayMfAmount +} diff --git a/schema/pledge_stat.go b/schema/dataset_pledge_stat.go similarity index 62% rename from schema/pledge_stat.go rename to schema/dataset_pledge_stat.go index b6f63a5..35e6ea2 100644 --- a/schema/pledge_stat.go +++ b/schema/dataset_pledge_stat.go @@ -1,7 +1,7 @@ package schema -// PledgeStat 股权质押统计(表 pledge_stat;dataset/stock 中原为 PledgeStatModel,业务层 Save 请留在各应用内)。 -type PledgeStat struct { +// DatasetPledgeStat 股权质押统计(表 dataset_pledge_stat;dataset/stock 中原为 PledgeStatModel,业务层 Save 请留在各应用内)。 +type DatasetPledgeStat struct { ID uint `gorm:"primarykey" json:"id"` TsCode string `gorm:"type:varchar(50);not null;default:'';comment:股票代码;uniqueIndex:uq_pledge_stat_ts_code" json:"ts_code"` EndDate int `gorm:"type:int;not null;default:0;comment:截止日期" json:"end_date"` @@ -12,19 +12,19 @@ type PledgeStat struct { PledgeRatio float64 `json:"pledge_ratio"` } -func (PledgeStat) TableName() string { - return "pledge_stat" +func (DatasetPledgeStat) TableName() string { + return "dataset_pledge_stat" } // Key 与唯一约束 uq_pledge_stat_ts_code 一致:ts_code。 -func (p *PledgeStat) Key() string { +func (p *DatasetPledgeStat) Key() string { return p.TsCode } // HasPledgeFacts 是否具备任一质押统计字段(比例、次数或股本)。 -func (p *PledgeStat) HasPledgeFacts() bool { +func (p *DatasetPledgeStat) HasPledgeFacts() bool { return p.PledgeRatio > 0 || p.PledgeCount > 0 || p.TotalShare > 0 } -// PledgeStatModel 兼容 dataset/stock 中的类型名。 -type PledgeStatModel = PledgeStat +// DatasetPledgeStatModel 兼容 dataset/stock 中的类型名。 +type DatasetPledgeStatModel = DatasetPledgeStat diff --git a/schema/money_total.go b/schema/money_total.go deleted file mode 100644 index 8f2578c..0000000 --- a/schema/money_total.go +++ /dev/null @@ -1,31 +0,0 @@ -package schema - -// MoneyTotal 资金流汇总(采用 dataset/stock 的索引定义)。 -type MoneyTotal struct { - ID uint `gorm:"primarykey"` - Code string `gorm:"type:varchar(20);not null;uniqueIndex:uq_money_total_code"` - Last1DayMfAmount float64 - Last3DayMfAmount float64 - Last1DayTotalAmount float64 - Last3DayTotalAmount float64 - IsGreaterPervious bool -} - -func (MoneyTotal) TableName() string { - return "money_total" -} - -// Key 与唯一索引 uq_money_total_code 一致。 -func (m *MoneyTotal) Key() string { - return m.Code -} - -// NetFlow1Day 最近 1 日主力净流入(万元),与字段语义一致。 -func (m *MoneyTotal) NetFlow1Day() float64 { - return m.Last1DayMfAmount -} - -// NetFlow3Day 最近 3 日主力净流入(万元)。 -func (m *MoneyTotal) NetFlow3Day() float64 { - return m.Last3DayMfAmount -} diff --git a/schema/register.go b/schema/register.go index 99c32cd..0cdd050 100644 --- a/schema/register.go +++ b/schema/register.go @@ -5,14 +5,15 @@ import "git.apinb.com/bsm-sdk/core/database" // RegisterAutoMigrate 将本包内与 stock/gostock 共用的表注册到 bsm-sdk 的迁移列表(可选;也可在各应用 init 中自行 AppendMigrate)。 func RegisterAutoMigrate() { for _, t := range []any{ - &StockBasic{}, - &StockDaily{}, - &BlocksIndex{}, - &BlocksMember{}, - &MoneyTotal{}, - &PledgeStat{}, - &StockIndicator{}, - &StockFinaIndicator{}, + &DatasetStockBasic{}, + &DatasetStockDaily{}, + &DatasetBlocksIndex{}, + &DatasetBlocksMember{}, + &DatasetMoneyTotal{}, + &DatasetPledgeStat{}, + &DatasetStockIndicator{}, + &DatasetIndicatorPro{}, + &DatasetStockFinaIndicator{}, } { database.AppendMigrate(t) } diff --git a/tushare/indicator.go b/tushare/indicator.go index 0e8454b..665f879 100644 --- a/tushare/indicator.go +++ b/tushare/indicator.go @@ -1,12 +1,26 @@ package tushare -func (cli *TushareClient) StkFactorPro(ts_code string, tradeDate string) (*TushareRespData, error) { +/* +StkFactorPro 获取股票技术面因子(专业版) + + ts_code: 股票代码,支持多个,逗号分隔 + trade_date: 交易日期,格式:YYYYMMDD + start_date: 开始日期,格式:YYYYMMDD + end_date: 结束日期,格式:YYYYMMDD +*/ +func (cli *TushareClient) StkFactorPro(ts_code, trade_date, start_date, end_date string) (*TushareRespData, error) { params := map[string]any{} if ts_code != "" { params["ts_code"] = ts_code } - if tradeDate != "" { - params["trade_date"] = tradeDate + if trade_date != "" { + params["trade_date"] = trade_date + } + if start_date != "" { + params["start_date"] = start_date + } + if end_date != "" { + params["end_date"] = end_date } req := TushareReq{ @@ -14,10 +28,52 @@ func (cli *TushareClient) StkFactorPro(ts_code string, tradeDate string) (*Tusha Params: params, } fields := []map[string]string{ - {"trade_date": "trade_date"}, - {"rsi_bfq_24": "rsi_bfq_24"}, - {"rsi_hfq_24": "rsi_hfq_24"}, - {"rsi_qfq_24": "rsi_qfq_24"}, + {"ts_code": "股票代码"}, + {"trade_date": "交易日期"}, + {"open": "开盘价"}, + {"high": "最高价"}, + {"low": "最低价"}, + {"close": "收盘价"}, + {"pre_close": "昨收价"}, + {"change": "涨跌额"}, + {"pct_chg": "涨跌幅%"}, + {"vol": "成交量(手)"}, + {"amount": "成交额(千元)"}, + {"turnover_rate": "换手率(%)"}, + {"turnover_rate_f": "换手率(自由流通股)"}, + {"volume_ratio": "量比"}, + {"pe": "市盈率"}, + {"pe_ttm": "市盈率TTM"}, + {"pb": "市净率"}, + {"ps": "市销率"}, + {"ps_ttm": "市销率TTM"}, + {"dv_ratio": "股息率(%)"}, + {"dv_ttm": "股息率TTM(%)"}, + {"total_share": "总股本(万股)"}, + {"float_share": "流通股本(万股)"}, + {"free_share": "自由流通股本(万股)"}, + {"total_mv": "总市值(万元)"}, + {"circ_mv": "流通市值(万元)"}, + {"adj_factor": "复权因子"}, + {"ma_bfq_5": "MA5不复权"}, + {"ma_bfq_10": "MA10不复权"}, + {"ma_bfq_20": "MA20不复权"}, + {"ma_bfq_60": "MA60不复权"}, + {"ema_bfq_5": "EMA5不复权"}, + {"ema_bfq_10": "EMA10不复权"}, + {"ema_bfq_20": "EMA20不复权"}, + {"macd_bfq": "MACD不复权"}, + {"macd_dif_bfq": "MACD DIF不复权"}, + {"macd_dea_bfq": "MACD DEA不复权"}, + {"rsi_bfq_6": "RSI6不复权"}, + {"rsi_bfq_12": "RSI12不复权"}, + {"rsi_bfq_24": "RSI24不复权"}, + {"kdj_k_bfq": "KDJ-K不复权"}, + {"kdj_d_bfq": "KDJ-D不复权"}, + {"kdj_bfq": "KDJ-J不复权"}, + {"boll_upper_bfq": "BOLL上轨不复权"}, + {"boll_mid_bfq": "BOLL中轨不复权"}, + {"boll_lower_bfq": "BOLL下轨不复权"}, } return cli.Do(req, fields) diff --git a/tushare/new.go b/tushare/new.go index 0d9bd9d..822f814 100644 --- a/tushare/new.go +++ b/tushare/new.go @@ -17,6 +17,7 @@ import ( type TushareClient struct { Token string `json:"token"` // API 访问令牌 BaseUrl string `json:"base_url"` // API 基础 URL + client *http.Client } // TushareReq Tushare API 请求结构 @@ -53,9 +54,40 @@ func NewClient(token string) *TushareClient { return &TushareClient{ Token: token, BaseUrl: "http://api.tushare.pro", + client: &http.Client{Timeout: 30 * time.Second}, } } +func (c *TushareClient) httpDo() *http.Client { + if c.client != nil { + return c.client + } + return &http.Client{Timeout: 30 * time.Second} +} + +// postTushare 发送 JSON 请求并解析为 TushareResp(不含业务码以外的处理)。 +func (c *TushareClient) postTushare(reqBytes []byte) (*TushareResp, error) { + resp, err := c.httpDo().Post(c.BaseUrl, "application/json", bytes.NewReader(reqBytes)) + if err != nil { + return nil, fmt.Errorf("#100 发送请求失败:%w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("#101 读取响应失败:%w", err) + } + + var tushareResp TushareResp + if err := json.Unmarshal(body, &tushareResp); err != nil { + return nil, fmt.Errorf("#101 响应解析失败:%v - %s", err, string(body)) + } + if tushareResp.Code != 0 { + return nil, fmt.Errorf("#102 API 返回错误:%s", tushareResp.Msg) + } + return &tushareResp, nil +} + // Do 执行 Tushare API 请求 // req: 请求参数,包含 API 名称、参数等 // fieldsVals: 字段配置列表,每个元素是一个 map,key 为字段名,value 为字段中文描述 @@ -69,8 +101,12 @@ func (c *TushareClient) Do(req TushareReq, fieldsVals []map[string]string) (*Tus } // 提取字段名和对应的中文表头 - var fields []string - var headers []string + n := 0 + for _, setting := range fieldsVals { + n += len(setting) + } + fields := make([]string, 0, n) + headers := make([]string, 0, n) for _, setting := range fieldsVals { for key, value := range setting { fields = append(fields, key) @@ -79,40 +115,15 @@ func (c *TushareClient) Do(req TushareReq, fieldsVals []map[string]string) (*Tus } payload.Fields = fields - // 序列化请求体为 JSON reqBytes, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("#100 请求序列化失败:%w", err) } - // 创建 HTTP 请求 - client := &http.Client{ - Timeout: 30 * time.Second, // 设置 30 秒超时 - } - resp, err := client.Post(c.BaseUrl, "application/json", bytes.NewReader(reqBytes)) + tushareResp, err := c.postTushare(reqBytes) if err != nil { - return nil, fmt.Errorf("#100 发送请求失败:%w", err) + return nil, err } - defer resp.Body.Close() - - // 读取响应体 - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("#101 读取响应失败:%w", err) - } - - // 解析响应 JSON - var tushareResp TushareResp - if err := json.Unmarshal(body, &tushareResp); err != nil { - return nil, fmt.Errorf("#101 响应解析失败:%v - %s", err, string(body)) - } - - // 检查响应码 - if tushareResp.Code != 0 { - return nil, fmt.Errorf("#102 API 返回错误:%s", tushareResp.Msg) - } - - // 设置表头信息 if tushareResp.Data != nil { tushareResp.Data.Headers = headers } @@ -125,12 +136,22 @@ func (c *TushareClient) Do(req TushareReq, fieldsVals []map[string]string) (*Tus // Params: 请求参数 // 返回:响应数据和错误信息 func (c *TushareClient) Exec(ApiName string, fields string, Params map[string]any) (*TushareRespData, error) { - fieldsList := strings.Split(fields, ",") + fields = strings.TrimSpace(fields) + if fields == "" { + return nil, fmt.Errorf("#100 字段列表不能为空") + } + raw := strings.Split(fields, ",") + fieldsList := make([]string, 0, len(raw)) + for _, f := range raw { + f = strings.TrimSpace(f) + if f != "" { + fieldsList = append(fieldsList, f) + } + } if len(fieldsList) == 0 { return nil, fmt.Errorf("#100 字段列表不能为空") } - // 构建请求体 payload := TushareReq{ APIName: ApiName, Token: c.Token, @@ -138,51 +159,25 @@ func (c *TushareClient) Exec(ApiName string, fields string, Params map[string]an Fields: fieldsList, } - // 序列化请求体为 JSON reqBytes, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("#100 请求序列化失败:%w", err) } - // 创建 HTTP 请求 - client := &http.Client{ - Timeout: 30 * time.Second, // 设置 30 秒超时 - } - resp, err := client.Post(c.BaseUrl, "application/json", bytes.NewReader(reqBytes)) + tushareResp, err := c.postTushare(reqBytes) if err != nil { - return nil, fmt.Errorf("#100 发送请求失败:%w", err) + return nil, err } - defer resp.Body.Close() - - // 读取响应体 - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("#101 读取响应失败:%w", err) - } - - // 解析响应 JSON - var tushareResp TushareResp - if err := json.Unmarshal(body, &tushareResp); err != nil { - return nil, fmt.Errorf("#101 响应解析失败:%v - %s", err, string(body)) - } - - // 检查响应码 - if tushareResp.Code != 0 { - return nil, fmt.Errorf("#102 API 返回错误:%s", tushareResp.Msg) - } - - // 设置表头信息 return tushareResp.Data, nil } // Map 将响应数据转换为 map 切片 // 每个 map 代表一行数据,key 为字段名,value 为对应的值 func (c *TushareRespData) Map() []map[string]any { - result := make([]map[string]any, 0) if c == nil || len(c.Items) == 0 || len(c.Fields) == 0 { - return result + return make([]map[string]any, 0) } - + result := make([]map[string]any, 0, len(c.Items)) for _, item := range c.Items { rowMap := make(map[string]any, len(c.Fields)) for idx, fn := range c.Fields { @@ -209,11 +204,17 @@ func (c *TushareRespData) Output(title string) table.Writer { tw := table.NewWriter() tw.SetStyle(table.StyleLight) tw.SetTitle(title) + if c == nil { + return tw + } - // 构建表头行 - headerRow := make(table.Row, 0, len(c.Headers)) - for idx, header := range c.Headers { - headerRow = append(headerRow, header+"("+c.Fields[idx]+")") + n := len(c.Fields) + if nh := len(c.Headers); nh < n { + n = nh + } + headerRow := make(table.Row, 0, n) + for idx := 0; idx < n; idx++ { + headerRow = append(headerRow, c.Headers[idx]+"("+c.Fields[idx]+")") } tw.AppendHeader(headerRow) diff --git a/tushare/stock.go b/tushare/stock.go index cdb15ad..00aafc0 100644 --- a/tushare/stock.go +++ b/tushare/stock.go @@ -2,6 +2,8 @@ package tushare import ( "time" + + "git.apinb.com/quant/qsdk/conv" ) /* @@ -49,7 +51,10 @@ func (cli *TushareClient) ReturnLastTradeDay() string { return "" } cal := result.Map() - return cal[0]["cal_date"].(string) + if len(cal) == 0 { + return "" + } + return conv.AnyToString(cal[0]["cal_date"]) } /*