375 lines
8.9 KiB
Go
375 lines
8.9 KiB
Go
package logic
|
|
|
|
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, "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)
|
|
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)
|
|
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)
|
|
}
|