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

138 lines
4.3 KiB
Go
Raw Permalink 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 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
}