package logic import ( "context" "math" "sync" "time" "big-qmt/go-client/config" "big-qmt/go-client/libs" "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, ticks map[string]sdk.Tick, openSignals map[string]libs.SignalItem, marketOK bool, buyBudget *float64) { if !marketOK { return } if buyBudget == nil || *buyBudget <= 0 { return } state := getState() if state.LoadError != "" { logf("ERROR", "[ZT][开仓] 状态文件异常,禁止新开仓: %s", state.LoadError) return } for code := range openSignals { if code == "" { logf("ERROR", "[ZT][开仓] 无效股票代码") continue } if state.Get(code) != nil { continue } price := ticks[code].LastPrice if price <= 0 { continue } if !dipTriggered(&openDip.mu, openDip.store, "开仓", code, price) { continue } volume := calcBuyVolume(price, config.Account.BuyValue) if volume <= 0 { continue } estimated := price * float64(volume) if estimated > *buyBudget { logf("INFO", "[ZT][开仓] %s 可用买入预算不足,需要=%.2f 剩余=%.2f", code, estimated, *buyBudget) continue } orderID := newOrderTag("base") if !books.place(ctx, client, sideBuy, code, volume, orderID) { continue } setPending(state.Ensure(code), pendingBaseOpening, orderID) *buyBudget -= estimated state.Save() logf("INFO", "[ZT][开仓] %s 买入 %d 股", code, volume) } } func calcBuyVolume(price, buyValue float64) int { if price <= 0 || buyValue <= 0 { return 0 } // 不足一手时仍按最低一手委托。 hands := int(math.Floor(buyValue / (price * 100))) if hands == 0 { hands = 1 } return hands * 100 } func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, 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(time.Duration(config.Account.WatchTimeoutSec) * time.Second)} logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price) return false } if price < watch.LastClose { watch.LastClose = price watch.ExpiresAt = now.Add(time.Duration(config.Account.WatchTimeoutSec) * time.Second) 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 < config.Account.ReboundThreshold { logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, config.Account.ReboundThreshold) return false } delete(store, code) logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose) return true }