This commit is contained in:
2026-08-25 22:35:44 +08:00
parent ec58641d09
commit f14423418a
23 changed files with 738 additions and 680 deletions

View File

@@ -4,18 +4,34 @@ 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{}{
@@ -44,11 +60,8 @@ func (o parsedOrder) cancelVolume() int {
}
type submission struct {
Code string
Side string
Volume int
At time.Time
Tag string
Code string
Side string
}
type OrderBook struct {
@@ -58,15 +71,63 @@ type OrderBook struct {
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)
state := getState()
if item := state.Get(receipt.StockCode); item != nil && item.PendingOrderID == receipt.OrderID {
item.OrderStatus = status
state.Save()
}
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()
@@ -74,7 +135,7 @@ func (o *OrderBook) invalidate() {
o.cached = nil
}
func (o *OrderBook) query(ctx context.Context, client *sdk.Client, cfg Config) ([]parsedOrder, error) {
func (o *OrderBook) query(ctx context.Context, client *sdk.Client) ([]parsedOrder, error) {
o.mu.Lock()
if o.hasCache {
out := append([]parsedOrder(nil), o.cached...)
@@ -98,14 +159,8 @@ func (o *OrderBook) query(ctx context.Context, client *sdk.Client, cfg Config) (
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)
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
}
@@ -114,7 +169,7 @@ func (o *OrderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Conf
if !item.Active || item.StockCode == "" {
continue
}
if item.Side == "buy" {
if item.Side == sideBuy {
buys[item.StockCode] = struct{}{}
} else {
sells[item.StockCode] = struct{}{}
@@ -123,16 +178,17 @@ func (o *OrderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Conf
return buys, sells, true
}
func (o *OrderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg Config) bool {
func (o *OrderBook) cancelExpired(ctx context.Context, client *sdk.Client) bool {
o.invalidate()
orders, err := o.query(ctx, client, cfg)
orders, err := o.query(ctx, client)
if err != nil {
return false
}
state := getState(cfg)
state := getState()
now := time.Now()
timeout := cfg.OrderTimeout
timeout := time.Duration(config.Account.OrderTimeoutSec) * time.Second
seen := map[string]struct{}{}
cancelled := false
for _, order := range orders {
if !order.Active || order.StockCode == "" {
continue
@@ -178,8 +234,12 @@ func (o *OrderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg C
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
}
@@ -203,10 +263,10 @@ func (o *OrderBook) claimed(state *ZTState, order parsedOrder) bool {
return false
}
switch item.Pending {
case "base_opening", "add":
return order.Side == "buy"
case "sell_add", "sell_base":
return order.Side == "sell"
case pendingBaseOpening, pendingAdd:
return order.Side == sideBuy
case pendingSellAdd, pendingSellBase:
return order.Side == sideSell
default:
return false
}
@@ -227,29 +287,29 @@ func (o *OrderBook) unlockSide(code, side string) {
o.subs = o.subs[:n]
}
func (o *OrderBook) sideBusy(cfg Config, code, side string, active map[string]struct{}) bool {
func (o *OrderBook) sideBusy(code, side string, active map[string]struct{}) bool {
if _, ok := active[code]; ok {
return true
}
return o.locked(cfg, code, side)
return o.locked(code, side)
}
func (o *OrderBook) locked(cfg Config, code, side string) bool {
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) < cfg.OrderTimeout
return ok && time.Since(ts) < time.Duration(config.Account.OrderTimeoutSec)*time.Second
}
func (o *OrderBook) locks(side string) map[string]time.Time {
if side == "buy" {
if side == sideBuy {
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)
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
}
@@ -261,43 +321,43 @@ func (o *OrderBook) hasActive(ctx context.Context, client *sdk.Client, cfg Confi
return false
}
func (o *OrderBook) place(ctx context.Context, client *sdk.Client, cfg Config, side, code string, volume int, tag string) bool {
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(cfg, code, side) {
if o.locked(code, side) {
logf("INFO", "[ZT][委托] %s %s锁定中", code, side)
return false
}
if o.hasActive(ctx, client, cfg, code, side) {
if o.hasActive(ctx, client, code, side) {
logf("INFO", "[ZT][委托] %s 已有%s在途委托", code, side)
return false
}
_, err := client.PassorderLatest(ctx, side == "buy", code, volume)
_, 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, Volume: volume, At: time.Now(), Tag: tag})
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 := 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")))
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 := mapGet(item, "m_strInsertDate")
clock := strings.ReplaceAll(mapGet(item, "m_strInsertTime"), ":", "")
date := item["m_strInsertDate"]
clock := strings.ReplaceAll(item["m_strInsertTime"], ":", "")
if date != "" {
if len(clock) < 6 {
clock = strings.Repeat("0", 6-len(clock)) + clock
@@ -307,21 +367,21 @@ func parseOrder(item map[string]string) parsedOrder {
}
}
}
side := "sell"
side := sideSell
if operation == opBuyStock || operation == opBuyAlt {
side = "buy"
side = sideBuy
}
left := asIntS(mapGet(item, "m_nVolumeTotal", "volume_left"))
traded := asIntS(mapGet(item, "m_nVolumeTraded", "volume_traded"))
orig := asIntS(mapGet(item, "m_nVolumeTotalOriginal", "volume"))
left, _ := strconv.Atoi(item["m_nVolumeTotal"])
traded, _ := strconv.Atoi(item["m_nVolumeTraded"])
orig, _ := strconv.Atoi(item["m_nVolumeTotalOriginal"])
_, active := activeStatuses[status]
return parsedOrder{
OrderID: mapGet(item, "m_strOrderSysID", "m_nOrderID", "order_id"),
StockCode: stockCodeFromMap(item),
OrderID: item["m_strOrderSysID"],
StockCode: item["m_strInstrumentID"],
Side: side,
Active: active,
OrderTime: orderTime,
RemarkOwned: strings.HasPrefix(tag, "zt:"),
RemarkOwned: strings.HasPrefix(tag, "zt-"),
VolumeOrig: orig,
VolumeLeft: left,
VolumeTraded: traded,
@@ -356,7 +416,8 @@ func newOrderTag(leg string) string {
}
var buf [6]byte
_, _ = rand.Read(buf[:])
tag := fmt.Sprintf("zt:%s:%s", legCode, hex.EncodeToString(buf[:]))
// 订单号同时用于 Windows 回写文件名,因此只使用文件名安全字符。
tag := fmt.Sprintf("zt-%s-%s", legCode, hex.EncodeToString(buf[:]))
if len(tag) > 24 {
return tag[:24]
}
@@ -367,8 +428,3 @@ 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)
}