dev 3
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user