Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
137
internal/datasource/tencent/realtime.go
Normal file
137
internal/datasource/tencent/realtime.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package tencent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"stock-analysis/internal/model"
|
||||
)
|
||||
|
||||
// quoteHost 腾讯实时行情接口域名(qt.gtimg.cn)。
|
||||
// 支持批量请求:q=sh113667,sh110062,sz123001;单次建议不超过 100 只。
|
||||
const quoteHost = "https://qt.gtimg.cn"
|
||||
|
||||
// quoteBatchSize 腾讯实时行情单次请求的最大标的数量。
|
||||
// 实测 50 只稳定,超过 100 可能触发限流。
|
||||
const quoteBatchSize = 50
|
||||
|
||||
// quoteClient 腾讯实时行情专用 HTTP 客户端(与 K 线客户端隔离,避免共享限流计数)。
|
||||
var quoteClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// FetchQuotes 批量拉取转债实时报价,回填到 bonds 中各标的的 Price 和 ChangePct 字段。
|
||||
//
|
||||
// 用途:作为东财 push2 实时行情接口故障时的备用报价源。
|
||||
// 调用方(如 fallback 包)先用东财 datacenter 拿到转债列表,再用本方法补充实时价格。
|
||||
//
|
||||
// 接口:https://qt.gtimg.cn/q=sh113667,sh110062,...(GET)
|
||||
// 响应每行一只,字段以 `~` 分隔,0 起始索引:1=名称, 2=代码, 3=当前价, 4=昨收, 31=涨跌幅。
|
||||
//
|
||||
// 已退市/停牌/未上市转债当前价为 0,调用方可据此过滤。
|
||||
func FetchQuotes(ctx context.Context, bonds []model.ConvertibleBond) error {
|
||||
if len(bonds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < len(bonds); i += quoteBatchSize {
|
||||
end := i + quoteBatchSize
|
||||
if end > len(bonds) {
|
||||
end = len(bonds)
|
||||
}
|
||||
batch := bonds[i:end]
|
||||
|
||||
symbols := make([]string, 0, len(batch))
|
||||
for _, b := range batch {
|
||||
symbols = append(symbols, symbolOf(b.Code))
|
||||
}
|
||||
reqURL := fmt.Sprintf("%s/q=%s", quoteHost, strings.Join(symbols, ","))
|
||||
|
||||
body, err := httpGet(ctx, quoteClient, reqURL)
|
||||
if err != nil {
|
||||
// 单批失败不中断整体流程,已退市标的会被调用方按 price=0 过滤。
|
||||
continue
|
||||
}
|
||||
parseQuoteBatch(body, batch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseQuoteBatch 解析腾讯批量实时行情响应,回填到 batch 中各转债的 Price/ChangePct 字段。
|
||||
//
|
||||
// 响应格式(每行一只,以 `;` 分隔):
|
||||
//
|
||||
// v_sh113667="1~春23转债~113667~210.976~214.702~...~-3.726~-1.74~...";
|
||||
// v_sz123001="51~蓝标转债~123001~135.998~135.998~...~0.000~0.00~...";
|
||||
//
|
||||
// 字段以 `~` 分隔,0 起始索引:
|
||||
//
|
||||
// 0: 市场标识(1=沪, 51=深), 1: 名称, 2: 代码, 3: 当前价, 4: 昨收
|
||||
// 30: 时间戳(YYYYMMDDHHmmss), 31: 涨跌额, 32: 涨跌幅(%)
|
||||
// 腾讯按 q 参数顺序返回,行数与 batch 对应(无数据标的返回 "v_=;")。
|
||||
func parseQuoteBatch(body string, batch []model.ConvertibleBond) {
|
||||
// 腾讯响应以 ";\n" 或 ";" 分隔,统一去重空行
|
||||
body = strings.ReplaceAll(body, "\r\n", "\n")
|
||||
body = strings.ReplaceAll(body, "\n", "")
|
||||
lines := strings.Split(body, ";")
|
||||
|
||||
for i := range batch {
|
||||
if i >= len(lines) {
|
||||
break
|
||||
}
|
||||
price, chgPct := parseQuoteLine(lines[i])
|
||||
if price > 0 {
|
||||
batch[i].Price = price
|
||||
batch[i].ChangePct = chgPct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseQuoteLine 解析单行腾讯实时行情,返回 (当前价, 涨跌幅%)。
|
||||
// 无法解析或无数据返回 (0, 0)。
|
||||
func parseQuoteLine(line string) (price, changePct float64) {
|
||||
// 行格式:v_sh113667="1~春23转债~113667~210.976~...";
|
||||
start := strings.Index(line, "\"")
|
||||
if start < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
end := strings.LastIndex(line, "\"")
|
||||
if end <= start {
|
||||
return 0, 0
|
||||
}
|
||||
content := line[start+1 : end]
|
||||
fields := strings.Split(content, "~")
|
||||
// 字段:0=市场标识, 1=名称, 2=代码, 3=当前价, 4=昨收, ..., 31=涨跌额, 32=涨跌幅
|
||||
if len(fields) < 33 {
|
||||
return 0, 0
|
||||
}
|
||||
price, _ = strconv.ParseFloat(fields[3], 64)
|
||||
changePct, _ = strconv.ParseFloat(fields[32], 64)
|
||||
return price, changePct
|
||||
}
|
||||
|
||||
// httpGet 执行 GET 请求并返回响应体字符串。
|
||||
func httpGet(ctx context.Context, client *http.Client, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", 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 := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("http status %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
Reference in New Issue
Block a user