Files
big-qmt/go-client/apps/zt/remote.go
2026-08-25 16:40:18 +08:00

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"
}
}