package logic import ( "context" "crypto/rand" "encoding/hex" "fmt" "strconv" "strings" "sync" "time" "big-qmt/go-client/sdk" ) const ( opBuyStock = 23 opBuyAlt = 48 ) var activeStatuses = map[int]struct{}{ 48: {}, 49: {}, 50: {}, 51: {}, 52: {}, 55: {}, } type parsedOrder struct { OrderID string StockCode string Side string Active bool OrderTime int64 RemarkOwned bool VolumeOrig int VolumeLeft int VolumeTraded int Tag string } func (o parsedOrder) cancelVolume() int { n := o.VolumeLeft + o.VolumeTraded if n > 0 { return n } return o.VolumeOrig } type submission struct { Code string Side string Volume int At time.Time Tag string } type OrderBook struct { mu sync.Mutex cached []parsedOrder hasCache bool buyLocks map[string]time.Time sellLocks map[string]time.Time subs []submission } func NewOrderBook() *OrderBook { return &OrderBook{ buyLocks: map[string]time.Time{}, sellLocks: map[string]time.Time{}, } } func (o *OrderBook) invalidate() { o.mu.Lock() defer o.mu.Unlock() o.hasCache = false o.cached = nil } func (o *OrderBook) query(ctx context.Context, client *sdk.Client, cfg Config) ([]parsedOrder, error) { o.mu.Lock() if o.hasCache { out := append([]parsedOrder(nil), o.cached...) o.mu.Unlock() return out, nil } o.mu.Unlock() raw, err := client.TradeDetailData(ctx, "order") if err != nil { logf("ERROR", "[ZT][委托] 查询失败: %v", err) return nil, err } orders := make([]parsedOrder, 0, len(raw)) for _, item := range raw { orders = append(orders, parseOrder(item)) } o.mu.Lock() o.cached = orders o.hasCache = true o.mu.Unlock() return orders, nil } func (o *OrderBook) refresh(ctx context.Context, client *sdk.Client, cfg Config) bool { o.invalidate() _, err := o.query(ctx, client, cfg) return err == nil } func (o *OrderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Config) (buys, sells map[string]struct{}, ok bool) { orders, err := o.query(ctx, client, cfg) if err != nil { return nil, nil, false } buys, sells = map[string]struct{}{}, map[string]struct{}{} for _, item := range orders { if !item.Active || item.StockCode == "" { continue } if item.Side == "buy" { buys[item.StockCode] = struct{}{} } else { sells[item.StockCode] = struct{}{} } } return buys, sells, true } func (o *OrderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg Config) bool { o.invalidate() orders, err := o.query(ctx, client, cfg) if err != nil { return false } state := getState(cfg) now := time.Now() timeout := cfg.OrderTimeout seen := map[string]struct{}{} for _, order := range orders { if !order.Active || order.StockCode == "" { continue } if !o.claimed(state, order) { continue } if order.OrderTime <= 0 || now.Sub(time.Unix(order.OrderTime, 0)) <= timeout { continue } vol := order.cancelVolume() if vol <= 0 { logf("WARNING", "[ZT][委托] 超时单缺少数量,跳过 %s %s", order.OrderID, order.StockCode) continue } key := order.StockCode + "|" + strconv.Itoa(vol) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} if order.OrderID != "" { can, err := client.CanCancelOrder(ctx, order.OrderID) if err != nil { logf("ERROR", "[ZT][委托] 查询是否可撤失败 %s: %v", order.OrderID, err) continue } if !truthy(can) { logf("INFO", "[ZT][委托] 不可撤 %s %s", order.OrderID, order.StockCode) continue } } ret, err := client.CancelByRule(ctx, order.StockCode, vol) if err != nil { logf("ERROR", "[ZT][委托] 撤单失败 %s %s: %v", order.OrderID, order.StockCode, err) continue } if ret == nil || ret.Status != "success" { msg := "" if ret != nil { msg = ret.Message } logf("WARNING", "[ZT][委托] 规则撤单未命中 %s %s volume=%d %s", order.OrderID, order.StockCode, vol, msg) continue } o.unlockSide(order.StockCode, order.Side) logf("INFO", "[ZT][委托] 撤销超时单 %s %s %s volume=%d", order.OrderID, order.StockCode, order.Side, vol) } return true } func (o *OrderBook) claimed(state *ZTState, order parsedOrder) bool { if order.RemarkOwned { return true } o.mu.Lock() for _, s := range o.subs { if s.Code == order.StockCode && s.Side == order.Side { o.mu.Unlock() return true } } o.mu.Unlock() if state == nil { return false } item := state.Get(order.StockCode) if item == nil || item.Pending == "" { return false } switch item.Pending { case "base_opening", "add": return order.Side == "buy" case "sell_add", "sell_base": return order.Side == "sell" default: return false } } func (o *OrderBook) unlockSide(code, side string) { o.mu.Lock() defer o.mu.Unlock() delete(o.locks(side), code) n := 0 for _, s := range o.subs { if s.Code == code && s.Side == side { continue } o.subs[n] = s n++ } o.subs = o.subs[:n] } func (o *OrderBook) sideBusy(cfg Config, code, side string, active map[string]struct{}) bool { if _, ok := active[code]; ok { return true } return o.locked(cfg, code, side) } func (o *OrderBook) locked(cfg Config, code, side string) bool { o.mu.Lock() defer o.mu.Unlock() ts, ok := o.locks(side)[code] return ok && time.Since(ts) < cfg.OrderTimeout } func (o *OrderBook) locks(side string) map[string]time.Time { if side == "buy" { return o.buyLocks } return o.sellLocks } func (o *OrderBook) hasActive(ctx context.Context, client *sdk.Client, cfg Config, code, side string) bool { orders, err := o.query(ctx, client, cfg) if err != nil { return true } for _, item := range orders { if item.StockCode == code && item.Active && item.Side == side { return true } } return false } func (o *OrderBook) place(ctx context.Context, client *sdk.Client, cfg Config, side, code string, volume int, tag string) bool { if volume <= 0 || volume%100 != 0 { logf("ERROR", "[ZT][委托] %s 拒绝非整手数量=%d", code, volume) return false } if o.locked(cfg, code, side) { logf("INFO", "[ZT][委托] %s %s锁定中", code, side) return false } if o.hasActive(ctx, client, cfg, code, side) { logf("INFO", "[ZT][委托] %s 已有%s在途委托", code, side) return false } _, err := client.PassorderLatest(ctx, side == "buy", code, volume) if err != nil { logf("ERROR", "[ZT][委托] %s 异常: %v", code, err) return false } o.mu.Lock() o.locks(side)[code] = time.Now() o.subs = append(o.subs, submission{Code: code, Side: side, Volume: volume, At: time.Now(), Tag: tag}) o.mu.Unlock() logf("INFO", "[ZT][委托] 已提交 %s %s %d股 tag=%s", side, code, volume, tag) return true } func parseOrder(item map[string]string) parsedOrder { operation := asIntS(mapGet(item, "m_nOffsetFlag", "m_nOrderType", "order_type")) status := asIntS(mapGet(item, "m_nOrderStatus", "order_status", "status")) tag := mapGet(item, "m_strRemark", "m_strUserOrderId", "order_remark") orderTime := int64(asIntS(mapGet(item, "m_nOrderTime", "order_time"))) if orderTime > 1e11 { orderTime /= 1000 } if orderTime <= 0 { date := mapGet(item, "m_strInsertDate") clock := strings.ReplaceAll(mapGet(item, "m_strInsertTime"), ":", "") if date != "" { if len(clock) < 6 { clock = strings.Repeat("0", 6-len(clock)) + clock } if t, err := time.ParseInLocation("20060102150405", date+clock, time.Local); err == nil { orderTime = t.Unix() } } } side := "sell" if operation == opBuyStock || operation == opBuyAlt { side = "buy" } left := asIntS(mapGet(item, "m_nVolumeTotal", "volume_left")) traded := asIntS(mapGet(item, "m_nVolumeTraded", "volume_traded")) orig := asIntS(mapGet(item, "m_nVolumeTotalOriginal", "volume")) _, active := activeStatuses[status] return parsedOrder{ OrderID: mapGet(item, "m_strOrderSysID", "m_nOrderID", "order_id"), StockCode: stockCodeFromMap(item), Side: side, Active: active, OrderTime: orderTime, RemarkOwned: strings.HasPrefix(tag, "zt:"), VolumeOrig: orig, VolumeLeft: left, VolumeTraded: traded, Tag: tag, } } func truthy(v any) bool { if v == nil { return false } switch x := v.(type) { case bool: return x case string: s := strings.ToLower(strings.TrimSpace(x)) return s == "true" || s == "1" || s == "yes" case float64: return x != 0 case int: return x != 0 default: s := strings.ToLower(strings.TrimSpace(fmt.Sprint(v))) return s == "true" || s == "1" } } func newOrderTag(leg string) string { legCode := map[string]string{"base": "b", "add": "a", "take_profit": "t", "all": "s"}[leg] if legCode == "" { legCode = "x" } var buf [6]byte _, _ = rand.Read(buf[:]) tag := fmt.Sprintf("zt:%s:%s", legCode, hex.EncodeToString(buf[:])) if len(tag) > 24 { return tag[:24] } return tag } func parseHM(now time.Time) int { n, _ := strconv.Atoi(now.Format("1504")) return n } func tradingTime(now time.Time) bool { hm := parseHM(now) return (hm >= 930 && hm <= 1130) || (hm >= 1300 && hm <= 1500) }