dev 1
This commit is contained in:
374
go-client/apps/zt/order.go
Normal file
374
go-client/apps/zt/order.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user