This commit is contained in:
2026-08-25 16:40:18 +08:00
parent 4514646c6a
commit fa62436a73
28 changed files with 6401 additions and 0 deletions

374
go-client/apps/zt/order.go Normal file
View File

@@ -0,0 +1,374 @@
package main
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, cfg.AccountType, "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, cfg.AccountType)
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, cfg.AccountType)
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)
}