// Package eastmoney 东财数据源:datacenter(列表/基础信息)与 push2(实时/历史行情)。 package eastmoney import ( "context" "encoding/json" "errors" "net/url" "strconv" "time" "stock-analysis/internal/model" ) // datacenterHost 东财数据中心接口域名(与 push2 分属不同集群,限流相对宽松)。 const datacenterHost = "https://datacenter-web.eastmoney.com" // datacenterResp 是 datacenter 列表接口响应结构。 type datacenterResp struct { Result struct { Data []struct { SecurityCode string `json:"SECURITY_CODE"` // 转债代码 SecurityName string `json:"SECURITY_NAME_ABBR"` // 转债简称 BondStartDate string `json:"BOND_START_DATE"` // 发行日期 ListingDate string `json:"LISTING_DATE"` // 上市日期(部分记录缺失) DelistDate string `json:"DELIST_DATE"` // 退市日期(未退市为空) ConvertStockCode string `json:"CONVERT_STOCK_CODE"` // 正股代码 InitialTransferPice float64 `json:"INITIAL_TRANSFER_PRICE"` // 初始转股价 } `json:"data"` } `json:"result"` Success bool `json:"success"` } // FetchAllBondCodes 从 datacenter 分页拉取全量转债代码列表(含已退市,约 1000 只)。 // 相比 push2 clist 接口不受实时行情集群限流影响,适合批量抓取场景。 // 返回的 ConvertibleBond 含正股代码/初始转股价/上市退市日期。 func (c *Client) FetchAllBondCodes(ctx context.Context) ([]model.ConvertibleBond, error) { const pageSize = 100 var bonds []model.ConvertibleBond for page := 1; ; page++ { params := url.Values{} params.Set("reportName", "RPT_BOND_CB_LIST") params.Set("columns", "SECURITY_CODE,SECURITY_NAME_ABBR,BOND_START_DATE,LISTING_DATE,DELIST_DATE,CONVERT_STOCK_CODE,INITIAL_TRANSFER_PRICE") params.Set("pageNumber", strconv.Itoa(page)) params.Set("pageSize", strconv.Itoa(pageSize)) var resp datacenterResp if err := c.getJSON(ctx, []string{datacenterHost}, "/api/data/v1/get", params, &resp); err != nil { return nil, err } if len(resp.Result.Data) == 0 { break } for _, d := range resp.Result.Data { if d.SecurityCode == "" { continue } listDate := d.ListingDate if len(listDate) < 10 { listDate = d.BondStartDate // 兜底:部分老债缺上市日期 } delistDate := "" if len(d.DelistDate) >= 10 { delistDate = d.DelistDate[:10] } bonds = append(bonds, model.ConvertibleBond{ Code: d.SecurityCode, Name: d.SecurityName, StockCode: d.ConvertStockCode, InitialConvertPrice: d.InitialTransferPice, ListDate: listDate[:10], DelistDate: delistDate, }) } if len(resp.Result.Data) < pageSize { break // 最后一页 } } return bonds, nil } // FetchLatestBonds 获取最近上市(已上市且有行情数据)的 n 只可转债。 // 走 push2 clist 接口按上市日期(f26)倒序,过滤上市日期缺失及尚未上市(未来日期)的记录。 func (c *Client) FetchLatestBonds(ctx context.Context, n int) ([]model.ConvertibleBond, error) { const fetchSize = 50 // 放宽单页数量,确保过滤后仍有足够样本 params := url.Values{} params.Set("pn", "1") params.Set("pz", strconv.Itoa(fetchSize)) params.Set("po", "1") params.Set("np", "1") params.Set("fltt", "2") params.Set("invt", "2") params.Set("fid", "f26") // 按上市日期排序 params.Set("fs", "b:MK0354") params.Set("fields", "f12,f14,f26") 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 { return nil, errors.New("eastmoney: empty clist") } 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} } today := time.Now().Format("2006-01-02") bonds := make([]model.ConvertibleBond, 0, n) for _, raw := range items { var b rawBond if err := json.Unmarshal(raw, &b); err != nil { continue } if b.F12 == "" { continue } listDate := parseDate(b.F26) // 跳过上市日期缺失或尚未上市(日期在未来)的转债。 if listDate == "" || listDate > today { continue } bonds = append(bonds, model.ConvertibleBond{ Code: b.F12, Name: b.F14, ListDate: listDate, }) if len(bonds) >= n { break } } return bonds, nil }