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,99 @@
package eastmoney
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"stock-analysis/internal/model"
)
// Kline 周期常量(东财 klt 参数)。
const (
KltDay = "101" // 日K
KltWeek = "102" // 周K
KltMonth = "103" // 月K
)
// klineResp 是 K线接口响应结构。
type klineResp struct {
Data *struct {
Code string `json:"code"`
Name string `json:"name"`
Klines []string `json:"klines"`
} `json:"data"`
}
// FetchKline 拉取指定标的历史K线。
// code 为标的代码(如 113667),klt 为周期,beg/end 形如 20200101。
// 返回按时间升序排列的K线。
func (c *Client) FetchKline(ctx context.Context, code, klt, beg, end string) ([]model.Kline, error) {
secid := secidOf(code)
params := url.Values{}
params.Set("secid", secid)
params.Set("klt", klt)
params.Set("fqt", "0") // 不复权(转债回测用不复权价)
params.Set("beg", beg)
params.Set("end", end)
params.Set("fields1", "f1,f2,f3,f4,f5,f6")
params.Set("fields2", "f51,f52,f53,f54,f55,f56,f57")
var resp klineResp
if err := c.getJSON(ctx, historyHosts, "/api/qt/stock/kline/get", params, &resp); err != nil {
return nil, err
}
if resp.Data == nil || len(resp.Data.Klines) == 0 {
return nil, fmt.Errorf("kline empty: secid=%s", secid)
}
klines := make([]model.Kline, 0, len(resp.Data.Klines))
for _, line := range resp.Data.Klines {
// 格式:日期,开,收,高,低,成交量,成交额
parts := strings.Split(line, ",")
if len(parts) < 7 {
continue
}
k, err := parseKline(parts)
if err != nil {
continue
}
klines = append(klines, k)
}
return klines, nil
}
// secidOf 根据代码生成东财 secid。
// 沪市(11/118 开头)前缀 1,深市(12 开头)前缀 0。
func secidOf(code string) string {
if strings.HasPrefix(code, "12") {
return "0." + code
}
return "1." + code
}
// parseKline 解析一行K线数据。
func parseKline(parts []string) (model.Kline, error) {
open, err1 := strconv.ParseFloat(parts[1], 64)
close_, err2 := strconv.ParseFloat(parts[2], 64)
high, err3 := strconv.ParseFloat(parts[3], 64)
low, err4 := strconv.ParseFloat(parts[4], 64)
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
return model.Kline{}, fmt.Errorf("bad kline numbers")
}
volume, err := strconv.ParseInt(parts[5], 10, 64)
if err != nil {
volume = 0
}
amount, _ := strconv.ParseFloat(parts[6], 64)
return model.Kline{
Date: parts[0],
Open: open,
Close: close_,
High: high,
Low: low,
Volume: volume,
Amount: amount,
}, nil
}