package logic import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" "sync" "time" "big-qmt/go-client/config" "big-qmt/go-client/sdk" ) type orderReceipt struct { OrderID string `json:"order_id"` QMTOrderID string `json:"qmt_order_id"` StockCode string `json:"stock_code"` Side string `json:"side"` Status string `json:"status"` RequestedVolume int `json:"requested_volume"` TradedVolume int `json:"traded_volume"` } const ( opBuyStock = 23 opBuyAlt = 48 sideBuy = "buy" sideSell = "sell" ) 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 } type OrderBook struct { mu sync.Mutex cached []parsedOrder hasCache bool buyLocks map[string]time.Time sellLocks map[string]time.Time subs []submission receipts map[string]time.Time } func NewOrderBook() *OrderBook { return &OrderBook{ buyLocks: map[string]time.Time{}, sellLocks: map[string]time.Time{}, receipts: map[string]time.Time{}, } } // readReceipts 读取 QMT 回写并同步委托状态。 func (o *OrderBook) readReceipts() { paths, _ := filepath.Glob(filepath.Join(config.Global.QMTDataDir, "order_*.json")) for _, path := range paths { info, err := os.Stat(path) if err != nil { continue } o.mu.Lock() last := o.receipts[path] o.mu.Unlock() if !info.ModTime().After(last) { continue } receipt, err := loadReceipt(path) if err != nil || !strings.HasPrefix(receipt.OrderID, "zt-") || receipt.StockCode == "" || receipt.Status == "" { continue } o.mu.Lock() o.receipts[path] = info.ModTime() o.mu.Unlock() status := strings.ToLower(receipt.Status) if (status == "filled" || status == "cancelled" || status == "rejected") && (receipt.Side == sideBuy || receipt.Side == sideSell) { o.unlockSide(receipt.StockCode, receipt.Side) o.invalidate() } logf("INFO", "[ZT][回写] %s status=%s traded=%d/%d", receipt.OrderID, status, receipt.TradedVolume, receipt.RequestedVolume) } } func loadReceipt(path string) (*orderReceipt, error) { raw, err := os.ReadFile(path) if err != nil { return nil, err } var receipt orderReceipt err = json.Unmarshal(raw, &receipt) return &receipt, err } 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) ([]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) activeSets(ctx context.Context, client *sdk.Client) (buys, sells map[string]struct{}, ok bool) { orders, err := o.query(ctx, client) 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 == sideBuy { buys[item.StockCode] = struct{}{} } else { sells[item.StockCode] = struct{}{} } } return buys, sells, true } func (o *OrderBook) CancelExpired(ctx context.Context, client *sdk.Client) bool { o.invalidate() orders, err := o.query(ctx, client) if err != nil { return false } state := QuantState now := time.Now() timeout := time.Duration(config.Account.OrderTimeoutSec) * time.Second seen := map[string]struct{}{} cancelled := false 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) cancelled = true logf("INFO", "[ZT][委托] 撤销超时单 %s %s %s volume=%d", order.OrderID, order.StockCode, order.Side, vol) } if cancelled { o.invalidate() } return true } func (o *OrderBook) claimed(state *State, 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, err := state.Get(order.StockCode) if err != nil { return false } return order.OrderID == item.BaseOrderId || order.OrderID == item.AddedOrderId || item.BaseStatus == StatusIng || item.AddedStatus == StatusIng } 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(code, side string, active map[string]struct{}) bool { if _, ok := active[code]; ok { return true } return o.locked(code, side) } func (o *OrderBook) locked(code, side string) bool { o.mu.Lock() defer o.mu.Unlock() ts, ok := o.locks(side)[code] return ok && time.Since(ts) < time.Duration(config.Account.OrderTimeoutSec)*time.Second } func (o *OrderBook) locks(side string) map[string]time.Time { if side == sideBuy { return o.buyLocks } return o.sellLocks } func (o *OrderBook) hasActive(ctx context.Context, client *sdk.Client, code, side string) bool { orders, err := o.query(ctx, client) 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, 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(code, side) { logf("INFO", "[ZT][委托] %s %s锁定中", code, side) return false } if o.hasActive(ctx, client, code, side) { logf("INFO", "[ZT][委托] %s 已有%s在途委托", code, side) return false } _, err := client.PassorderLatestTagged(ctx, side == sideBuy, code, volume, tag) 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}) 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, _ := strconv.Atoi(item["m_nOffsetFlag"]) status, _ := strconv.Atoi(item["m_nOrderStatus"]) tag := item["m_strRemark"] orderTime, _ := strconv.ParseInt(item["m_nOrderTime"], 10, 64) if orderTime > 1e11 { orderTime /= 1000 } if orderTime <= 0 { date := item["m_strInsertDate"] clock := strings.ReplaceAll(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 := sideSell if operation == opBuyStock || operation == opBuyAlt { side = sideBuy } left, _ := strconv.Atoi(item["m_nVolumeTotal"]) traded, _ := strconv.Atoi(item["m_nVolumeTraded"]) orig, _ := strconv.Atoi(item["m_nVolumeTotalOriginal"]) _, active := activeStatuses[status] return parsedOrder{ OrderID: item["m_strOrderSysID"], StockCode: item["m_strInstrumentID"], 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[:]) // 订单号同时用于 Windows 回写文件名,因此只使用文件名安全字符。 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 }