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