package eastmoney import ( "context" "encoding/json" "net/url" "strconv" "strings" "stock-analysis/internal/model" ) // clistResp 是 clist 接口响应结构。 type clistResp struct { Data struct { Total int `json:"total"` Diff json.RawMessage `json:"diff"` } `json:"data"` } // rawBond 是 clist 接口返回的可转债原始字段。 // 东财的值可能是数字或 "-" 字符串,数字字段统一用 json.RawMessage 容错。 type rawBond struct { F2 json.RawMessage `json:"f2"` // 转债最新价 F3 json.RawMessage `json:"f3"` // 转债涨跌幅 F12 string `json:"f12"` // 转债代码 F14 string `json:"f14"` // 转债名称 F26 json.RawMessage `json:"f26"` // 上市日期 F227 json.RawMessage `json:"f227"` // 纯债价值 F228 json.RawMessage `json:"f228"` // 正股最新价 F230 json.RawMessage `json:"f230"` // 正股涨跌幅 F232 string `json:"f232"` // 正股代码 F234 string `json:"f234"` // 正股名称 F235 json.RawMessage `json:"f235"` // 转股价 F236 json.RawMessage `json:"f236"` // 转股价值 F237 json.RawMessage `json:"f237"` // 转股溢价率 F238 json.RawMessage `json:"f238"` // 纯债溢价率 F239 json.RawMessage `json:"f239"` // 回售触发价 F240 json.RawMessage `json:"f240"` // 强赎触发价 } // FetchConvertibles 分页拉取全量可转债实时行情。 func (c *Client) FetchConvertibles(ctx context.Context) ([]model.ConvertibleBond, error) { const pageSize = 100 // 东财 clist 单页上限 bonds := make([]model.ConvertibleBond, 0, 400) for page := 1; ; page++ { params := url.Values{} params.Set("pn", strconv.Itoa(page)) params.Set("pz", strconv.Itoa(pageSize)) params.Set("po", "1") params.Set("np", "1") params.Set("fltt", "2") params.Set("invt", "2") params.Set("fid", "f3") params.Set("fs", "b:MK0354") // 可转债市场 params.Set("fields", "f2,f3,f12,f14,f26,f227,f228,f230,f232,f234,f235,f236,f237,f238,f239,f240") var resp clistResp if err := c.getJSON(ctx, realTimeHosts, "/api/qt/clist/get", params, &resp); err != nil { return nil, err } if len(resp.Data.Diff) == 0 { break } // diff 可能是数组,也可能(仅一条时)是对象。 var items []json.RawMessage if isJSONArray(resp.Data.Diff) { if err := json.Unmarshal(resp.Data.Diff, &items); err != nil { return nil, err } } else { items = []json.RawMessage{resp.Data.Diff} } for _, raw := range items { var b rawBond if err := json.Unmarshal(raw, &b); err != nil { continue } if b.F12 == "" { continue } bonds = append(bonds, model.ConvertibleBond{ Code: b.F12, Name: b.F14, Price: parseFloat(b.F2), ChangePct: parseFloat(b.F3), ListDate: parseDate(b.F26), StockCode: b.F232, StockName: b.F234, StockPrice: parseFloat(b.F228), StockChangePct: parseFloat(b.F230), ConvertPrice: parseFloat(b.F235), ConvertValue: parseFloat(b.F236), PremiumRate: parseFloat(b.F237), PureBondVal: parseFloat(b.F227), PureBondPremium: parseFloat(b.F238), ResaleTrigPrice: parseFloat(b.F239), RedeemTrigPrice: parseFloat(b.F240), }) } // 拉满即停止。 if len(bonds) >= resp.Data.Total { break } } return bonds, nil } // isJSONArray 判断原始 JSON 是否为数组。 func isJSONArray(raw json.RawMessage) bool { s := strings.TrimSpace(string(raw)) return strings.HasPrefix(s, "[") } // parseFloat 容错解析东财字段:数字、"-"、空串、null 均处理。 func parseFloat(raw json.RawMessage) float64 { if len(raw) == 0 { return 0 } s := strings.Trim(string(raw), `"`) if s == "" || s == "-" || s == "null" || s == "None" { return 0 } v, _ := strconv.ParseFloat(s, 64) return v } // parseDate 解析东财日期字段(如 20260812 -> 2006-01-02 格式)。 func parseDate(raw json.RawMessage) string { s := strings.Trim(string(raw), `"`) if len(s) != 8 { return s } return s[:4] + "-" + s[4:6] + "-" + s[6:8] }