- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
140 lines
4.0 KiB
Go
140 lines
4.0 KiB
Go
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]
|
|
}
|