Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
139
internal/datasource/eastmoney/convertible.go
Normal file
139
internal/datasource/eastmoney/convertible.go
Normal file
@@ -0,0 +1,139 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user