Files
stock-analysis/internal/datasource/sina/convert.go
WarrenZhou e28bfec957 Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
2026-08-17 20:23:22 +08:00

126 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
}