package backtest import ( "context" "stock-analysis/internal/model" ) // FundamentalFilter 正股基本面过滤器,用于排除信用风险过高的转债。 // 过滤规则: // - ST / *ST 正股对应的转债一律排除 // - 资产负债率超过阈值的转债排除 // - 净利润为负(连续亏损)的转债排除 type FundamentalFilter struct { MaxDebtRatio float64 // 资产负债率上限(%),默认 70 stock2bond map[string]string enabled bool } // NewFundamentalFilter 创建基本面过滤器。 // stock2bond 为正股代码到转债代码的映射(一只正股可能对应多只转债,取任意一只即可)。 func NewFundamentalFilter(stock2bond map[string]string) *FundamentalFilter { return &FundamentalFilter{ MaxDebtRatio: 70, stock2bond: stock2bond, enabled: true, } } // SetEnabled 启用/禁用过滤器。 func (f *FundamentalFilter) SetEnabled(v bool) { f.enabled = v } // Apply 给定当日 universe 行情和基本面数据,返回过滤后的行情列表。 // fundamentals 的 key 为正股代码。 func (f *FundamentalFilter) Apply(ctx context.Context, universe []Quote, fundamentals map[string]*model.StockFundamental) []Quote { if !f.enabled || len(fundamentals) == 0 { return universe } // 构建转债代码到正股代码的映射(反向查找)。 bond2stock := make(map[string]string, len(f.stock2bond)) for stock, bond := range f.stock2bond { bond2stock[bond] = stock } filtered := make([]Quote, 0, len(universe)) for _, q := range universe { stockCode, ok := bond2stock[q.Code] if !ok { // 无正股映射,允许通过(不因数据缺失误杀)。 filtered = append(filtered, q) continue } fund, ok := fundamentals[stockCode] if !ok { // 无基本面数据,允许通过。 filtered = append(filtered, q) continue } if f.isBlocked(fund) { continue } filtered = append(filtered, q) } return filtered } // isBlocked 判断正股基本面是否触发过滤条件。 func (f *FundamentalFilter) isBlocked(fund *model.StockFundamental) bool { // ST / *ST 一律排除。 if fund.Status >= 2 { return true } // 资产负债率超过阈值。 if f.MaxDebtRatio > 0 && fund.DebtRatio > f.MaxDebtRatio { return true } // 净利润为负(连续亏损)。 if fund.NetProfit < 0 { return true } return false }