dev 5
This commit is contained in:
@@ -4,415 +4,151 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"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"`
|
||||
}
|
||||
var (
|
||||
STOCK_DIRECTION = 48
|
||||
STOCK_SIDE_BUY = 48
|
||||
STOCK_SIDE_SELL = 49
|
||||
OffsetFlag = map[string]string{"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
|
||||
OrderTimeout = 5 * time.Minute
|
||||
|
||||
const (
|
||||
opBuyStock = 23
|
||||
opBuyAlt = 48
|
||||
sideBuy = "buy"
|
||||
sideSell = "sell"
|
||||
OrderBook *Books
|
||||
)
|
||||
|
||||
var activeStatuses = map[int]struct{}{
|
||||
48: {}, 49: {}, 50: {}, 51: {}, 52: {}, 55: {},
|
||||
type OrderItem struct {
|
||||
ID string
|
||||
Code string
|
||||
Side string
|
||||
Remark string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
Volume int
|
||||
}
|
||||
|
||||
type parsedOrder struct {
|
||||
OrderID string
|
||||
StockCode string
|
||||
Side string
|
||||
Active bool
|
||||
OrderTime int64
|
||||
RemarkOwned bool
|
||||
VolumeOrig int
|
||||
VolumeLeft int
|
||||
VolumeTraded int
|
||||
Tag string
|
||||
type Books struct {
|
||||
mu sync.Mutex
|
||||
Data map[string]*OrderItem
|
||||
Index []string
|
||||
}
|
||||
|
||||
func (o parsedOrder) cancelVolume() int {
|
||||
n := o.VolumeLeft + o.VolumeTraded
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
return o.VolumeOrig
|
||||
func NewOrderBook() {
|
||||
OrderBook = &Books{Data: make(map[string]*OrderItem), Index: make([]string, 0)}
|
||||
}
|
||||
|
||||
type submission struct {
|
||||
Code string
|
||||
Side string
|
||||
}
|
||||
|
||||
type OrderBook struct {
|
||||
mu sync.Mutex
|
||||
cached []parsedOrder
|
||||
hasCache bool
|
||||
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)
|
||||
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()
|
||||
o.hasCache = false
|
||||
o.cached = nil
|
||||
}
|
||||
|
||||
func (o *OrderBook) query(ctx context.Context, client *sdk.Client) ([]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) 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
|
||||
}
|
||||
buys, sells = map[string]struct{}{}, map[string]struct{}{}
|
||||
for _, item := range orders {
|
||||
if !item.Active || item.StockCode == "" {
|
||||
continue
|
||||
}
|
||||
if item.Side == sideBuy {
|
||||
buys[item.StockCode] = struct{}{}
|
||||
} else {
|
||||
sells[item.StockCode] = struct{}{}
|
||||
}
|
||||
}
|
||||
return buys, sells, true
|
||||
}
|
||||
|
||||
func (o *OrderBook) CancelExpired(ctx context.Context, client *sdk.Client) bool {
|
||||
o.invalidate()
|
||||
orders, err := o.query(ctx, client)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
state := QuantState
|
||||
now := time.Now()
|
||||
timeout := time.Duration(config.Account.OrderTimeoutSec) * time.Second
|
||||
seen := map[string]struct{}{}
|
||||
cancelled := false
|
||||
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)
|
||||
cancelled = true
|
||||
logf("INFO", "[ZT][委托] 撤销超时单 %s %s %s volume=%d", order.OrderID, order.StockCode, order.Side, vol)
|
||||
}
|
||||
if cancelled {
|
||||
o.invalidate()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *OrderBook) claimed(state *State, 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, err := state.Get(order.StockCode)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return order.OrderID == item.BaseOrderId || order.OrderID == item.AddedOrderId || item.BaseStatus == StatusIng || item.AddedStatus == StatusIng
|
||||
}
|
||||
|
||||
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(code, side string, active map[string]struct{}) bool {
|
||||
if _, ok := active[code]; ok {
|
||||
return true
|
||||
}
|
||||
return o.locked(code, side)
|
||||
}
|
||||
|
||||
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) < time.Duration(config.Account.OrderTimeoutSec)*time.Second
|
||||
}
|
||||
|
||||
func (o *OrderBook) locks(side string) map[string]time.Time {
|
||||
if side == sideBuy {
|
||||
return o.buyLocks
|
||||
}
|
||||
return o.sellLocks
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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, 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(code, side) {
|
||||
logf("INFO", "[ZT][委托] %s %s锁定中", code, side)
|
||||
return false
|
||||
}
|
||||
if o.hasActive(ctx, client, code, side) {
|
||||
logf("INFO", "[ZT][委托] %s 已有%s在途委托", code, side)
|
||||
return false
|
||||
}
|
||||
_, 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})
|
||||
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, _ := 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 := item["m_strInsertDate"]
|
||||
clock := strings.ReplaceAll(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 := sideSell
|
||||
if operation == opBuyStock || operation == opBuyAlt {
|
||||
side = sideBuy
|
||||
}
|
||||
left, _ := strconv.Atoi(item["m_nVolumeTotal"])
|
||||
traded, _ := strconv.Atoi(item["m_nVolumeTraded"])
|
||||
orig, _ := strconv.Atoi(item["m_nVolumeTotalOriginal"])
|
||||
_, active := activeStatuses[status]
|
||||
return parsedOrder{
|
||||
OrderID: item["m_strOrderSysID"],
|
||||
StockCode: item["m_strInstrumentID"],
|
||||
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[:])
|
||||
// 订单号同时用于 Windows 回写文件名,因此只使用文件名安全字符。
|
||||
tag := fmt.Sprintf("zt-%s-%s", legCode, hex.EncodeToString(buf[:]))
|
||||
func NewOrderID(leg string) string {
|
||||
var random [6]byte
|
||||
_, _ = rand.Read(random[:])
|
||||
tag := fmt.Sprintf("zt-%s-%s", leg, hex.EncodeToString(random[:]))
|
||||
if len(tag) > 24 {
|
||||
return tag[:24]
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func parseHM(now time.Time) int {
|
||||
n, _ := strconv.Atoi(now.Format("1504"))
|
||||
return n
|
||||
func (o *Books) IsLock(side, code string) bool {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
keyStr := fmt.Sprintf("%s-%s", side, code)
|
||||
return slices.Contains(o.Index, keyStr)
|
||||
}
|
||||
|
||||
func (o *Books) Refresh(client *sdk.Client) error {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
raw, err := client.TradeDetailData(context.Background(), "order")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var idx []string
|
||||
orders := make(map[string]*OrderItem)
|
||||
for _, row := range raw {
|
||||
keyStr, item := parseOrder(row)
|
||||
orders[keyStr] = item
|
||||
idx = append(idx, keyStr)
|
||||
}
|
||||
o.Data = orders
|
||||
o.Index = idx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Books) CancelExpired(client *sdk.Client) error {
|
||||
ctx := context.Background()
|
||||
err := o.Refresh(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[委托] 查询失败: %v", err)
|
||||
}
|
||||
for _, order := range o.Data {
|
||||
if order.CreatedAt.IsZero() || time.Since(order.CreatedAt) <= OrderTimeout {
|
||||
continue
|
||||
}
|
||||
if order.ID != "" {
|
||||
rs, err := client.CanCancelOrder(ctx, order.ID)
|
||||
if err != nil {
|
||||
logf("ERROR", "[委托] 撤销失败:%v", err)
|
||||
continue
|
||||
} else {
|
||||
logf("INFO", "[委托] 撤销成功:%v", rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Books) Place(client *sdk.Client, op int, code string, volume int, sn string) bool {
|
||||
if _, err := client.PassorderLatestTagged(context.Background(), op, code, volume, sn); err != nil {
|
||||
logf("ERROR", "[委托] %s 下单失败: %v", code, err)
|
||||
return false
|
||||
}
|
||||
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
keyStr := fmt.Sprintf("%s-%s", OffsetFlag[strconv.Itoa(op)], code)
|
||||
o.Index = append(o.Index, keyStr)
|
||||
|
||||
logf("INFO", "[委托] 下单已提交 %d %s %d股", op, code, volume)
|
||||
return true
|
||||
}
|
||||
|
||||
func parseOrder(row map[string]string) (string, *OrderItem) {
|
||||
left, _ := strconv.Atoi(row["m_nVolumeTotal"])
|
||||
traded, _ := strconv.Atoi(row["m_nVolumeTraded"])
|
||||
volume := left + traded
|
||||
|
||||
item := &OrderItem{
|
||||
ID: row["m_strOrderSysID"],
|
||||
Code: row["m_strInstrumentID"],
|
||||
Side: OffsetFlag[row["m_nOffsetFlag"]],
|
||||
Remark: row["m_strRemark"],
|
||||
Status: row["m_nOrderStatus"],
|
||||
Volume: volume,
|
||||
CreatedAt: time.Unix(parseTimestamp(row), 0),
|
||||
}
|
||||
keyStr := fmt.Sprintf("%s-%s", item.Side, item.Code)
|
||||
return keyStr, item
|
||||
}
|
||||
|
||||
func parseTimestamp(row map[string]string) int64 {
|
||||
ts, _ := strconv.ParseInt(row["m_nOrderTime"], 10, 64)
|
||||
if ts > 1e11 {
|
||||
return ts / 1000
|
||||
}
|
||||
if ts > 0 {
|
||||
return ts
|
||||
}
|
||||
date := row["m_strInsertDate"]
|
||||
clock := strings.ReplaceAll(row["m_strInsertTime"], ":", "")
|
||||
clock = strings.Repeat("0", max(0, 6-len(clock))) + clock
|
||||
t, _ := time.ParseInLocation("20060102150405", date+clock, time.Local)
|
||||
return t.Unix()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user