This commit is contained in:
2026-08-26 16:37:06 +08:00
parent 566a07fea8
commit c30c1541d2
9 changed files with 331 additions and 638 deletions

View File

@@ -4,415 +4,151 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"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"`
}
var (
STOCK_DIRECTION = 48
STOCK_SIDE_BUY = 48
STOCK_SIDE_SELL = 49
OffsetFlag = map[string]string{"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
OrderTimeout = 5 * time.Minute
const (
opBuyStock = 23
opBuyAlt = 48
sideBuy = "buy"
sideSell = "sell"
OrderBook *Books
)
var activeStatuses = map[int]struct{}{
48: {}, 49: {}, 50: {}, 51: {}, 52: {}, 55: {},
type OrderItem struct {
ID string
Code string
Side string
Remark string
Status string
CreatedAt time.Time
Volume int
}
type parsedOrder struct {
OrderID string
StockCode string
Side string
Active bool
OrderTime int64
RemarkOwned bool
VolumeOrig int
VolumeLeft int
VolumeTraded int
Tag string
type Books struct {
mu sync.Mutex
Data map[string]*OrderItem
Index []string
}
func (o parsedOrder) cancelVolume() int {
n := o.VolumeLeft + o.VolumeTraded
if n > 0 {
return n
}
return o.VolumeOrig
func NewOrderBook() {
OrderBook = &Books{Data: make(map[string]*OrderItem), Index: make([]string, 0)}
}
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[:]))
func NewOrderID(leg string) string {
var random [6]byte
_, _ = rand.Read(random[:])
tag := fmt.Sprintf("zt-%s-%s", leg, hex.EncodeToString(random[:]))
if len(tag) > 24 {
return tag[:24]
}
return tag
}
func parseHM(now time.Time) int {
n, _ := strconv.Atoi(now.Format("1504"))
return n
func (o *Books) IsLock(side, code string) bool {
o.mu.Lock()
defer o.mu.Unlock()
keyStr := fmt.Sprintf("%s-%s", side, code)
return slices.Contains(o.Index, keyStr)
}
func (o *Books) Refresh(client *sdk.Client) error {
o.mu.Lock()
defer o.mu.Unlock()
raw, err := client.TradeDetailData(context.Background(), "order")
if err != nil {
return err
}
var idx []string
orders := make(map[string]*OrderItem)
for _, row := range raw {
keyStr, item := parseOrder(row)
orders[keyStr] = item
idx = append(idx, keyStr)
}
o.Data = orders
o.Index = idx
return nil
}
func (o *Books) CancelExpired(client *sdk.Client) error {
ctx := context.Background()
err := o.Refresh(client)
if err != nil {
return fmt.Errorf("[委托] 查询失败: %v", err)
}
for _, order := range o.Data {
if order.CreatedAt.IsZero() || time.Since(order.CreatedAt) <= OrderTimeout {
continue
}
if order.ID != "" {
rs, err := client.CanCancelOrder(ctx, order.ID)
if err != nil {
logf("ERROR", "[委托] 撤销失败:%v", err)
continue
} else {
logf("INFO", "[委托] 撤销成功:%v", rs)
}
}
}
return nil
}
func (o *Books) Place(client *sdk.Client, op int, code string, volume int, sn string) bool {
if _, err := client.PassorderLatestTagged(context.Background(), op, code, volume, sn); err != nil {
logf("ERROR", "[委托] %s 下单失败: %v", code, err)
return false
}
o.mu.Lock()
defer o.mu.Unlock()
keyStr := fmt.Sprintf("%s-%s", OffsetFlag[strconv.Itoa(op)], code)
o.Index = append(o.Index, keyStr)
logf("INFO", "[委托] 下单已提交 %d %s %d股", op, code, volume)
return true
}
func parseOrder(row map[string]string) (string, *OrderItem) {
left, _ := strconv.Atoi(row["m_nVolumeTotal"])
traded, _ := strconv.Atoi(row["m_nVolumeTraded"])
volume := left + traded
item := &OrderItem{
ID: row["m_strOrderSysID"],
Code: row["m_strInstrumentID"],
Side: OffsetFlag[row["m_nOffsetFlag"]],
Remark: row["m_strRemark"],
Status: row["m_nOrderStatus"],
Volume: volume,
CreatedAt: time.Unix(parseTimestamp(row), 0),
}
keyStr := fmt.Sprintf("%s-%s", item.Side, item.Code)
return keyStr, item
}
func parseTimestamp(row map[string]string) int64 {
ts, _ := strconv.ParseInt(row["m_nOrderTime"], 10, 64)
if ts > 1e11 {
return ts / 1000
}
if ts > 0 {
return ts
}
date := row["m_strInsertDate"]
clock := strings.ReplaceAll(row["m_strInsertTime"], ":", "")
clock = strings.Repeat("0", max(0, 6-len(clock))) + clock
t, _ := time.ParseInLocation("20060102150405", date+clock, time.Local)
return t.Unix()
}