package main import ( "context" "math" "sync" "time" "big-qmt/go-client/sdk" ) type dipWatch struct { LastClose float64 ExpiresAt time.Time } var openDip = struct { mu sync.Mutex store map[string]dipWatch }{store: map[string]dipWatch{}} func openSignal(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, assets *sdk.Assets, ticks map[string]sdk.Tick, openSignals map[string]map[string]any, marketOK bool) { if !marketOK { return } if assets == nil { return } if assets.Available < assets.Total*cfg.MinCashRatio { return } state := getState(cfg) if state.LoadError != "" { logf("ERROR", "[ZT][开仓] 状态文件异常,禁止新开仓: %s", state.LoadError) return } for signalCode, signal := range openSignals { code := normalizeCode(signalCode, "") if code == "" { if c, ok := signal["code"].(string); ok { code = normalizeCode(c, "") } } if code == "" { logf("ERROR", "[ZT][开仓] 无效股票代码=%s", signalCode) continue } if state.Get(code) != nil { continue } price := ticks[code].LastPrice if price <= 0 { continue } if !dipTriggered(&openDip.mu, openDip.store, cfg, "开仓", code, price) { continue } volume := calcOpenVolume(price, cfg.OpenMoney) if volume <= 0 { continue } if !books.place(ctx, client, cfg, "buy", code, volume, newOrderTag("base")) { continue } state.Ensure(code).Pending = "base_opening" state.Save() logf("INFO", "[ZT][开仓] %s 买入 %d 股", code, volume) } state.Save() } func calcOpenVolume(price, openMoney float64) int { if price <= 0 || openMoney <= 0 { return 0 } hands := int(math.Floor(openMoney / (price * 100))) if hands == 0 { hands = 1 } return hands * 100 } func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, cfg Config, tag, code string, price float64) bool { if price <= 0 { return false } mu.Lock() defer mu.Unlock() now := time.Now() watch, ok := store[code] if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) { store[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(cfg.WatchTimeout)} logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price) return false } if price < watch.LastClose { watch.LastClose = price watch.ExpiresAt = now.Add(cfg.WatchTimeout) store[code] = watch logf("INFO", "[%s-下跌] %s 刷新低点=%.2f", tag, code, price) return false } rebound := (price - watch.LastClose) / watch.LastClose * 100 if rebound <= 0 { return false } if rebound < cfg.ReboundThreshold { logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, cfg.ReboundThreshold) return false } delete(store, code) logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose) return true }