Initial commit: 可转债轮动分析系统

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

View File

@@ -0,0 +1,114 @@
// 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()
}