dev 1
This commit is contained in:
278
go-client/apps/zt/remote.go
Normal file
278
go-client/apps/zt/remote.go
Normal file
@@ -0,0 +1,278 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user