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

115 lines
3.4 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 eastmoney 实现 datasource.MarketDataSource 接口。
// 数据来源:push2.eastmoney.com(实时行情)、push2his.eastmoney.com(历史K线)。
// 注意:该接口为非官方免费接口,无 SLA 保障,使用需限速并做好容错。
package eastmoney
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
// requestInterval 请求间隔,避免高频调用被封 IP(东财对短时高频请求限流较严格)。
requestInterval = 2 * time.Second
// maxRetry 每个域名上的失败重试次数。
maxRetry = 2
)
// realTimeHosts 实时行情域名列表,主域名被限流时轮询备用(东财数字前缀节点)。
var realTimeHosts = []string{
"https://push2.eastmoney.com",
"https://16.push2.eastmoney.com",
"https://20.push2.eastmoney.com",
"https://30.push2.eastmoney.com",
"https://41.push2.eastmoney.com",
"https://54.push2.eastmoney.com",
"https://69.push2.eastmoney.com",
"https://82.push2.eastmoney.com",
}
// historyHosts 历史K线域名列表(主域名被限流时轮询备用节点)。
var historyHosts = []string{
"https://push2his.eastmoney.com",
"https://16.push2his.eastmoney.com",
"https://20.push2his.eastmoney.com",
"https://30.push2his.eastmoney.com",
"https://41.push2his.eastmoney.com",
"https://54.push2his.eastmoney.com",
"https://69.push2his.eastmoney.com",
"https://82.push2his.eastmoney.com",
}
// Client 是东方财富行情接口客户端,实现 datasource.MarketDataSource。
type Client struct {
http *http.Client
lastReq time.Time
}
// NewClient 创建东财行情客户端。
func NewClient() *Client {
return &Client{
http: &http.Client{Timeout: 15 * time.Second},
}
}
// getJSON 带限速与多域名轮询重试的 GET 请求,解析 JSON 到 out。
// hostIdx 标识域名列表(0 实时 / 1 历史),path 为接口路径(如 /api/qt/clist/get)。
func (c *Client) getJSON(ctx context.Context, hosts []string, path string, params url.Values, out any) error {
var lastErr error
for _, base := range hosts {
reqURL := base + path + "?" + params.Encode()
for attempt := 0; attempt < maxRetry; attempt++ {
if attempt > 0 {
// 退避:3s、6s
time.Sleep(time.Duration(3*(attempt+1)) * time.Second)
}
if err := c.fetch(ctx, reqURL, out); err != nil {
lastErr = err
continue
}
return nil
}
}
return fmt.Errorf("eastmoney request failed across all hosts: %w", lastErr)
}
// fetch 执行 HTTP GET 并解析 JSON。
func (c *Client) fetch(ctx context.Context, reqURL string, out any) error {
c.throttle()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return err
}
// 模拟浏览器 UA,降低被风控概率。
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")
req.Header.Set("Referer", "https://quote.eastmoney.com/")
resp, err := c.http.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 json.Unmarshal(body, out)
}
// throttle 保证请求间隔不小于 requestInterval。
func (c *Client) throttle() {
elapsed := time.Since(c.lastReq)
if wait := requestInterval - elapsed; wait > 0 {
time.Sleep(wait)
}
c.lastReq = time.Now()
}