Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
125
internal/datasource/sina/convert.go
Normal file
125
internal/datasource/sina/convert.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Package sina 新浪财经数据源:提供可转债转股价变动历史。
|
||||
// 数据来源:money.finance.sina.com.cn/bond/conversion/{market}{code}.html(转股价变动表),
|
||||
// 含初始转股价与历次调整/修正记录,字段:生效日期 + 转股价。
|
||||
package sina
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"stock-analysis/internal/model"
|
||||
)
|
||||
|
||||
// conversionBase 新浪转债行权详情页(含转股价变动历史)。
|
||||
const conversionBase = "http://money.finance.sina.com.cn/bond/conversion/%s%s.html"
|
||||
|
||||
var (
|
||||
bluecntRowRe = regexp.MustCompile(`<tr class="bluecnt">(.*?)</tr>`)
|
||||
cellRe = regexp.MustCompile(`<td[^>]*>(.*?)</td>`)
|
||||
stripTagRe = regexp.MustCompile(`<[^>]+>`)
|
||||
spaceRe = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// Client 是新浪财经客户端。
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// NewClient 创建新浪财经客户端。
|
||||
func NewClient() *Client {
|
||||
return &Client{http: &http.Client{Timeout: 15 * time.Second}}
|
||||
}
|
||||
|
||||
// FetchConvertPrices 拉取指定转债的转股价变动历史(含初始转股价)。
|
||||
// 返回按生效日期升序的记录;页面缺失时返回空切片(由调用方用初始转股价兜底)。
|
||||
func (c *Client) FetchConvertPrices(ctx context.Context, code string) ([]model.ConvertPrice, error) {
|
||||
reqURL := fmt.Sprintf(conversionBase, symbolOf(code), code)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 新浪页面为 GBK 编码,转成 UTF-8 后解析。
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
utf8Body, _, err := transform.Bytes(simplifiedchinese.GBK.NewDecoder(), body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode gbk: %w", err)
|
||||
}
|
||||
|
||||
return parseConvertPrices(string(utf8Body)), nil
|
||||
}
|
||||
|
||||
// parseConvertPrices 从 HTML 中提取转股价变动表的记录。
|
||||
// 仅解析"转股价变动 begin/end"注释区间内的数据行,避免误取行权/回售表。
|
||||
func parseConvertPrices(html string) []model.ConvertPrice {
|
||||
const beginMark = "<!--转股价变动 begin-->"
|
||||
const endMark = "<!--转股价变动 end-->"
|
||||
beginIdx := strings.Index(html, beginMark)
|
||||
if beginIdx < 0 {
|
||||
return nil
|
||||
}
|
||||
seg := html[beginIdx+len(beginMark):]
|
||||
if endIdx := strings.Index(seg, endMark); endIdx >= 0 {
|
||||
seg = seg[:endIdx]
|
||||
}
|
||||
|
||||
var prices []model.ConvertPrice
|
||||
for _, row := range bluecntRowRe.FindAllString(seg, -1) {
|
||||
cells := cellsOf(row)
|
||||
// 列:序号 | 价格变动类型 | 公告日期 | 生效日期 | 执行日期 | 转股价 | 转股比例
|
||||
if len(cells) < 6 {
|
||||
continue
|
||||
}
|
||||
effective := strings.TrimSpace(cells[3])
|
||||
price, err := strconv.ParseFloat(strings.TrimSpace(cells[5]), 64)
|
||||
if err != nil || effective == "" || price <= 0 {
|
||||
continue
|
||||
}
|
||||
prices = append(prices, model.ConvertPrice{
|
||||
EffectiveDate: effective,
|
||||
Price: price,
|
||||
})
|
||||
}
|
||||
return prices
|
||||
}
|
||||
|
||||
// cellsOf 解析一行 <tr> 中所有 <td> 的文本(去标签去空白)。
|
||||
func cellsOf(row string) []string {
|
||||
var cells []string
|
||||
for _, m := range cellRe.FindAllStringSubmatch(row, -1) {
|
||||
text := stripTagRe.ReplaceAllString(m[1], "")
|
||||
text = spaceRe.ReplaceAllString(text, "")
|
||||
cells = append(cells, text)
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
// symbolOf 生成新浪标的符号(沪市 sh / 深市 sz),与腾讯规则一致。
|
||||
func symbolOf(code string) string {
|
||||
if strings.HasPrefix(code, "12") {
|
||||
return "sz"
|
||||
}
|
||||
return "sh"
|
||||
}
|
||||
Reference in New Issue
Block a user