- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
100 lines
2.5 KiB
Go
100 lines
2.5 KiB
Go
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
|
||
}
|