From ba41a6af7634dc17432b4f675f33d6864fcf7ab7 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Sat, 2 May 2026 10:01:33 +0800 Subject: [PATCH] add money flow. --- schema/dataset_money.go | 33 ++++ schema/dataset_money_total.go | 31 ---- schema/dataset_summary.go | 325 ++++++++++++++++++++++++++++++++++ schema/register.go | 3 +- tushare/finance.go | 29 +-- 5 files changed, 379 insertions(+), 42 deletions(-) create mode 100644 schema/dataset_money.go delete mode 100644 schema/dataset_money_total.go create mode 100644 schema/dataset_summary.go diff --git a/schema/dataset_money.go b/schema/dataset_money.go new file mode 100644 index 0000000..25fb977 --- /dev/null +++ b/schema/dataset_money.go @@ -0,0 +1,33 @@ +package schema + +// DatasetMoney个股资金流向(与 Tushare moneyflow 接口字段一致,每行 ts_code+trade_date)。 +// 金额单位为万元,量单位为手;分类阈值见 Tushare 文档(小单 5 万以下、中单 5~20 万等)。 +type DatasetMoney struct { + ID uint `gorm:"primarykey;autoIncrement"` + TsCode string `gorm:"type:varchar(20);not null;uniqueIndex:un_money_ts_date;index;comment:TS代码" json:"ts_code"` + UpdateYmd int `gorm:"index;comment:更新日期" json:"update_ymd"` + + BuySmVol int64 `gorm:"comment:小单买入量(手)" json:"buy_sm_vol"` + BuySmAmount float64 `gorm:"type:decimal(20,4);comment:小单买入金额(万元)" json:"buy_sm_amount"` + SellSmVol int64 `gorm:"comment:小单卖出量(手)" json:"sell_sm_vol"` + SellSmAmount float64 `gorm:"type:decimal(20,4);comment:小单卖出金额(万元)" json:"sell_sm_amount"` + BuyMdVol int64 `gorm:"comment:中单买入量(手)" json:"buy_md_vol"` + BuyMdAmount float64 `gorm:"type:decimal(20,4);comment:中单买入金额(万元)" json:"buy_md_amount"` + SellMdVol int64 `gorm:"comment:中单卖出量(手)" json:"sell_md_vol"` + SellMdAmount float64 `gorm:"type:decimal(20,4);comment:中单卖出金额(万元)" json:"sell_md_amount"` + BuyLgVol int64 `gorm:"comment:大单买入量(手)" json:"buy_lg_vol"` + BuyLgAmount float64 `gorm:"type:decimal(20,4);comment:大单买入金额(万元)" json:"buy_lg_amount"` + SellLgVol int64 `gorm:"comment:大单卖出量(手)" json:"sell_lg_vol"` + SellLgAmount float64 `gorm:"type:decimal(20,4);comment:大单卖出金额(万元)" json:"sell_lg_amount"` + BuyElgVol int64 `gorm:"comment:特大单买入量(手)" json:"buy_elg_vol"` + BuyElgAmount float64 `gorm:"type:decimal(20,4);comment:特大单买入金额(万元)" json:"buy_elg_amount"` + SellElgVol int64 `gorm:"comment:特大单卖出量(手)" json:"sell_elg_vol"` + SellElgAmount float64 `gorm:"type:decimal(20,4);comment:特大单卖出金额(万元)" json:"sell_elg_amount"` + NetMfVol int64 `gorm:"comment:净流入量(手)" json:"net_mf_vol"` + NetMfAmount float64 `gorm:"type:decimal(20,4);comment:净流入额(万元)" json:"net_mf_amount"` + MainForceNetWan float64 `gorm:"type:decimal(20,4);comment:主力净流入(万元)" json:"main_force_net_wan"` +} + +func (DatasetMoney) TableName() string { + return "dataset_money" +} diff --git a/schema/dataset_money_total.go b/schema/dataset_money_total.go deleted file mode 100644 index f2e553d..0000000 --- a/schema/dataset_money_total.go +++ /dev/null @@ -1,31 +0,0 @@ -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/dataset_summary.go b/schema/dataset_summary.go new file mode 100644 index 0000000..447d49b --- /dev/null +++ b/schema/dataset_summary.go @@ -0,0 +1,325 @@ +package schema + +import ( + "errors" + "strconv" + "strings" + "time" + + "gorm.io/gorm" +) + +// DatasetSummary 个股重要指标总览(每股一行,由 Basic / Daily / Indicator / IndicatorPro / +// FinaIndicator / MoneyTotal / PledgeStat / BlocksMember 等表聚合写入)。 +type DatasetSummary struct { + ID uint `gorm:"primarykey"` + TsCode string `gorm:"type:varchar(20);not null;uniqueIndex:uq_dataset_summary_ts_code;index;comment:TS代码" json:"ts_code"` + UpdateYmd int `gorm:"index;comment:更新日期" json:"update_ymd"` + + // --- DatasetBasic:身份与分类 --- + Name string `gorm:"type:varchar(50);not null;default:'';comment:股票名称" json:"name"` + Industry string `gorm:"type:varchar(50);not null;default:'';comment:所属行业" json:"industry"` + Area string `gorm:"type:varchar(50);not null;default:'';comment:地域" json:"area"` + Market string `gorm:"type:varchar(50);not null;default:'';comment:市场类型" json:"market"` + ListDate string `gorm:"type:varchar(50);not null;default:'';comment:上市日期" json:"list_date"` + IsHS string `gorm:"type:varchar(2);default:'N';comment:沪深港通 N/H/S" json:"is_hs"` + IsSt string `gorm:"type:varchar(2);default:'N';comment:是否是ST" json:"is_st"` + + // --- DatasetDaily / DatasetIndicator / DatasetIndicatorPro:最近交易日行情与估值 --- + Close float64 `gorm:"type:decimal(20,4);comment:收盘价" json:"close"` + PctChg float64 `gorm:"type:decimal(20,6);comment:涨跌幅(%)" json:"pct_chg"` + Vol float64 `gorm:"type:decimal(20,2);comment:成交量(手)" json:"vol"` + Amount float64 `gorm:"type:decimal(20,2);comment:成交额(千元)" json:"amount"` + TurnoverRate float64 `gorm:"type:decimal(20,4);comment:换手率(%)" json:"turnover_rate"` + TurnoverRateF float64 `gorm:"type:decimal(20,4);comment:换手率(自由流通股%)" json:"turnover_rate_f"` + VolumeRatio float64 `gorm:"type:decimal(20,4);comment:量比" json:"volume_ratio"` + Pe float64 `gorm:"type:decimal(20,4);comment:市盈率" json:"pe"` + PeTtm float64 `gorm:"type:decimal(20,4);comment:市盈率TTM" json:"pe_ttm"` + Pb float64 `gorm:"type:decimal(20,4);comment:市净率" json:"pb"` + PsTtm float64 `gorm:"type:decimal(20,4);comment:市销率TTM" json:"ps_ttm"` + DvTtm float64 `gorm:"type:decimal(20,4);comment:股息率TTM(%)" json:"dv_ttm"` + TotalShare float64 `gorm:"type:decimal(20,4);comment:总股本(万股)" json:"total_share"` + FloatShare float64 `gorm:"type:decimal(20,4);comment:流通股本(万股)" json:"float_share"` + FreeShare float64 `gorm:"type:decimal(20,4);comment:自由流通股本(万)" json:"free_share"` + TotalMv float64 `gorm:"type:decimal(20,4);comment:总市值(万元)" json:"total_mv"` + CircMv float64 `gorm:"type:decimal(20,4);comment:流通市值(万元)" json:"circ_mv"` + AdjFactor float64 `gorm:"type:decimal(20,6);comment:复权因子(来自因子表)" json:"adj_factor"` + MaBfq5 float64 `gorm:"type:decimal(20,6);comment:MA5不复权" json:"ma_bfq5"` + MaBfq20 float64 `gorm:"type:decimal(20,6);comment:MA20不复权" json:"ma_bfq20"` + MacdDifBfq float64 `gorm:"type:decimal(20,6);comment:MACD DIF不复权" json:"macd_dif_bfq"` + RsiBfq12 float64 `gorm:"type:decimal(20,6);comment:RSI12不复权" json:"rsi_bfq12"` + + // --- DatasetFinaIndicator:最近一期财务核心 --- + FinaPeriod int `gorm:"index;comment:财务报告期数(与 fina 表 period 一致)" json:"fina_period"` + FinaAnnDate string `gorm:"type:varchar(32);default:'';comment:财报公告日" json:"fina_ann_date"` + FinaEndDate string `gorm:"type:varchar(32);default:'';comment:财报报告期" json:"fina_end_date"` + Eps float64 `gorm:"type:decimal(20,4);comment:基本每股收益" json:"eps"` + Bps float64 `gorm:"type:decimal(20,4);comment:每股净资产" json:"bps"` + Ocfps float64 `gorm:"type:decimal(20,4);comment:每股经营活动现金流净额" json:"ocfps"` + Roe float64 `gorm:"type:decimal(20,4);comment:净资产收益率" json:"roe"` + RoeWaa float64 `gorm:"type:decimal(20,4);comment:加权平均净资产收益率" json:"roe_waa"` + Roa float64 `gorm:"type:decimal(20,4);comment:总资产报酬率" json:"roa"` + DebtToAssets float64 `gorm:"type:decimal(20,4);comment:资产负债率" json:"debt_to_assets"` + GrossprofitMargin float64 `gorm:"type:decimal(20,4);comment:销售毛利率" json:"grossprofit_margin"` + NetprofitYoy float64 `gorm:"type:decimal(20,4);comment:归母净利润同比(%)" json:"netprofit_yoy"` + OrYoy float64 `gorm:"type:decimal(20,4);comment:营业收入同比(%)" json:"or_yoy"` + TrYoy float64 `gorm:"type:decimal(20,4);comment:营业总收入同比(%)" json:"tr_yoy"` + + // --- DatasetMoneyTotal(moneyflow):近 1/3 交易日主力净流入(万元) 与成交额,由源表按 update_ymd 倒序聚合 --- + Last1DayMfAmount float64 `gorm:"type:decimal(20,4);comment:最近1个交易日主力净流入(万元)" json:"last_1day_mf_amount"` + Last3DayMfAmount float64 `gorm:"type:decimal(20,4);comment:最近3个交易日主力净流入合计(万元)" json:"last_3day_mf_amount"` + Last1DayTotalAmount float64 `gorm:"type:decimal(20,4);comment:最近1个交易日成交额(万元,来自日线千元换算)" json:"last_1day_total_amount"` + Last3DayTotalAmount float64 `gorm:"type:decimal(20,4);comment:最近3个交易日成交额合计(万元)" json:"last_3day_total_amount"` + IsGreaterPervious bool `gorm:"comment:主力净流入是否大于上一交易日" json:"is_greater_pervious"` + + // --- DatasetPledgeStat:股权质押 --- + PledgeEndDate int `gorm:"comment:质押统计截止日期" json:"pledge_end_date"` + PledgeRatio float64 `gorm:"type:decimal(20,4);comment:质押比例" json:"pledge_ratio"` + PledgeCount float64 `gorm:"type:decimal(20,4);comment:质押次数" json:"pledge_count"` + + // --- DatasetBlocksMember:板块覆盖度(成分条数) --- + BlockMemberCount int `gorm:"not null;default:0;comment:所属板块/指数成分条数" json:"block_member_count"` +} + +func (DatasetSummary) TableName() string { + return "dataset_summary" +} + +// UpdateByTsCode 根据 ts_code 从各源表聚合并写入或更新本表对应行。 +func (DatasetSummary) UpdateByTsCode(db *gorm.DB, tsCode string) error { + sum, err := buildDatasetSummary(db, tsCode) + if err != nil { + return err + } + return upsertDatasetSummary(db, sum) +} + +// UpdateAll 遍历 dataset_basic 中全部 ts_code,逐只聚合并写入或更新本表。 +func (DatasetSummary) UpdateAll(db *gorm.DB) error { + var codes []string + if err := db.Model(&DatasetBasic{}).Pluck("ts_code", &codes).Error; err != nil { + return err + } + var z DatasetSummary + for _, c := range codes { + if c == "" { + continue + } + if err := z.UpdateByTsCode(db, c); err != nil { + return err + } + } + return nil +} + +func upsertDatasetSummary(db *gorm.DB, sum *DatasetSummary) error { + if sum == nil || sum.TsCode == "" { + return errors.New("schema: invalid dataset_summary row") + } + var cur DatasetSummary + err := db.Where("ts_code = ?", sum.TsCode).Take(&cur).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return db.Create(sum).Error + } + if err != nil { + return err + } + sum.ID = cur.ID + return db.Save(sum).Error +} + +func buildDatasetSummary(db *gorm.DB, tsCode string) (*DatasetSummary, error) { + if tsCode == "" { + return nil, errors.New("schema: empty ts_code") + } + var basic DatasetBasic + if err := db.Where("ts_code = ?", tsCode).Take(&basic).Error; err != nil { + return nil, err + } + + var dip DatasetIndicatorPro + hasDip := db.Where("ts_code = ?", tsCode).Order("trade_date DESC").Take(&dip).Error == nil + + var day DatasetDaily + hasDay := db.Where("ts_code = ?", tsCode).Order("trade_date DESC").Take(&day).Error == nil + + var ind DatasetIndicator + hasInd := db.Where("ts_code = ?", tsCode).Order("trade_date DESC").Take(&ind).Error == nil + + var fina DatasetFinaIndicator + hasFina := db.Where("ts_code = ?", tsCode).Order("period DESC").Take(&fina).Error == nil + + last1Mf, last3Mf, last1Tot, last3Tot, mfGreater := aggregateMoneyForSummary(db, tsCode) + + var pledge DatasetPledgeStat + hasPledge := db.Where("ts_code = ?", tsCode).Take(&pledge).Error == nil + + var blockCnt int64 + if err := db.Model(&DatasetBlocksMember{}).Where("ts_code = ?", tsCode).Count(&blockCnt).Error; err != nil { + return nil, err + } + + updateYmd := todayYmd() + switch { + case hasDip && dip.TradeDate > 0: + updateYmd = dip.TradeDate + case hasDay && day.TradeDate > 0: + updateYmd = day.TradeDate + case hasInd && ind.TradeDate > 0: + updateYmd = ind.TradeDate + } + + s := &DatasetSummary{ + TsCode: tsCode, + UpdateYmd: updateYmd, + Name: basic.Name, + Industry: basic.Industry, + Area: basic.Area, + Market: basic.Market, + ListDate: basic.ListDate, + IsHS: basic.IsHS, + IsSt: isStFlag(basic.Name), + BlockMemberCount: int(blockCnt), + Last1DayMfAmount: last1Mf, + Last3DayMfAmount: last3Mf, + Last1DayTotalAmount: last1Tot, + Last3DayTotalAmount: last3Tot, + IsGreaterPervious: mfGreater, + } + + if hasPledge { + s.PledgeEndDate = pledge.EndDate + s.PledgeRatio = pledge.PledgeRatio + s.PledgeCount = pledge.PledgeCount + } + + if hasFina { + s.FinaPeriod = fina.Period + s.FinaAnnDate = fina.AnnDate + s.FinaEndDate = fina.EndDate + s.Eps = fina.Eps + s.Bps = fina.Bps + s.Ocfps = fina.Ocfps + s.Roe = fina.Roe + s.RoeWaa = fina.RoeWaa + s.Roa = fina.Roa + s.DebtToAssets = fina.DebtToAssets + s.GrossprofitMargin = fina.GrossprofitMargin + s.NetprofitYoy = fina.NetprofitYoy + s.OrYoy = fina.OrYoy + s.TrYoy = fina.TrYoy + } + + switch { + case hasDip: + s.Close = dip.Close + s.PctChg = dip.PctChg + s.Vol = dip.Vol + s.Amount = dip.Amount + s.TurnoverRate = dip.TurnoverRate + s.TurnoverRateF = dip.TurnoverRateF + s.VolumeRatio = dip.VolumeRatio + s.Pe = dip.Pe + s.PeTtm = dip.PeTtm + s.Pb = dip.Pb + s.PsTtm = dip.PsTtm + s.DvTtm = dip.DvTtm + s.TotalShare = dip.TotalShare + s.FloatShare = dip.FloatShare + s.FreeShare = dip.FreeShare + s.TotalMv = dip.TotalMv + s.CircMv = dip.CircMv + s.AdjFactor = dip.AdjFactor + s.MaBfq5 = dip.MaBfq5 + s.MaBfq20 = dip.MaBfq20 + s.MacdDifBfq = dip.MacdDifBfq + s.RsiBfq12 = dip.RsiBfq12 + case hasDay: + s.Close = day.Close + s.PctChg = day.PctChg + s.Vol = day.Vol + s.Amount = day.Amount + if hasInd { + s.TurnoverRate = ind.TurnoverRate + s.TurnoverRateF = ind.TurnoverRateF + s.VolumeRatio = ind.VolumeRatio + s.Pe = ind.Pe + s.PeTtm = ind.PeTtm + s.Pb = ind.Pb + s.PsTtm = ind.PsTtm + s.DvTtm = ind.DvTtm + s.TotalShare = ind.TotalShare + s.FloatShare = ind.FloatShare + s.FreeShare = ind.FreeShare + s.TotalMv = ind.TotalMv + s.CircMv = ind.CircMv + } + case hasInd: + s.Close = ind.Close + s.TurnoverRate = ind.TurnoverRate + s.TurnoverRateF = ind.TurnoverRateF + s.VolumeRatio = ind.VolumeRatio + s.Pe = ind.Pe + s.PeTtm = ind.PeTtm + s.Pb = ind.Pb + s.PsTtm = ind.PsTtm + s.DvTtm = ind.DvTtm + s.TotalShare = ind.TotalShare + s.FloatShare = ind.FloatShare + s.FreeShare = ind.FreeShare + s.TotalMv = ind.TotalMv + s.CircMv = ind.CircMv + } + + return s, nil +} + +// aggregateMoneyForSummary 由 dataset_money_total(moneyflow)与 dataset_daily 聚合近 1/3 日指标。 +// 主力净流入取源表 MainForceNetWan(万元);按 update_ymd 从新到旧取至多 3 行。成交额取日线 amount(千元)换算为万元。 +func aggregateMoneyForSummary(db *gorm.DB, tsCode string) (last1Mf, last3Mf, last1Tot, last3Tot float64, greater bool) { + var mfRows []DatasetMoney + _ = db.Where("ts_code = ?", tsCode).Order("update_ymd DESC, id DESC").Limit(3).Find(&mfRows).Error + for i, r := range mfRows { + v := r.MainForceNetWan + if i == 0 { + last1Mf = v + } + if i < 3 { + last3Mf += v + } + } + if len(mfRows) >= 2 { + greater = mfRows[0].MainForceNetWan > mfRows[1].MainForceNetWan + } + var dayRows []DatasetDaily + _ = db.Where("ts_code = ?", tsCode).Order("trade_date DESC").Limit(3).Find(&dayRows).Error + for i, d := range dayRows { + wan := d.Amount / 10 // 千元 -> 万元 + if i == 0 { + last1Tot = wan + } + if i < 3 { + last3Tot += wan + } + } + return +} + +func todayYmd() int { + n, err := strconv.Atoi(time.Now().Format("20060102")) + if err != nil { + return 0 + } + return n +} + +func isStFlag(name string) string { + s := strings.TrimSpace(name) + if len(s) >= 3 && strings.HasPrefix(s, "*ST") { + return "Y" + } + if len(s) >= 2 && strings.HasPrefix(s, "ST") { + return "Y" + } + return "N" +} diff --git a/schema/register.go b/schema/register.go index 3bc78d3..953fe63 100644 --- a/schema/register.go +++ b/schema/register.go @@ -9,11 +9,12 @@ func RegisterAutoMigrate() { &DatasetDaily{}, &DatasetBlocksIndex{}, &DatasetBlocksMember{}, - &DatasetMoneyTotal{}, + &DatasetMoney{}, &DatasetPledgeStat{}, &DatasetIndicator{}, &DatasetIndicatorPro{}, &DatasetFinaIndicator{}, + &DatasetSummary{}, } { database.AppendMigrate(t) } diff --git a/tushare/finance.go b/tushare/finance.go index 060f529..91583ec 100644 --- a/tushare/finance.go +++ b/tushare/finance.go @@ -117,17 +117,26 @@ func (cli *TushareClient) Moneyflow(ts_code, trade_date, start_date, end_date st } fields := []map[string]string{ - {"ts_code": "股票代码"}, + {"ts_code": "TS代码"}, {"trade_date": "交易日期"}, - {"buy_sm_amount": "小单买入金额 (千元)"}, - {"sell_sm_amount": "小单卖出金额 (千元)"}, - {"buy_md_amount": "中单买入金额 (千元)"}, - {"sell_md_amount": "中单卖出金额 (千元)"}, - {"buy_lg_amount": "大单买入金额 (千元)"}, - {"sell_lg_amount": "大单卖出金额 (千元)"}, - {"buy_elg_amount": "特大单买入金额 (千元)"}, - {"sell_elg_amount": "特大单卖出金额 (千元)"}, - {"net_mf_amount": "净流入金额 (千元)"}, + {"buy_sm_vol": "小单买入量 (手)"}, + {"buy_sm_amount": "小单买入金额 (万元)"}, + {"sell_sm_vol": "小单卖出量 (手)"}, + {"sell_sm_amount": "小单卖出金额 (万元)"}, + {"buy_md_vol": "中单买入量 (手)"}, + {"buy_md_amount": "中单买入金额 (万元)"}, + {"sell_md_vol": "中单卖出量 (手)"}, + {"sell_md_amount": "中单卖出金额 (万元)"}, + {"buy_lg_vol": "大单买入量 (手)"}, + {"buy_lg_amount": "大单买入金额 (万元)"}, + {"sell_lg_vol": "大单卖出量 (手)"}, + {"sell_lg_amount": "大单卖出金额 (万元)"}, + {"buy_elg_vol": "特大单买入量 (手)"}, + {"buy_elg_amount": "特大单买入金额 (万元)"}, + {"sell_elg_vol": "特大单卖出量 (手)"}, + {"sell_elg_amount": "特大单卖出金额 (万元)"}, + {"net_mf_vol": "净流入量 (手)"}, + {"net_mf_amount": "净流入额 (万元)"}, } return cli.Do(req, fields)