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,116 @@
package eastmoney
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"stock-analysis/internal/model"
)
// datacenterFundamentalResp 是 datacenter 财务数据接口响应。
type datacenterFundamentalResp struct {
Result struct {
Pages int `json:"pages"`
Data []datacenterFundamentalItem `json:"data"`
} `json:"result"`
Success bool `json:"success"`
Message string `json:"message"`
}
// datacenterFundamentalItem 是单条财务数据记录。
type datacenterFundamentalItem struct {
SecurityCode string `json:"SECURITY_CODE"`
SecurityName string `json:"SECURITY_NAME_ABBR"`
ROE *float64 `json:"ROEJQ"` // 加权净资产收益率(%)
DebtRatio *float64 `json:"ZCFZL"` // 资产负债率(%)
NetProfit *float64 `json:"PARENTNETPROFIT"` // 归属净利润(元)
IsBZ string `json:"IS_BZ"` // ST状态:0=正常, 1=ST, 2=*ST
ReportDate string `json:"REPORT_DATE"`
}
// FetchStockFundamentals 批量拉取指定正股的最新年度基本面数据。
// 使用 datacenter API(非 push2),按 REPORT_DATE 倒序取每只股票的最新一条年报。
func (c *Client) FetchStockFundamentals(ctx context.Context, codes []string) ([]model.StockFundamental, error) {
if len(codes) == 0 {
return nil, nil
}
codeSet := make(map[string]bool, len(codes))
for _, code := range codes {
codeSet[code] = true
}
const pageSize = 5000
seen := make(map[string]bool, len(codes))
var funds []model.StockFundamental
// 按 REPORT_DATE 倒序拉取,每只股票取第一条(最新年报)。
for page := 1; page <= 3; page++ {
params := url.Values{}
params.Set("reportName", "RPT_F10_FINANCE_MAINFINADATA")
params.Set("columns", "SECURITY_CODE,SECURITY_NAME_ABBR,ROEJQ,ZCFZL,PARENTNETPROFIT,IS_BZ,REPORT_DATE")
params.Set("filter", `(REPORT_TYPE="年报")`)
params.Set("pageNumber", strconv.Itoa(page))
params.Set("pageSize", strconv.Itoa(pageSize))
params.Set("sortColumns", "REPORT_DATE")
params.Set("sortTypes", "-1")
var resp datacenterFundamentalResp
if err := c.getJSON(ctx, []string{datacenterHost}, "/api/data/v1/get", params, &resp); err != nil {
return nil, fmt.Errorf("datacenter page %d: %w", page, err)
}
if !resp.Success {
return nil, fmt.Errorf("datacenter API error: %s", resp.Message)
}
if len(resp.Result.Data) == 0 {
break
}
for _, d := range resp.Result.Data {
code := d.SecurityCode
if code == "" || seen[code] || !codeSet[code] {
continue
}
seen[code] = true
roe := 0.0
if d.ROE != nil {
roe = *d.ROE
}
debtRatio := 0.0
if d.DebtRatio != nil {
debtRatio = *d.DebtRatio
}
netProfit := 0.0
if d.NetProfit != nil {
netProfit = *d.NetProfit
}
status := 1
if d.IsBZ == "1" || d.IsBZ == "2" {
status = 2
}
updateDate := strings.Split(d.ReportDate, " ")[0]
funds = append(funds, model.StockFundamental{
Code: code,
Name: d.SecurityName,
ROE: roe,
DebtRatio: debtRatio,
NetProfit: netProfit,
Status: status,
UpdateDate: updateDate,
})
}
// 已找到全部目标股票,停止翻页。
if len(seen) >= len(codes) {
break
}
}
return funds, nil
}