279 lines
6.4 KiB
Go
279 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type dailyCache struct {
|
|
Date string `json:"date"`
|
|
FetchedAt string `json:"fetched_at"`
|
|
OK bool `json:"ok"`
|
|
Data any `json:"data"`
|
|
}
|
|
|
|
var memCache sync.Map
|
|
|
|
func getJSON(rawURL string, params url.Values, timeout time.Duration) (map[string]any, error) {
|
|
if params != nil {
|
|
if strings.Contains(rawURL, "?") {
|
|
rawURL += "&" + params.Encode()
|
|
} else {
|
|
rawURL += "?" + params.Encode()
|
|
}
|
|
}
|
|
req, err := http.NewRequest(http.MethodGet, rawURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "big-qmt-go-zt/1")
|
|
client := &http.Client{Timeout: timeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
out := map[string]any{}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func daily(cfg Config, name, filename string, loader func() (any, error), now time.Time) *dailyCache {
|
|
if int(parseHM(now)) < cfg.ReadyCacheStart {
|
|
return nil
|
|
}
|
|
day := now.Format("20060102")
|
|
path := filepath.Join(cfg.DataDir, fmt.Sprintf(filename, day))
|
|
if v, ok := memCache.Load(path); ok {
|
|
if c, ok := v.(*dailyCache); ok && c.Date == day {
|
|
return c
|
|
}
|
|
}
|
|
cached := loadDailyFile(path)
|
|
if cached != nil && cached.Date == day {
|
|
memCache.Store(path, cached)
|
|
return cached
|
|
}
|
|
data, err := loader()
|
|
ok := err == nil
|
|
if err != nil {
|
|
logf("ERROR", "%s 当日请求失败: %v", name, err)
|
|
data = map[string]any{}
|
|
}
|
|
cached = &dailyCache{
|
|
Date: day,
|
|
FetchedAt: now.Format("2006-01-02 15:04:05"),
|
|
OK: ok,
|
|
Data: data,
|
|
}
|
|
raw, _ := json.MarshalIndent(map[string]any{"version": 1, "data": map[string]any{
|
|
"date": cached.Date, "fetched_at": cached.FetchedAt, "ok": cached.OK, "data": cached.Data,
|
|
}}, "", " ")
|
|
if err := os.WriteFile(path+".tmp", raw, 0o644); err == nil {
|
|
_ = os.Rename(path+".tmp", path)
|
|
}
|
|
memCache.Store(path, cached)
|
|
return cached
|
|
}
|
|
|
|
func loadDailyFile(path string) *dailyCache {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var payload struct {
|
|
Version int `json:"version"`
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
if json.Unmarshal(raw, &payload) != nil || payload.Version != 1 || payload.Data == nil {
|
|
return nil
|
|
}
|
|
c := &dailyCache{}
|
|
b, _ := json.Marshal(payload.Data)
|
|
if json.Unmarshal(b, c) != nil {
|
|
return nil
|
|
}
|
|
return c
|
|
}
|
|
|
|
func fetchSignal(cfg Config, name string) map[string]map[string]any {
|
|
cached := daily(cfg, name, "open_%s.json", func() (any, error) {
|
|
q := url.Values{"host_key": {cfg.HostKey}}
|
|
payload, err := getJSON(cfg.APIHost+"/a/"+name, q, cfg.HTTPTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return normalizeZT(payload), nil
|
|
}, time.Now())
|
|
if cached == nil || !cached.OK {
|
|
return map[string]map[string]any{}
|
|
}
|
|
return asSignalMap(cached.Data)
|
|
}
|
|
|
|
func asSignalMap(data any) map[string]map[string]any {
|
|
out := map[string]map[string]any{}
|
|
switch v := data.(type) {
|
|
case map[string]map[string]any:
|
|
return v
|
|
case map[string]any:
|
|
for code, val := range v {
|
|
if m, ok := val.(map[string]any); ok {
|
|
out[code] = m
|
|
} else {
|
|
out[code] = map[string]any{"code": code}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeZT(payload map[string]any) map[string]map[string]any {
|
|
data, _ := payload["data"]
|
|
out := map[string]map[string]any{}
|
|
switch v := data.(type) {
|
|
case []any:
|
|
for _, item := range v {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
code, _ := m["code"].(string)
|
|
if code != "" {
|
|
out[code] = m
|
|
}
|
|
}
|
|
case map[string]any:
|
|
if code, _ := v["code"].(string); code != "" {
|
|
out[code] = v
|
|
return out
|
|
}
|
|
for code, val := range v {
|
|
if m, ok := val.(map[string]any); ok {
|
|
if _, has := m["code"]; !has {
|
|
m["code"] = code
|
|
}
|
|
out[code] = m
|
|
} else {
|
|
out[code] = map[string]any{"code": code}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func passCodes(cfg Config) []string {
|
|
load := func() (any, error) {
|
|
payload, err := getJSON(cfg.APIHost+"/a/pass_codes", nil, cfg.HTTPTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, _ := payload["data"].([]any)
|
|
if data == nil {
|
|
return nil, fmt.Errorf("接口 data 不是数组")
|
|
}
|
|
codes := make([]string, 0, len(data))
|
|
for _, item := range data {
|
|
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(item)))
|
|
if s != "" && s != "<nil>" {
|
|
codes = append(codes, s)
|
|
}
|
|
}
|
|
return codes, nil
|
|
}
|
|
cached := daily(cfg, "pass_codes", "pass_codes_%s.json", load, time.Now())
|
|
codes := codesFromAny(cached)
|
|
if len(codes) > 0 {
|
|
return codes
|
|
}
|
|
logf("INFO", "pass_codes 为空,重新获取")
|
|
data, err := load()
|
|
if err != nil {
|
|
logf("ERROR", "pass_codes 重新获取失败: %v", err)
|
|
return nil
|
|
}
|
|
list, _ := data.([]string)
|
|
return list
|
|
}
|
|
|
|
func codesFromAny(cached *dailyCache) []string {
|
|
if cached == nil || !cached.OK {
|
|
return nil
|
|
}
|
|
switch v := cached.Data.(type) {
|
|
case []string:
|
|
return v
|
|
case []any:
|
|
out := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(item)))
|
|
if s != "" && s != "<nil>" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func marketAllowOpen(cfg Config) bool {
|
|
payload, err := getJSON(cfg.APIHost+"/a/market", url.Values{"period": {"60m"}}, cfg.HTTPTimeout)
|
|
if err != nil {
|
|
logf("ERROR", "获取60m大盘信号失败: %s %v", cfg.APIHost+"/a/market", err)
|
|
return false
|
|
}
|
|
status := marketStatus(payload)
|
|
logf("INFO", "大盘信号: status=%s", status)
|
|
return status == "UP"
|
|
}
|
|
|
|
func marketStatus(payload map[string]any) string {
|
|
var value any = payload
|
|
if m, ok := value.(map[string]any); ok {
|
|
if d, exists := m["data"]; exists {
|
|
value = d
|
|
}
|
|
}
|
|
if arr, ok := value.([]any); ok {
|
|
if len(arr) == 0 {
|
|
value = nil
|
|
} else {
|
|
value = arr[len(arr)-1]
|
|
}
|
|
}
|
|
if m, ok := value.(map[string]any); ok {
|
|
if v, exists := m["action"]; exists {
|
|
value = v
|
|
} else if v, exists := m["status"]; exists {
|
|
value = v
|
|
} else if v, exists := m["signal"]; exists {
|
|
value = v
|
|
}
|
|
}
|
|
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(value)))
|
|
switch s {
|
|
case "UP", "DOWN", "NEUTRAL":
|
|
return s
|
|
default:
|
|
return "UNKNOWN"
|
|
}
|
|
}
|