From d09f27156922c517e433d0d38ad2d71c4d0abe84 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Fri, 28 Aug 2026 18:52:27 +0800 Subject: [PATCH] feat --- __pycache__/main.cpython-311.pyc | Bin 0 -> 9052 bytes go-client/apps/cmd/main.go | 125 ------ go-client/apps/trend/logic/boot.go | 111 ------ go-client/apps/trend/logic/open.go | 90 ----- go-client/apps/trend/logic/order.go | 154 -------- go-client/apps/trend/logic/positions.go | 132 ------- go-client/apps/trend/logic/state.go | 152 ------- go-client/apps/trend/logic/watch.go | 66 ---- go-client/apps/trend/main.go | 94 ----- go-client/config/config.go | 104 ----- go-client/etc/dev.yaml | 11 - go-client/etc/global.yaml | 27 -- go-client/go.mod | 15 - go-client/go.sum | 15 - go-client/libs/calc.go | 38 -- go-client/libs/const.go | 8 - go-client/libs/http.go | 33 -- go-client/libs/market.go | 67 ---- go-client/libs/signal.go | 57 --- go-client/sdk/account.go | 179 --------- go-client/sdk/check.go | 30 -- go-client/sdk/client.go | 141 ------- go-client/sdk/coerce.go | 63 --- go-client/sdk/context.go | 78 ---- go-client/sdk/data.go | 374 ------------------ go-client/sdk/doc.go | 4 - go-client/sdk/error.go | 38 -- go-client/sdk/ext.go | 27 -- go-client/sdk/sys.go | 30 -- go-client/sdk/trade.go | 252 ------------ py-client/__pycache__/main.cpython-311.pyc | Bin 0 -> 6151 bytes py-client/__pycache__/test.cpython-311.pyc | Bin 0 -> 2584 bytes py-client/config/__init__.py | 146 +++++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 7650 bytes py-client/etc/_global.yaml | 11 + py-client/etc/dev.yaml | 11 + py-client/libs/__init__.py | 5 + .../libs/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 522 bytes .../libs/__pycache__/calc.cpython-311.pyc | Bin 0 -> 1850 bytes .../grid_take_profit.cpython-311.pyc | Bin 0 -> 5131 bytes .../libs/__pycache__/http.cpython-311.pyc | Bin 0 -> 884 bytes .../libs/__pycache__/market.cpython-311.pyc | Bin 0 -> 2130 bytes .../libs/__pycache__/signal.cpython-311.pyc | Bin 0 -> 3346 bytes py-client/libs/calc.py | 32 ++ py-client/libs/grid_take_profit.py | 80 ++++ py-client/libs/http.py | 8 + py-client/libs/market.py | 24 ++ py-client/libs/signal.py | 29 ++ py-client/main.py | 111 ++++++ py-client/sdk/__init__.py | 14 + .../sdk/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 924 bytes .../sdk/__pycache__/account.cpython-311.pyc | Bin 0 -> 5343 bytes .../sdk/__pycache__/client.cpython-311.pyc | Bin 0 -> 5675 bytes .../sdk/__pycache__/data.cpython-311.pyc | Bin 0 -> 17201 bytes .../sdk/__pycache__/errors.cpython-311.pyc | Bin 0 -> 1374 bytes .../sdk/__pycache__/misc.cpython-311.pyc | Bin 0 -> 6698 bytes .../sdk/__pycache__/models.cpython-311.pyc | Bin 0 -> 8956 bytes .../sdk/__pycache__/trade.cpython-311.pyc | Bin 0 -> 10098 bytes py-client/sdk/account.py | 33 ++ py-client/sdk/client.py | 59 +++ py-client/sdk/data.py | 83 ++++ py-client/sdk/errors.py | 14 + py-client/sdk/misc.py | 31 ++ py-client/sdk/models.py | 106 +++++ py-client/sdk/trade.py | 54 +++ py-client/strategy/__init__.py | 5 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 264 bytes .../strategy/__pycache__/boot.cpython-311.pyc | Bin 0 -> 2852 bytes py-client/strategy/trend/__init__.py | 5 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 538 bytes .../trend/__pycache__/boot.cpython-311.pyc | Bin 0 -> 9042 bytes .../trend/__pycache__/open.cpython-311.pyc | Bin 0 -> 4982 bytes .../trend/__pycache__/order.cpython-311.pyc | Bin 0 -> 9829 bytes .../__pycache__/positions.cpython-311.pyc | Bin 0 -> 7782 bytes .../trend/__pycache__/run.cpython-311.pyc | Bin 0 -> 1084 bytes .../trend/__pycache__/runtime.cpython-311.pyc | Bin 0 -> 2057 bytes .../trend/__pycache__/state.cpython-311.pyc | Bin 0 -> 8725 bytes .../__pycache__/strategy.cpython-311.pyc | Bin 0 -> 6449 bytes .../trend/__pycache__/watch.cpython-311.pyc | Bin 0 -> 3219 bytes py-client/strategy/trend/boot.py | 176 +++++++++ py-client/strategy/trend/open.py | 105 +++++ py-client/strategy/trend/order.py | 148 +++++++ py-client/strategy/trend/positions.py | 167 ++++++++ py-client/strategy/trend/runtime.py | 43 ++ py-client/strategy/trend/state.py | 146 +++++++ py-client/strategy/trend/watch.py | 34 ++ py-client/test.py | 25 ++ run.bat | 7 + setup.bat | 4 + 89 files changed, 1716 insertions(+), 2515 deletions(-) create mode 100644 __pycache__/main.cpython-311.pyc delete mode 100644 go-client/apps/cmd/main.go delete mode 100644 go-client/apps/trend/logic/boot.go delete mode 100644 go-client/apps/trend/logic/open.go delete mode 100644 go-client/apps/trend/logic/order.go delete mode 100644 go-client/apps/trend/logic/positions.go delete mode 100644 go-client/apps/trend/logic/state.go delete mode 100644 go-client/apps/trend/logic/watch.go delete mode 100644 go-client/apps/trend/main.go delete mode 100644 go-client/config/config.go delete mode 100644 go-client/etc/dev.yaml delete mode 100644 go-client/etc/global.yaml delete mode 100644 go-client/go.mod delete mode 100644 go-client/go.sum delete mode 100644 go-client/libs/calc.go delete mode 100644 go-client/libs/const.go delete mode 100644 go-client/libs/http.go delete mode 100644 go-client/libs/market.go delete mode 100644 go-client/libs/signal.go delete mode 100644 go-client/sdk/account.go delete mode 100644 go-client/sdk/check.go delete mode 100644 go-client/sdk/client.go delete mode 100644 go-client/sdk/coerce.go delete mode 100644 go-client/sdk/context.go delete mode 100644 go-client/sdk/data.go delete mode 100644 go-client/sdk/doc.go delete mode 100644 go-client/sdk/error.go delete mode 100644 go-client/sdk/ext.go delete mode 100644 go-client/sdk/sys.go delete mode 100644 go-client/sdk/trade.go create mode 100644 py-client/__pycache__/main.cpython-311.pyc create mode 100644 py-client/__pycache__/test.cpython-311.pyc create mode 100644 py-client/config/__init__.py create mode 100644 py-client/config/__pycache__/__init__.cpython-311.pyc create mode 100644 py-client/etc/_global.yaml create mode 100644 py-client/etc/dev.yaml create mode 100644 py-client/libs/__init__.py create mode 100644 py-client/libs/__pycache__/__init__.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/calc.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/grid_take_profit.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/http.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/market.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/signal.cpython-311.pyc create mode 100644 py-client/libs/calc.py create mode 100644 py-client/libs/grid_take_profit.py create mode 100644 py-client/libs/http.py create mode 100644 py-client/libs/market.py create mode 100644 py-client/libs/signal.py create mode 100644 py-client/main.py create mode 100644 py-client/sdk/__init__.py create mode 100644 py-client/sdk/__pycache__/__init__.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/account.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/client.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/data.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/errors.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/misc.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/models.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/trade.cpython-311.pyc create mode 100644 py-client/sdk/account.py create mode 100644 py-client/sdk/client.py create mode 100644 py-client/sdk/data.py create mode 100644 py-client/sdk/errors.py create mode 100644 py-client/sdk/misc.py create mode 100644 py-client/sdk/models.py create mode 100644 py-client/sdk/trade.py create mode 100644 py-client/strategy/__init__.py create mode 100644 py-client/strategy/__pycache__/__init__.cpython-311.pyc create mode 100644 py-client/strategy/__pycache__/boot.cpython-311.pyc create mode 100644 py-client/strategy/trend/__init__.py create mode 100644 py-client/strategy/trend/__pycache__/__init__.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/boot.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/open.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/order.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/positions.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/run.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/runtime.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/state.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/strategy.cpython-311.pyc create mode 100644 py-client/strategy/trend/__pycache__/watch.cpython-311.pyc create mode 100644 py-client/strategy/trend/boot.py create mode 100644 py-client/strategy/trend/open.py create mode 100644 py-client/strategy/trend/order.py create mode 100644 py-client/strategy/trend/positions.py create mode 100644 py-client/strategy/trend/runtime.py create mode 100644 py-client/strategy/trend/state.py create mode 100644 py-client/strategy/trend/watch.py create mode 100644 py-client/test.py create mode 100644 run.bat create mode 100644 setup.bat diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f4124f87ac1055d139e8656c55ec4acb1c1e1f8 GIT binary patch literal 9052 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 0 && IsAllow { - openSignal(client, ticks, allowOpen) - } - - // 8 持仓计算 - managePositions(client, ticks, positions, IsAllow) -} - -func SignalFilter(in []*libs.SignalItem, name []string) []*libs.SignalItem { - newSignalItem := make([]*libs.SignalItem, 0) - if len(name) == 0 { - return newSignalItem - } - for _, i := range in { - for _, n := range name { - if i.SignalKey == n { - newSignalItem = append(newSignalItem, i) - } - } - } - return newSignalItem -} diff --git a/go-client/apps/trend/logic/open.go b/go-client/apps/trend/logic/open.go deleted file mode 100644 index 12c7b19..0000000 --- a/go-client/apps/trend/logic/open.go +++ /dev/null @@ -1,90 +0,0 @@ -package logic - -import ( - "strconv" - "strings" - "time" - - "big-qmt/go-client/config" - "big-qmt/go-client/libs" - "big-qmt/go-client/sdk" -) - -func openSignal(client *sdk.Client, ticks map[string]sdk.Tick, openSignals []*libs.SignalItem) { - for _, item := range openSignals { - // 验证信号配置的时间区间 - if !CheckTimezone(item.SignalKey) { - continue - } - // 是否有锁 - if OrderBook.IsLock("BUY", item.Code) { - continue - } - // 验证价格 - price := ticks[item.Code].LastPrice - if price <= 0 { - continue - } - // 防止接飞刀 - if !OpenWatch.Triggered("开仓", item.Code, price) { - continue - } - // 计算开仓数量 - volume := libs.CalcBuyVolume(price, config.Account.BuyValue) - if volume <= 0 { - continue - } - // 开仓 - orderID := NewOrderID("base") - if !OrderBook.Place(client, sdk.OpBuy, item.Code, volume, orderID) { - continue - } - // 保存状态 - QuantState.Set(&StateItem{Code: item.Code, BaseOrderId: orderID, BaseQty: volume, BaseCost: price, BaseStatus: StatusIng}) - if err := QuantState.Save(); err != nil { - logf("ERROR", "%v", err) - } - logf("INFO", "[ZT][开仓] %s 买入 %d 股", item.Code, volume) - } -} - -// 当前时间区间验证 *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段 -func CheckTimezone(sk string) bool { - timezone := strings.TrimSpace(config.Global.Signals[sk].Timezone) - if timezone == "*" { - return true - } - - parse := func(value string) (int, bool) { - parts := strings.Split(strings.TrimSpace(value), ":") - if len(parts) != 2 { - return 0, false - } - hour, errHour := strconv.Atoi(parts[0]) - minute, errMinute := strconv.Atoi(parts[1]) - if errHour != nil || errMinute != nil || hour < 0 || hour > 23 || minute < 0 || minute > 59 { - return 0, false - } - return hour*60 + minute, true - } - - current := time.Now() - now := current.Hour()*60 + current.Minute() - for _, section := range strings.Split(timezone, ",") { - bounds := strings.Split(strings.TrimSpace(section), "-") - if len(bounds) != 2 { - continue - } - start, startOK := parse(bounds[0]) - end, endOK := parse(bounds[1]) - if !startOK || !endOK { - continue - } - if (start <= end && now >= start && now <= end) || - (start > end && (now >= start || now <= end)) { - return true - } - } - - return false -} diff --git a/go-client/apps/trend/logic/order.go b/go-client/apps/trend/logic/order.go deleted file mode 100644 index c0c88c0..0000000 --- a/go-client/apps/trend/logic/order.go +++ /dev/null @@ -1,154 +0,0 @@ -package logic - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "slices" - "strconv" - "strings" - "sync" - "time" - - "big-qmt/go-client/sdk" -) - -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 - - OrderBook *Books -) - -type OrderItem struct { - ID string - Code string - Side string - Remark string - Status string - CreatedAt time.Time - Volume int -} - -type Books struct { - mu sync.Mutex - Data map[string]*OrderItem - Index []string -} - -func NewOrderBook() { - OrderBook = &Books{Data: make(map[string]*OrderItem), Index: make([]string, 0)} -} - -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 (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() -} diff --git a/go-client/apps/trend/logic/positions.go b/go-client/apps/trend/logic/positions.go deleted file mode 100644 index 0b4e463..0000000 --- a/go-client/apps/trend/logic/positions.go +++ /dev/null @@ -1,132 +0,0 @@ -package logic - -import ( - "math" - "sync" - - "big-qmt/go-client/config" - "big-qmt/go-client/libs" - "big-qmt/go-client/sdk" -) - -const ( - legBase = "base" - legAdded = "add" -) - -var ( - peakMu sync.Mutex - peakGrids = make(map[string]int) -) - -func managePositions(client *sdk.Client, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) { - -} - -func syncAdded(item *StateItem, position sdk.Position) { - addedQty := position.Volume - item.BaseQty - if addedQty <= 0 { - item.BaseQty = position.Volume - item.BaseCost = position.OpenPrice - item.AddedQty = 0 - item.AddedCost = 0 - item.AddedStatus = StatusNone - peakMu.Lock() - delete(peakGrids, item.Code+"|"+legAdded) - peakMu.Unlock() - return - } - - item.AddedQty = addedQty - totalCost := position.OpenPrice * float64(position.Volume) - baseCost := item.BaseCost * float64(item.BaseQty) - item.AddedCost = math.Max(0, (totalCost-baseCost)/float64(addedQty)) - item.AddedStatus = StatusOk -} - -func buyAdded(client *sdk.Client, item *StateItem, price float64, marketOK bool, budget *float64) { - if !marketOK || !PosbuyWatch.Triggered("补仓", item.Code, price) { - return - } - volume := libs.CalcBuyVolume(price, config.Account.BuyValue) - amount := price * float64(volume) - if amount > *budget || orderBusy(item.Code, "BUY") { - return - } - - orderID := NewOrderID(legAdded) - if !OrderBook.Place(client, sdk.OpBuy, item.Code, volume, orderID) { - return - } - item.AddedOrderId = orderID - item.AddedNum++ - item.AddedQty = volume - item.AddedCost = price - item.AddedStatus = StatusIng - QuantState.Set(item) - *budget -= amount -} - -func sell(client *sdk.Client, item *StateItem, usable, volume int, leg string, pnl float64) { - volume -= volume % 100 - if volume <= 0 || usable < volume || orderBusy(item.Code, "SELL") { - return - } - orderID := NewOrderID(leg) - if !OrderBook.Place(client, sdk.OpSell, item.Code, volume, orderID) { - return - } - if leg == legAdded { - item.AddedOrderId = orderID - item.AddedStatus = StatusIng - } else { - item.BaseOrderId = orderID - item.BaseStatus = StatusIng - } - QuantState.Set(item) - logf("INFO", "[止盈] %s 卖出%d股,盈利=%.2f%%", item.Code, volume, pnl) -} - -func orderBusy(code, side string) bool { - OrderBook.mu.Lock() - defer OrderBook.mu.Unlock() - order := OrderBook.Data[side+"-"+code] - if order == nil { - return false - } - switch order.Status { - case "48", "49", "50", "51", "52", "55": - return true - default: - return false - } -} - -func shouldSell(code, leg string, pnl float64) bool { - if pnl < config.Account.MinProfitPct { - return false - } - grid := int(math.Floor(pnl / config.Account.GridStepPct)) - key := code + "|" + leg - peakMu.Lock() - defer peakMu.Unlock() - peak, tracked := peakGrids[key] - if !tracked || grid > peak { - peakGrids[key] = grid - return false - } - return grid < peak -} - -func forget(code string) { - OpenWatch.mu.Lock() - delete(OpenWatch.Data, code) - OpenWatch.mu.Unlock() - PosbuyWatch.mu.Lock() - delete(PosbuyWatch.Data, code) - PosbuyWatch.mu.Unlock() - peakMu.Lock() - delete(peakGrids, code+"|"+legBase) - delete(peakGrids, code+"|"+legAdded) - peakMu.Unlock() -} diff --git a/go-client/apps/trend/logic/state.go b/go-client/apps/trend/logic/state.go deleted file mode 100644 index f1bc9d3..0000000 --- a/go-client/apps/trend/logic/state.go +++ /dev/null @@ -1,152 +0,0 @@ -package logic - -import ( - "encoding/json" - "fmt" - "os" - "path" - "slices" - "sync" - - "big-qmt/go-client/config" - "big-qmt/go-client/sdk" -) - -var ( - StatusNone = "" - StatusIng = "ING" // 处理中 - StatusOk = "OK" // 成功 - QuantState *State -) - -type State struct { - AbsPath string - mu sync.Mutex - Items map[string]*StateItem - Codes []string -} - -type StateItem struct { - Code string `json:"code"` - BaseOrderId string `json:"base_order_id"` - BaseQty int `json:"base_qty"` - BaseCost float64 `json:"base_cost"` - BaseStatus string `json:"base_status,omitempty"` - AddedOrderId string `json:"added_order_id"` - AddedNum int `json:"add_num"` - AddedQty int `json:"add_qty"` - AddedCost float64 `json:"add_cost"` - AddedStatus string `json:"added_status,omitempty"` -} - -func InitState(sn string) error { - absPath := path.Join(config.Global.QMTDataDir, fmt.Sprintf("%s_%s_state.json", sn, config.Account.AccountID)) - items, err := loadStateFile(absPath) - if err != nil { - return err - } - - var codes []string - for code, _ := range items { - codes = append(codes, code) - } - - QuantState = &State{ - AbsPath: absPath, - Items: items, - Codes: codes, - } - return nil -} - -func loadStateFile(fp string) (map[string]*StateItem, error) { - raw, err := os.ReadFile(fp) - if err != nil { - return nil, fmt.Errorf("[状态] 读取失败: %v", err) - } - var items map[string]*StateItem - if err := json.Unmarshal(raw, &items); err != nil { - return nil, fmt.Errorf("[状态] 解析失败:%s", err) - } - return items, nil - -} - -func SyncPositions(positions []sdk.Position) error { - for _, pos := range positions { - code := pos.StockCode - if code == "" || pos.Volume <= 0 || pos.OpenPrice <= 0 { - continue - } - if !slices.Contains(QuantState.Codes, code) { - item := &StateItem{ - Code: code, - BaseQty: pos.Volume, - BaseCost: pos.OpenPrice, - BaseStatus: StatusOk, - } - QuantState.Append(item) - logf("WARNING", "[状态] %s 首次接管为底仓 数量=%d 成本=%.2f", code, pos.Volume, pos.OpenPrice) - } - } - - return QuantState.Save() -} - -func (s *State) Append(i *StateItem) { - s.mu.Lock() - defer s.mu.Unlock() - - s.Items[i.Code] = i - s.Codes = append(s.Codes, i.Code) -} - -func (s *State) Get(code string) (*StateItem, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if i, ok := s.Items[code]; ok { - return i, nil - } else { - return nil, fmt.Errorf("%s not found.", code) - } -} - -func (s *State) Set(i *StateItem) { - s.mu.Lock() - defer s.mu.Unlock() - - if _, ok := s.Items[i.Code]; !ok { - s.Codes = append(s.Codes, i.Code) - } - s.Items[i.Code] = i -} - -func (s *State) Delete(code string) { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.Items, code) - if index := slices.Index(s.Codes, code); index >= 0 { - s.Codes = slices.Delete(s.Codes, index, index+1) - } -} - -func (s *State) Save() error { - s.mu.Lock() - defer s.mu.Unlock() - - // 写入AbsPath文件 - f, err := os.OpenFile(s.AbsPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) - if err != nil { - return fmt.Errorf("[状态] 打开文件失败: %v", err) - } - defer f.Close() - - encoder := json.NewEncoder(f) - encoder.SetIndent("", " ") - if err := encoder.Encode(s.Items); err != nil { - return fmt.Errorf("[状态] 写入失败: %v", err) - } - return nil -} diff --git a/go-client/apps/trend/logic/watch.go b/go-client/apps/trend/logic/watch.go deleted file mode 100644 index 132d899..0000000 --- a/go-client/apps/trend/logic/watch.go +++ /dev/null @@ -1,66 +0,0 @@ -package logic - -import ( - "sync" - "time" -) - -var ( - WatchExpireTime = 5 * time.Minute - WatchReThreshold = 0.61 - - OpenWatch *WatchMu - PosbuyWatch *WatchMu -) - -type dipWatch struct { - LastClose float64 - ExpiresAt time.Time -} - -type WatchMu struct { - mu sync.Mutex - Data map[string]dipWatch -} - -func InitWatch() { - OpenWatch = &WatchMu{ - Data: make(map[string]dipWatch), - } - PosbuyWatch = &WatchMu{ - Data: make(map[string]dipWatch), - } -} - -func (w *WatchMu) Triggered(tag, code string, price float64) bool { - if price <= 0 { - return false - } - w.mu.Lock() - defer w.mu.Unlock() - now := time.Now() - watch, ok := w.Data[code] - if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) { - w.Data[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(WatchExpireTime)} - logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price) - return false - } - if price < watch.LastClose { - watch.LastClose = price - watch.ExpiresAt = now.Add(WatchExpireTime) - w.Data[code] = watch - logf("INFO", "[%s-下跌] %s 刷新低点=%.2f", tag, code, price) - return false - } - rebound := (price - watch.LastClose) / watch.LastClose * 100 - if rebound <= 0 { - return false - } - if rebound < WatchReThreshold { - logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, WatchReThreshold) - return false - } - delete(w.Data, code) - logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose) - return true -} diff --git a/go-client/apps/trend/main.go b/go-client/apps/trend/main.go deleted file mode 100644 index 5fdcfd3..0000000 --- a/go-client/apps/trend/main.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - "os/signal" - "syscall" - "time" - - "big-qmt/go-client/apps/trend/logic" - "big-qmt/go-client/config" - "big-qmt/go-client/libs" - "big-qmt/go-client/sdk" - - "github.com/robfig/cron/v3" -) - -var ( - StrategyName = "trend" -) - -func main() { - log.SetFlags(log.LstdFlags | log.Lmicroseconds) - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - - // 第一步:只从 YAML 文件加载系统配置和本机账户配置。 - err := config.Load("etc") - if err != nil { - log.Fatalf("[ERROR] 加载配置失败: %v", err) - } - client := sdk.New(config.Global.QMTBaseURL, config.Global.QMTToken, config.HttpTimeOut) - - // 第二步:QMT 未就绪时持续重试,退出信号仍可立即终止等待。 - assets, positions, ok := waitForQMT(ctx, client) - if !ok { - return - } - - // 第三步 初始化 - logic.NewOrderBook() - logic.InitWatch() - if err := logic.InitState(StrategyName); err != nil { - log.Panicln("ERROR", err.Error()) - } - if err := logic.SyncPositions(positions); err != nil { - log.Panicln("ERROR", err.Error()) - } - signals, err := libs.InitSignals() - if err != nil { - log.Panicln("ERROR", err.Error()) - } - - // 打印启动信息 - logic.Overview(assets, positions) - log.Printf("[INFO] 已加载 %d 个开仓信号", len(signals)) - - // 第四步:工作日每 30 秒触发,交易时段由 RunOnce 统一判断。 - scheduler := cron.New( - cron.WithSeconds(), - cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger)), - ) - if _, err := scheduler.AddFunc("0,30 * 9-15 * * 1-5", func() { - logic.RunOnce(ctx, client, signals) - }); err != nil { - log.Fatalf("[ERROR] 创建计划任务失败: %v", err) - } - scheduler.Start() - log.Printf("[INFO] 计划任务已启动") - - <-ctx.Done() - <-scheduler.Stop().Done() - log.Printf("[INFO] 停止") -} - -func waitForQMT(ctx context.Context, client *sdk.Client) (*sdk.Assets, []sdk.Position, bool) { - for { - attempt, cancel := context.WithTimeout(ctx, config.HttpTimeOut) - assets, assetsErr := client.Assets(attempt) - _, positions, positionsErr := client.Positions(attempt) - cancel() - if assetsErr == nil && positionsErr == nil { - log.Printf("[INFO] QMT连接成功: %s", config.Global.QMTBaseURL) - return assets, positions, true - } - log.Printf("[WARNING] QMT未就绪,5秒后重试: assets=%v positions=%v", assetsErr, positionsErr) - select { - case <-ctx.Done(): - return nil, nil, false - case <-time.After(5 * time.Second): - } - } -} diff --git a/go-client/config/config.go b/go-client/config/config.go deleted file mode 100644 index 7e784c1..0000000 --- a/go-client/config/config.go +++ /dev/null @@ -1,104 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "gopkg.in/yaml.v3" -) - -var ( - Global *GlobalConfig - Account *AccountConfig - HttpTimeOut time.Duration = 5 * time.Second -) - -type GlobalConfig struct { - QMTBaseURL string `yaml:"qmt_base_url"` - QMTToken string `yaml:"qmt_token"` - APIHost string `yaml:"api_host"` - QMTDataDir string `yaml:"qmt_data_dir"` - Hosts map[string]string `yaml:"hosts"` - Signals map[string]SignalConfig `yaml:"signals"` -} - -type SignalConfig struct { - Url string `yaml:"url"` - Timezone string `yaml:"timezone"` - GtLastPriceIsOpen bool `yaml:"gt_last_price_is_open"` -} - -type AccountConfig struct { - AccountID string `yaml:"account_id"` - HostKey string `yaml:"host_key"` - BuyValue float64 `yaml:"buy_value"` - MinCashRatio float64 `yaml:"min_cash_ratio"` - LossTriggerPct float64 `yaml:"loss_trigger_pct"` - GridStepPct float64 `yaml:"grid_step_pct"` - MinProfitPct float64 `yaml:"min_profit_pct"` - SignalAllow []string `yaml:"signal_allow"` -} - -// Load 根据 global.yaml 中的 hosts 映射加载当前计算机的账户配置。 -func Load(etcDir string) error { - var global GlobalConfig - if err := readYAML(filepath.Join(etcDir, "global.yaml"), &global); err != nil { - return err - } - hostname, err := os.Hostname() - if err != nil { - return fmt.Errorf("读取计算机名失败: %w", err) - } - if global.QMTBaseURL == "" || global.APIHost == "" || global.QMTDataDir == "." { - return fmt.Errorf("Global 配置缺少必要参数") - } - if err := os.MkdirAll(global.QMTDataDir, 0o755); err != nil { - return fmt.Errorf("创建目录 %s 失败: %w", global.QMTDataDir, err) - } - - accountFile := hostAccountFile(global.Hosts, hostname) - if accountFile == "" { - return fmt.Errorf("global.yaml 未配置计算机 %q", hostname) - } - if filepath.Ext(accountFile) == "" { - accountFile += ".yaml" - } - - var account AccountConfig - if err := readYAML(filepath.Join(etcDir, accountFile), &account); err != nil { - return err - } - - if account.BuyValue <= 0 || account.GridStepPct <= 0 { - return fmt.Errorf("buy_value、grid_step_pct 和超时时间必须大于 0") - } - - account.HostKey = strings.ToLower(account.HostKey) - Global = &global - Account = &account - - return nil -} - -func readYAML(path string, dest any) error { - raw, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("读取配置 %s 失败: %w", path, err) - } - if err := yaml.Unmarshal(raw, dest); err != nil { - return fmt.Errorf("解析配置 %s 失败: %w", path, err) - } - return nil -} - -func hostAccountFile(hosts map[string]string, hostname string) string { - for host, file := range hosts { - if strings.EqualFold(strings.TrimSpace(host), strings.TrimSpace(hostname)) { - return strings.TrimSpace(file) - } - } - return "" -} diff --git a/go-client/etc/dev.yaml b/go-client/etc/dev.yaml deleted file mode 100644 index ed1763f..0000000 --- a/go-client/etc/dev.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# 当前计算机使用的账户和策略参数。 -account_id: CHANGE_ME -host_key: "dev" -buy_value: 5000 -min_cash_ratio: 0.10 -loss_trigger_pct: -30 -grid_step_pct: 1 -min_profit_pct: 2 -rebound_threshold: 0.61 -signal_allow: - - dcm diff --git a/go-client/etc/global.yaml b/go-client/etc/global.yaml deleted file mode 100644 index fec6d29..0000000 --- a/go-client/etc/global.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# 系统公共参数。hosts 将 Windows 计算机名映射到账户配置文件。 -qmt_base_url: http://127.0.0.1:10086 -qmt_token: QMTbyYanweidong -api_host: http://139.224.247.176:13499 -qmt_data_dir: D:/qmt_strategy_data - -hosts: - DESKTOP-39H91QV: dev.yaml - -signals: - dcm: - url: /a/dcm_signal - timezone: * # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段 - gt_last_price_is_open: false # 大于信号的昨收价是否开仓 - morning: - url: /a/morning_signal - timezone: 9:30-10:30 # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段 - gt_last_price_is_open: true # 大于信号的昨收价是否开仓 - tail: - url: /a/tail_signal - timezone: 14:30-14:55 # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段 - gt_last_price_is_open: false # 大于信号的昨收价是否开仓 - arbitrage: - url: /a/arbitrage_signal - timezone: * # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段 - gt_last_price_is_open: false # 大于信号的昨收价是否开仓 - diff --git a/go-client/go.mod b/go-client/go.mod deleted file mode 100644 index 3e33354..0000000 --- a/go-client/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module big-qmt/go-client - -go 1.26.5 - -require ( - git.apinb.com/bsm-sdk/core v0.2.1 - github.com/robfig/cron/v3 v3.0.1 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/oklog/ulid/v2 v2.1.2 // indirect - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect -) diff --git a/go-client/go.sum b/go-client/go.sum deleted file mode 100644 index fea000d..0000000 --- a/go-client/go.sum +++ /dev/null @@ -1,15 +0,0 @@ -git.apinb.com/bsm-sdk/core v0.2.1 h1:1kpbdij3qOlf1DmKTq3coIXSgLth5iJHJ3LvVZnjaXM= -git.apinb.com/bsm-sdk/core v0.2.1/go.mod h1:BL/aGHujCWdxrKZrWaiebmLx69J0OrTVv5XfugbbyhE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= -github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-client/libs/calc.go b/go-client/libs/calc.go deleted file mode 100644 index 2764370..0000000 --- a/go-client/libs/calc.go +++ /dev/null @@ -1,38 +0,0 @@ -package libs - -import ( - "math" - "math/rand" - "time" -) - -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func randStr(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} - -func TradingTime(t time.Time) bool { - if t.Weekday() == time.Saturday || t.Weekday() == time.Sunday { - return false - } - second := t.Hour()*3600 + t.Minute()*60 + t.Second() - return (second >= 9*3600+30*60 && second <= 11*3600+30*60) || - (second >= 13*3600 && second <= 15*3600) -} - -func CalcBuyVolume(price, buyValue float64) int { - if price <= 0 || buyValue <= 0 { - return 0 - } - // 不足一手时仍按最低一手委托。 - hands := int(math.Floor(buyValue / (price * 100))) - if hands == 0 { - hands = 1 - } - return hands * 100 -} diff --git a/go-client/libs/const.go b/go-client/libs/const.go deleted file mode 100644 index 5ebf0f2..0000000 --- a/go-client/libs/const.go +++ /dev/null @@ -1,8 +0,0 @@ -package libs - -import "time" - -var ( - API_HOST = "http://139.224.247.176:13499" - HTTPTimeout = 5 * time.Second -) diff --git a/go-client/libs/http.go b/go-client/libs/http.go deleted file mode 100644 index cb62abd..0000000 --- a/go-client/libs/http.go +++ /dev/null @@ -1,33 +0,0 @@ -package libs - -import ( - "fmt" - "io" - "net/http" - "strings" - "time" -) - -// GetJSON 请求 JSON 接口并返回对象。 -func GetJSON(rawURL string, timeout time.Duration) ([]byte, error) { - req, err := http.NewRequest(http.MethodGet, rawURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "big-qmt-go/1") - client := &http.Client{Timeout: timeout} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) - } - return body, nil -} diff --git a/go-client/libs/market.go b/go-client/libs/market.go deleted file mode 100644 index bdfb528..0000000 --- a/go-client/libs/market.go +++ /dev/null @@ -1,67 +0,0 @@ -package libs - -import ( - "encoding/json" - "fmt" - "log" - "strings" -) - -var ( - MarketUrl = "/a/market" - Period = "60m" -) - -// AllowOpen 每次开仓或补仓前取 60 分钟大盘信号,只有 UP 才放行。 -func MarketAllowOpen() bool { - // gen url. - fullUrl := fmt.Sprintf("%s%s?period=%s&t=%s", API_HOST, MarketUrl, Period, randStr(16)) - payload, err := GetJSON(fullUrl, HTTPTimeout) - if err != nil { - log.Printf("[ERROR] 获取大盘指数失败: %s %v", fullUrl, err) - return false - } - var result map[string]any - err = json.Unmarshal(payload, &result) - if err != nil { - log.Printf("[ERROR] 获取大盘指数解析: %v", err) - return false - } - - status := Status(result) - log.Printf("[INFO] 大盘信号: url=%s status=%s", fullUrl, status) - return status == "UP" -} - -// Status 兼容常见响应结构;无法识别的值统一按 UNKNOWN 处理。 -func Status(payload map[string]any) string { - var value any = payload - if m, ok := value.(map[string]any); ok { - if d, exists := m["data"]; exists { - value = d - } - } - if arr, ok := value.([]any); ok { - if len(arr) == 0 { - value = nil - } else { - value = arr[len(arr)-1] - } - } - if m, ok := value.(map[string]any); ok { - if v, exists := m["action"]; exists { - value = v - } else if v, exists := m["status"]; exists { - value = v - } else if v, exists := m["signal"]; exists { - value = v - } - } - s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(value))) - switch s { - case "UP", "DOWN", "NEUTRAL": - return s - default: - return "UNKNOWN" - } -} diff --git a/go-client/libs/signal.go b/go-client/libs/signal.go deleted file mode 100644 index 84e0547..0000000 --- a/go-client/libs/signal.go +++ /dev/null @@ -1,57 +0,0 @@ -package libs - -import ( - "big-qmt/go-client/config" - "encoding/json" - "fmt" -) - -type SignalResult struct { - Code string `json:"code"` - Total int `json:"total"` - Updated string `json:"updated"` - Data map[string]SignalItem `json:"data"` - Message string `json:"message"` -} - -type SignalItem struct { - SignalKey string `json:"signal_key"` - Code string `json:"code"` - Name string `json:"name"` - Desc string `json:"desc"` - LastClose float64 `json:"last_close"` - TechIndicator map[string]float64 `json:"tech_indicator"` -} - -func InitSignals() ([]*SignalItem, error) { - CacheSignals := make([]*SignalItem, 0) - for key, s := range config.Global.Signals { - result, err := FetchSignal(s.Url) - if err != nil { - return nil, fmt.Errorf("[信号] %s, 获取错误:%v", key, err) - } - for _, item := range result.Data { - item.SignalKey = key - CacheSignals = append(CacheSignals, &item) - } - } - return CacheSignals, nil -} - -// FetchSignals 启动时读取信号,运行期间直接使用内存数据。 -func FetchSignal(subUrl string) (*SignalResult, error) { - // gen url. - fullUrl := fmt.Sprintf("%s%s?t=%s", API_HOST, subUrl, randStr(16)) - // doing - payload, err := GetJSON(fullUrl, HTTPTimeout) - if err != nil { - return nil, fmt.Errorf("开仓信号获取失败: %s %v", fullUrl, err) - } - - var result SignalResult - err = json.Unmarshal(payload, &result) - if err != nil { - return nil, fmt.Errorf("开仓信号解析失败: %s %v", fullUrl, err) - } - return &result, nil -} diff --git a/go-client/sdk/account.go b/go-client/sdk/account.go deleted file mode 100644 index 97b8ece..0000000 --- a/go-client/sdk/account.go +++ /dev/null @@ -1,179 +0,0 @@ -package sdk - -import ( - "context" - "encoding/json" - "fmt" -) - -// Position 对应 HoldingHandler 封装后的持仓。 -type Position struct { - StockCode string `json:"StockCode"` - StockName string `json:"StockName"` - Direction any `json:"Direction"` - Volume int `json:"Volume"` - OpenPrice float64 `json:"OpenPrice"` - FloatProfit float64 `json:"FloatProfit"` - MarketValue float64 `json:"MarketValue"` - StockHolder string `json:"StockHolder"` - FrozenVolume int `json:"FrozenVolume"` - CanUseVolume int `json:"CanUseVolume"` - OnRoadVolume int `json:"OnRoadVolume"` - YesterdayVolume int `json:"YesterdayVolume"` - LastPrice float64 `json:"LastPrice"` - ProfitRate float64 `json:"ProfitRate"` - FutureTradeType any `json:"FutureTradeType"` - ExpireDate string `json:"ExpireDate"` -} - -type Assets struct { - Total float64 `json:"total"` - Available float64 `json:"available"` -} - -func (c *Client) Positions(ctx context.Context) ([]string, []Position, error) { - return c.decodePositions(ctx, "/api/v2/positions") -} - -func (c *Client) Holding(ctx context.Context) ([]string, []Position, error) { - return c.decodePositions(ctx, "/api/holding") -} - -func (c *Client) decodePositions(ctx context.Context, path string) ([]string, []Position, error) { - raw := map[string]json.RawMessage{} - if err := c.post(ctx, path, map[string]any{"account": c.accountType}, &raw); err != nil { - return nil, nil, err - } - codes := make([]string, 0, len(raw)) - out := make([]Position, 0, len(raw)) - for code, blob := range raw { - var p Position - if err := json.Unmarshal(blob, &p); err != nil { - return nil, nil, fmt.Errorf("position %s: %w", code, err) - } - if p.StockCode == "" { - p.StockCode = code - } - codes = append(codes, code) - out = append(out, p) - } - return codes, out, nil -} - -func (c *Client) Assets(ctx context.Context) (*Assets, error) { - var out Assets - if err := c.post(ctx, "/api/v2/assets", map[string]any{"account": c.accountType}, &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) TotalMoney(ctx context.Context) (float64, error) { - var out struct { - TotalMoney float64 `json:"total_money"` - } - if err := c.post(ctx, "/api/money/total", map[string]any{"account": c.accountType}, &out); err != nil { - return 0, err - } - return out.TotalMoney, nil -} - -func (c *Client) AvailableMoney(ctx context.Context) (float64, error) { - var out struct { - AvailableMoney float64 `json:"available_money"` - } - if err := c.post(ctx, "/api/money/available", map[string]any{"account": c.accountType}, &out); err != nil { - return 0, err - } - return out.AvailableMoney, nil -} - -type OrderRefResult struct { - Status string `json:"status"` - Action string `json:"action"` - Stock string `json:"stock"` - OpType int `json:"opType"` - OrderRef string `json:"order_ref"` -} - -func (c *Client) Buy(ctx context.Context, stock string, price float64, volume int, prType int) (*OrderRefResult, error) { - body := map[string]any{"stock": stock, "price": price, "volume": volume} - if prType != 0 { - body["prType"] = prType - } - var out OrderRefResult - if err := c.post(ctx, "/api/order/buy", body, &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) Sell(ctx context.Context, stock string, price float64, volume int, prType int) (*OrderRefResult, error) { - body := map[string]any{"stock": stock, "price": price, "volume": volume} - if prType != 0 { - body["prType"] = prType - } - var out OrderRefResult - if err := c.post(ctx, "/api/order/sell", body, &out); err != nil { - return nil, err - } - return &out, nil -} - -type OrderStatus struct { - OrderSysID string `json:"order_sys_id"` - Status int `json:"status"` - VolumeLeft int `json:"volume_left"` - VolumeTraded int `json:"volume_traded"` -} - -func (c *Client) OrderStatusList(ctx context.Context) ([]OrderStatus, error) { - var out struct { - Orders []OrderStatus `json:"orders"` - } - if err := c.post(ctx, "/api/order/status", map[string]any{"account": c.accountType}, &out); err != nil { - return nil, err - } - return out.Orders, nil -} - -type CanceledOrder struct { - OrderSysID string `json:"order_sys_id"` - Stock string `json:"stock"` - VolumeLeft int `json:"volume_left"` -} - -type CancelAllResult struct { - Status string `json:"status"` - Message string `json:"message"` - CanceledOrders []CanceledOrder `json:"canceled_orders"` - CanceledSysIDs []string `json:"canceled_sys_ids"` -} - -func (c *Client) CancelAll(ctx context.Context) (*CancelAllResult, error) { - var out CancelAllResult - if err := c.post(ctx, "/api/order/cancel_all", map[string]any{"account": c.accountType}, &out); err != nil { - return nil, err - } - return &out, nil -} - -// CancelByRule 按「代码.市场 + (剩余+已成)」匹配撤单。HTTP 没有按委托号撤单。 -func (c *Client) CancelByRule(ctx context.Context, stock string, volume int) (*CancelAllResult, error) { - var out CancelAllResult - body := map[string]any{"stock": stock, "volume": volume, "account": c.accountType} - if err := c.post(ctx, "/api/order/cancel_order", body, &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) Deals(ctx context.Context) ([]map[string]string, error) { - var out struct { - Deals []map[string]string `json:"deals"` - } - if err := c.post(ctx, "/api/order/deal", map[string]any{"account": c.accountType}, &out); err != nil { - return nil, err - } - return out.Deals, nil -} diff --git a/go-client/sdk/check.go b/go-client/sdk/check.go deleted file mode 100644 index e0109cf..0000000 --- a/go-client/sdk/check.go +++ /dev/null @@ -1,30 +0,0 @@ -package sdk - -import "context" - -func (c *Client) IsLastBar(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/check/is_last_bar", "is_last_bar") -} - -func (c *Client) IsNewBar(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/check/is_new_bar", "is_new_bar") -} - -func (c *Client) IsSuspendedStock(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/check/is_suspended_stock", map[string]any{"stockcode": stockcode}, "is_suspended") -} - -func (c *Client) IsSectorStock(ctx context.Context, sectorname, market, stockcode string) (any, error) { - body := map[string]any{"sectorname": sectorname, "market": market, "stockcode": stockcode} - return c.postField(ctx, "/api/check/is_sector_stock", body, "is_in_sector") -} - -func (c *Client) IsTypedStock(ctx context.Context, stocktypenum int, market, stockcode string) (any, error) { - body := map[string]any{"stocktypenum": stocktypenum, "market": market, "stockcode": stockcode} - return c.postField(ctx, "/api/check/is_typed_stock", body, "result") -} - -func (c *Client) IndustryNameOfStock(ctx context.Context, industryType, stockcode string) (any, error) { - body := map[string]any{"industryType": industryType, "stockcode": stockcode} - return c.postField(ctx, "/api/check/get_industry_name_of_stock", body, "industry_name") -} diff --git a/go-client/sdk/client.go b/go-client/sdk/client.go deleted file mode 100644 index b8009c8..0000000 --- a/go-client/sdk/client.go +++ /dev/null @@ -1,141 +0,0 @@ -package sdk - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" -) - -type Client struct { - baseURL string - token string - accountType string - http *http.Client -} - -func New(baseURL, token string, timeout time.Duration) *Client { - if timeout <= 0 { - timeout = 15 * time.Second - } - return &Client{ - baseURL: strings.TrimRight(baseURL, "/"), - token: token, - accountType: "stock", - http: &http.Client{Timeout: timeout}, - } -} - -func (c *Client) SetAccountType(accountType string) *Client { - if strings.TrimSpace(accountType) != "" { - c.accountType = accountType - } - return c -} - -func (c *Client) get(ctx context.Context, path string, dest any) error { - return c.do(ctx, http.MethodGet, path, nil, dest) -} - -func (c *Client) post(ctx context.Context, path string, body any, dest any) error { - if body == nil { - body = map[string]any{} - } - return c.do(ctx, http.MethodPost, path, body, dest) -} - -func (c *Client) do(ctx context.Context, method, path string, body any, dest any) error { - var rdr io.Reader - if body != nil && method != http.MethodGet { - raw, err := json.Marshal(body) - if err != nil { - return fmt.Errorf("marshal request: %w", err) - } - rdr = bytes.NewReader(raw) - } - req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, rdr) - if err != nil { - return err - } - req.Header.Set("X-Token", c.token) - req.Header.Set("Accept", "application/json") - if rdr != nil { - req.Header.Set("Content-Type", "application/json") - } - resp, err := c.http.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - raw, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - if resp.StatusCode >= 400 { - apiErr := &APIError{StatusCode: resp.StatusCode, Message: strings.TrimSpace(string(raw))} - var parsed APIError - if json.Unmarshal(raw, &parsed) == nil { - if parsed.StatusCode == 0 { - parsed.StatusCode = resp.StatusCode - } - if parsed.Message != "" { - apiErr = &parsed - } - } - return apiErr - } - if dest == nil || len(raw) == 0 { - return nil - } - if err := json.Unmarshal(raw, dest); err != nil { - return fmt.Errorf("unmarshal %s: %w; body=%s", path, err, truncate(raw, 512)) - } - return nil -} - -func (c *Client) getField(ctx context.Context, path, key string) (any, error) { - var out map[string]any - if err := c.get(ctx, path, &out); err != nil { - return nil, err - } - return out[key], nil -} - -func (c *Client) postField(ctx context.Context, path string, body any, key string) (any, error) { - var out map[string]any - if err := c.post(ctx, path, body, &out); err != nil { - return nil, err - } - if msg, ok := out["error"].(string); ok && msg != "" { - return nil, &BusinessError{Message: msg} - } - if key == "" { - return out, nil - } - if v, ok := out[key]; ok { - return v, nil - } - return out, nil -} - -func csvJoin(items []string) string { - parts := make([]string, 0, len(items)) - for _, s := range items { - s = strings.TrimSpace(s) - if s != "" { - parts = append(parts, s) - } - } - return strings.Join(parts, ",") -} - -func truncate(b []byte, n int) string { - if len(b) <= n { - return string(b) - } - return string(b[:n]) + "..." -} diff --git a/go-client/sdk/coerce.go b/go-client/sdk/coerce.go deleted file mode 100644 index 0dc2aef..0000000 --- a/go-client/sdk/coerce.go +++ /dev/null @@ -1,63 +0,0 @@ -package sdk - -import ( - "encoding/json" - "strconv" - "strings" -) - -func asString(v any) string { - if v == nil { - return "" - } - switch x := v.(type) { - case string: - return x - case json.Number: - return x.String() - default: - return strings.TrimSpace(fmtSprint(v)) - } -} - -func fmtSprint(v any) string { - b, err := json.Marshal(v) - if err != nil { - return "" - } - return strings.Trim(string(b), `"`) -} - -func asFloat(v any) float64 { - if v == nil { - return 0 - } - switch x := v.(type) { - case float64: - return x - case float32: - return float64(x) - case int: - return float64(x) - case int64: - return float64(x) - case json.Number: - f, _ := x.Float64() - return f - case string: - f, _ := strconv.ParseFloat(strings.TrimSpace(x), 64) - return f - default: - f, _ := strconv.ParseFloat(asString(v), 64) - return f - } -} - -func mapField(m map[string]any, names ...string) any { - for _, name := range names { - if v, ok := m[name]; ok && v != nil { - return v - } - } - return nil -} diff --git a/go-client/sdk/context.go b/go-client/sdk/context.go deleted file mode 100644 index bcaf736..0000000 --- a/go-client/sdk/context.go +++ /dev/null @@ -1,78 +0,0 @@ -package sdk - -import "context" - -type ContextInfo struct { - Period any `json:"period"` - Barpos any `json:"barpos"` - TimeTickSize any `json:"time_tick_size"` - Stockcode any `json:"stockcode"` - DividendType any `json:"dividend_type"` - Market any `json:"market"` - DoBackTest any `json:"do_back_test"` - Benchmark any `json:"benchmark"` - Capital any `json:"capital"` - Universe any `json:"universe"` -} - -func (c *Client) ContextPeriod(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/period", "period") -} - -func (c *Client) ContextBarpos(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/barpos", "barpos") -} - -func (c *Client) ContextTimeTickSize(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/time_tick_size", "time_tick_size") -} - -func (c *Client) ContextStockcode(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/stockcode", "stockcode") -} - -func (c *Client) ContextDividendType(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/dividend_type", "dividend_type") -} - -func (c *Client) ContextMarket(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/market", "market") -} - -func (c *Client) ContextDoBackTest(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/do_back_test", "do_back_test") -} - -func (c *Client) ContextBenchmark(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/benchmark", "benchmark") -} - -func (c *Client) ContextCapital(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/context/capital", "capital") -} - -func (c *Client) ContextUniverse(ctx context.Context) ([]string, error) { - v, err := c.getField(ctx, "/api/context/universe", "universe") - if err != nil { - return nil, err - } - switch u := v.(type) { - case nil: - return nil, nil - case []any: - codes := make([]string, 0, len(u)) - for _, item := range u { - if s := asString(item); s != "" { - codes = append(codes, s) - } - } - return codes, nil - case []string: - return u, nil - default: - if s := asString(u); s != "" { - return []string{s}, nil - } - return nil, nil - } -} diff --git a/go-client/sdk/data.go b/go-client/sdk/data.go deleted file mode 100644 index ac6e9ed..0000000 --- a/go-client/sdk/data.go +++ /dev/null @@ -1,374 +0,0 @@ -package sdk - -import ( - "context" - "strconv" - "strings" -) - -type Tick struct { - LastPrice float64 - LastClose float64 - Raw map[string]any -} - -type HistoryDataRequest struct { - Len int - Period string - Field string - DividendType int - SkipPaused *bool -} - -type MarketDataRequest struct { - Fields []string - Stocks []string - StartTime string - EndTime string - Period string - DividendType string - Count int -} - -type SubscribeResult struct { - Status string `json:"status"` - SubID any `json:"sub_id"` -} - -func (c *Client) StockName(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/stock_name", map[string]any{"stockcode": stockcode}, "name") -} - -func (c *Client) OpenDate(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/open_date", map[string]any{"stockcode": stockcode}, "open_date") -} - -func (c *Client) LastVolume(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/last_volume", map[string]any{"stockcode": stockcode}, "last_volume") -} - -func (c *Client) BarTimetag(ctx context.Context, index int) (any, error) { - return c.postField(ctx, "/api/data/bar_timetag", map[string]any{"index": index}, "timetag") -} - -func (c *Client) TickTimetag(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/data/tick_timetag", "timetag") -} - -func (c *Client) Sector(ctx context.Context, sector string, realtime int) ([]any, error) { - var out struct { - Stocks []any `json:"stocks"` - } - if err := c.post(ctx, "/api/data/sector", map[string]any{"sector": sector, "realtime": realtime}, &out); err != nil { - return nil, err - } - return out.Stocks, nil -} - -func (c *Client) Industry(ctx context.Context, industry string) ([]any, error) { - var out struct { - Stocks []any `json:"stocks"` - } - if err := c.post(ctx, "/api/data/industry", map[string]any{"industry": industry}, &out); err != nil { - return nil, err - } - return out.Stocks, nil -} - -func (c *Client) StockListInSector(ctx context.Context, sectorname string) ([]any, error) { - var out struct { - Stocks []any `json:"stocks"` - } - if err := c.post(ctx, "/api/data/stock_list_in_sector", map[string]any{"sectorname": sectorname}, &out); err != nil { - return nil, err - } - return out.Stocks, nil -} - -func (c *Client) WeightInIndex(ctx context.Context, indexcode, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/weight_in_index", map[string]any{"indexcode": indexcode, "stockcode": stockcode}, "weight") -} - -func (c *Client) ContractMultiplier(ctx context.Context, contractcode string) (any, error) { - return c.postField(ctx, "/api/data/contract_multiplier", map[string]any{"contractcode": contractcode}, "multiplier") -} - -func (c *Client) RiskFreeRate(ctx context.Context, index int) (any, error) { - return c.postField(ctx, "/api/data/risk_free_rate", map[string]any{"index": index}, "risk_free_rate") -} - -func (c *Client) DateLocation(ctx context.Context, strdate string) (any, error) { - return c.postField(ctx, "/api/data/date_location", map[string]any{"strdate": strdate}, "location") -} - -func (c *Client) HistoryData(ctx context.Context, req HistoryDataRequest) (any, error) { - if req.Len == 0 { - req.Len = 10 - } - skip := "true" - if req.SkipPaused != nil { - skip = strconv.FormatBool(*req.SkipPaused) - } - return c.postField(ctx, "/api/data/history_data", map[string]any{ - "len": req.Len, "period": req.Period, "field": req.Field, - "dividend_type": req.DividendType, "skip_paused": skip, - }, "data") -} - -func (c *Client) marketDataBody(req MarketDataRequest) map[string]any { - return map[string]any{ - "fields": csvJoin(req.Fields), - "stock_code": csvJoin(req.Stocks), - "start_time": req.StartTime, - "end_time": req.EndTime, - "period": req.Period, - "dividend_type": req.DividendType, - "count": req.Count, - } -} - -func (c *Client) MarketData(ctx context.Context, req MarketDataRequest) (any, error) { - return c.postField(ctx, "/api/data/market_data", c.marketDataBody(req), "data") -} - -func (c *Client) MarketDataEx(ctx context.Context, req MarketDataRequest) (any, error) { - return c.postField(ctx, "/api/data/market_data_ex", c.marketDataBody(req), "data") -} - -func (c *Client) FullTick(ctx context.Context, stocks []string) (map[string]Tick, error) { - raw := map[string]any{} - if err := c.post(ctx, "/api/data/full_tick", map[string]any{"stocks": stocks}, &raw); err != nil { - return nil, err - } - out := make(map[string]Tick, len(raw)) - for code, v := range raw { - tick := Tick{Raw: map[string]any{}} - if m, ok := v.(map[string]any); ok { - tick.Raw = m - tick.LastPrice = asFloat(mapField(m, "lastPrice", "last_price", "LastPrice")) - tick.LastClose = asFloat(mapField(m, "lastClose", "last_close", "LastClose")) - } - out[code] = tick - } - return out, nil -} - -func (c *Client) DividFactors(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/divid_factors", map[string]any{"stockcode": stockcode}, "factors") -} - -func (c *Client) MainContract(ctx context.Context, codemarket string) (any, error) { - return c.postField(ctx, "/api/data/main_contract", map[string]any{"codemarket": codemarket}, "main_contract") -} - -func (c *Client) TimetagToDatetime(ctx context.Context, timetag int64, format string) (any, error) { - body := map[string]any{"timetag": timetag} - if format != "" { - body["format"] = format - } - return c.postField(ctx, "/api/data/timetag_to_datetime", body, "datetime") -} - -func (c *Client) TotalShare(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/total_share", map[string]any{"stockcode": stockcode}, "total_share") -} - -func (c *Client) TradingDates(ctx context.Context, stockcode, startDate, endDate, period string, count int) ([]any, error) { - body := map[string]any{"stockcode": stockcode, "start_date": startDate, "end_date": endDate, "period": period} - if count != 0 { - body["count"] = count - } - var out struct { - Dates []any `json:"dates"` - } - if err := c.post(ctx, "/api/data/trading_dates", body, &out); err != nil { - return nil, err - } - return out.Dates, nil -} - -func (c *Client) Svol(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/svol", map[string]any{"stockcode": stockcode}, "svol") -} - -func (c *Client) Bvol(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/bvol", map[string]any{"stockcode": stockcode}, "bvol") -} - -func (c *Client) Longhubang(ctx context.Context, stockList []string, startTime, endTime string) (any, error) { - return c.postField(ctx, "/api/data/longhubang", map[string]any{ - "stock_list": csvJoin(stockList), "startTime": startTime, "endTime": endTime, - }, "data") -} - -func (c *Client) Top10ShareHolder(ctx context.Context, stockList []string, dataName, startTime, endTime string) (any, error) { - return c.postField(ctx, "/api/data/top10_share_holder", map[string]any{ - "stock_list": csvJoin(stockList), "data_name": dataName, "start_time": startTime, "end_time": endTime, - }, "data") -} - -func (c *Client) OptionDetail(ctx context.Context, optioncode string) (any, error) { - return c.postField(ctx, "/api/data/option_detail", map[string]any{"optioncode": optioncode}, "detail") -} - -func (c *Client) TurnoverRate(ctx context.Context, stockList []string, startTime, endTime string) (any, error) { - return c.postField(ctx, "/api/data/turnover_rate", map[string]any{ - "stock_list": csvJoin(stockList), "startTime": startTime, "endTime": endTime, - }, "data") -} - -func (c *Client) ETFInfo(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/etf_info", map[string]any{"stockcode": stockcode}, "info") -} - -func (c *Client) ETFIOPV(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/etf_iopv", map[string]any{"stockcode": stockcode}, "iopv") -} - -func (c *Client) InstrumentDetail(ctx context.Context, stockcode string) (any, error) { - return c.postField(ctx, "/api/data/instrumentdetail", map[string]any{"stockcode": stockcode}, "detail") -} - -func (c *Client) ContractExpireDate(ctx context.Context, codemarket string) (any, error) { - return c.postField(ctx, "/api/data/contract_expire_date", map[string]any{"codemarket": codemarket}, "expire_date") -} - -func (c *Client) OptionUndlData(ctx context.Context, undlCodeRef string) (any, error) { - return c.postField(ctx, "/api/data/option_undl_data", map[string]any{"undl_code_ref": undlCodeRef}, "data") -} - -type FinancialDataRequest struct { - Tabname string - Colname string - Market string - Code string - ReportType string - Barpos int - FieldList []string - StockList []string - StartDate string - EndDate string -} - -func (c *Client) FinancialData(ctx context.Context, req FinancialDataRequest) (any, error) { - body := map[string]any{ - "tabname": req.Tabname, "colname": req.Colname, "market": req.Market, "code": req.Code, - "report_type": req.ReportType, "barpos": req.Barpos, - "fieldList": csvJoin(req.FieldList), "stockList": csvJoin(req.StockList), - "startDate": req.StartDate, "endDate": req.EndDate, - } - return c.postField(ctx, "/api/data/financial_data", body, "data") -} - -type FactorDataRequest struct { - FieldList []string - StockList []string - StockCode string - StartDate string - EndDate string -} - -func (c *Client) FactorData(ctx context.Context, req FactorDataRequest) (any, error) { - body := map[string]any{ - "fieldList": csvJoin(req.FieldList), "stockList": csvJoin(req.StockList), - "stockCode": req.StockCode, "startDate": req.StartDate, "endDate": req.EndDate, - } - return c.postField(ctx, "/api/data/factor_data", body, "data") -} - -func (c *Client) HisSTData(ctx context.Context, stockCode string) (any, error) { - return c.postField(ctx, "/api/data/his_st_data", map[string]any{"stockCode": stockCode}, "data") -} - -func (c *Client) HisIndexData(ctx context.Context, index string) (any, error) { - return c.postField(ctx, "/api/data/his_index_data", map[string]any{"index": index}, "data") -} - -func (c *Client) AllSubscription(ctx context.Context) (any, error) { - return c.getField(ctx, "/api/data/all_subscription", "subscriptions") -} - -func (c *Client) OptionList(ctx context.Context, undlCode, dedate, opttype string, isavailable bool) (any, error) { - body := map[string]any{ - "undl_code": undlCode, "dedate": dedate, "opttype": opttype, - "isavailable": strconv.FormatBool(isavailable), - } - return c.postField(ctx, "/api/data/option_list", body, "option_list") -} - -func (c *Client) HisContractList(ctx context.Context, market string) (any, error) { - return c.postField(ctx, "/api/data/his_contract_list", map[string]any{"market": market}, "contracts") -} - -func (c *Client) OptionIV(ctx context.Context, optioncode string) (any, error) { - return c.postField(ctx, "/api/data/option_iv", map[string]any{"optioncode": optioncode}, "iv") -} - -type BSMPriceRequest struct { - OptionType string - ObjectPrices any // float64 或 []float64 - StrikePrice float64 - RiskFree float64 - Sigma float64 - Days int - Dividend float64 -} - -func (c *Client) BSMPrice(ctx context.Context, req BSMPriceRequest) (any, error) { - prices := req.ObjectPrices - if vals, ok := req.ObjectPrices.([]float64); ok { - parts := make([]string, len(vals)) - for i, v := range vals { - parts[i] = strconv.FormatFloat(v, 'f', -1, 64) - } - prices = strings.Join(parts, ",") - } - return c.postField(ctx, "/api/data/bsm_price", map[string]any{ - "optionType": req.OptionType, "objectPrices": prices, "strikePrice": req.StrikePrice, - "riskFree": req.RiskFree, "sigma": req.Sigma, "days": req.Days, "dividend": req.Dividend, - }, "price") -} - -type BSMIVRequest struct { - OptionType string `json:"optionType"` - ObjectPrices float64 `json:"objectPrices"` - StrikePrice float64 `json:"strikePrice"` - OptionPrice float64 `json:"optionPrice"` - RiskFree float64 `json:"riskFree"` - Days int `json:"days"` - Dividend float64 `json:"dividend"` -} - -func (c *Client) BSMIV(ctx context.Context, req BSMIVRequest) (any, error) { - return c.postField(ctx, "/api/data/bsm_iv", req, "iv") -} - -type LocalDataRequest struct { - StockCode string `json:"stock_code"` - StartTime string `json:"start_time,omitempty"` - EndTime string `json:"end_time,omitempty"` - Period string `json:"period,omitempty"` - DividType string `json:"divid_type,omitempty"` - Count int `json:"count"` -} - -func (c *Client) LocalData(ctx context.Context, req LocalDataRequest) (any, error) { - return c.postField(ctx, "/api/data/local_data", req, "data") -} - -func (c *Client) SubscribeQuote(ctx context.Context, stockCode, period, dividendType string) (*SubscribeResult, error) { - var out SubscribeResult - body := map[string]any{"stock_code": stockCode, "period": period, "dividend_type": dividendType} - if err := c.post(ctx, "/api/data/subscribe_quote", body, &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) UnsubscribeQuote(ctx context.Context, subID int) (*SubscribeResult, error) { - var out SubscribeResult - if err := c.post(ctx, "/api/data/unsubscribe_quote", map[string]any{"sub_id": subID}, &out); err != nil { - return nil, err - } - return &out, nil -} diff --git a/go-client/sdk/doc.go b/go-client/sdk/doc.go deleted file mode 100644 index 9ac602a..0000000 --- a/go-client/sdk/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package sdk 是 QMT_API.py HTTP 服务的 Go 客户端。 -// -// 用法:sdk.New(baseURL, token, timeout),账户类型默认 stock,资金账号由服务端环境变量决定。 -package sdk diff --git a/go-client/sdk/error.go b/go-client/sdk/error.go deleted file mode 100644 index cd888e4..0000000 --- a/go-client/sdk/error.go +++ /dev/null @@ -1,38 +0,0 @@ -package sdk - -import ( - "fmt" - "net/http" -) - -// APIError 表示服务端返回的 HTTP 错误(write_error 格式)。 -type APIError struct { - StatusCode int `json:"status_code"` - Message string `json:"error"` -} - -func (e *APIError) Error() string { - if e == nil { - return "qmt api error" - } - if e.Message == "" { - return fmt.Sprintf("qmt api: http %d", e.StatusCode) - } - return fmt.Sprintf("qmt api: http %d: %s", e.StatusCode, e.Message) -} - -func (e *APIError) Unauthorized() bool { - return e != nil && e.StatusCode == http.StatusUnauthorized -} - -// BusinessError 表示 HTTP 200 但业务 JSON 带 error 字段。 -type BusinessError struct { - Message string -} - -func (e *BusinessError) Error() string { - if e == nil || e.Message == "" { - return "qmt api business error" - } - return e.Message -} diff --git a/go-client/sdk/ext.go b/go-client/sdk/ext.go deleted file mode 100644 index b42e122..0000000 --- a/go-client/sdk/ext.go +++ /dev/null @@ -1,27 +0,0 @@ -package sdk - -import "context" - -func (c *Client) ExtData(ctx context.Context, extdataname, stockcode string, deviation int) (any, error) { - return c.postField(ctx, "/api/ext/ext_data", map[string]any{ - "extdataname": extdataname, "stockcode": stockcode, "deviation": deviation, - }, "value") -} - -func (c *Client) ExtDataRank(ctx context.Context, extdataname, stockcode string, deviation int) (any, error) { - return c.postField(ctx, "/api/ext/ext_data_rank", map[string]any{ - "extdataname": extdataname, "stockcode": stockcode, "deviation": deviation, - }, "rank") -} - -func (c *Client) GetFactorValue(ctx context.Context, factorname, stockcode string, deviation int) (any, error) { - return c.postField(ctx, "/api/ext/get_factor_value", map[string]any{ - "factorname": factorname, "stockcode": stockcode, "deviation": deviation, - }, "value") -} - -func (c *Client) GetFactorRank(ctx context.Context, factorname, stockcode string, deviation int) (any, error) { - return c.postField(ctx, "/api/ext/get_factor_rank", map[string]any{ - "factorname": factorname, "stockcode": stockcode, "deviation": deviation, - }, "rank") -} diff --git a/go-client/sdk/sys.go b/go-client/sdk/sys.go deleted file mode 100644 index e7d3f03..0000000 --- a/go-client/sdk/sys.go +++ /dev/null @@ -1,30 +0,0 @@ -package sdk - -import "context" - -type PythonVersion struct { - PythonVersion string `json:"python_version"` - PythonVersionInfo struct { - Major int `json:"major"` - Minor int `json:"minor"` - Micro int `json:"micro"` - ReleaseLevel string `json:"releaselevel"` - Serial int `json:"serial"` - } `json:"python_version_info"` -} - -func (c *Client) PythonVersion(ctx context.Context) (*PythonVersion, error) { - var out PythonVersion - if err := c.get(ctx, "/api/sys/python_version", &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) Shutdown(ctx context.Context) (map[string]any, error) { - var out map[string]any - if err := c.post(ctx, "/api/sys/shutdown", map[string]any{}, &out); err != nil { - return nil, err - } - return out, nil -} diff --git a/go-client/sdk/trade.go b/go-client/sdk/trade.go deleted file mode 100644 index 7728adb..0000000 --- a/go-client/sdk/trade.go +++ /dev/null @@ -1,252 +0,0 @@ -package sdk - -import "context" - -const ( - OpBuy = 23 - OpSell = 24 - OrderTypeVolume = 1101 - PrTypeLatest = 5 - QuickTradeNow = 2 -) - -type PassorderRequest struct { - OpType int `json:"opType"` - OrderType int `json:"orderType,omitempty"` - Stock string `json:"stock"` - PrType int `json:"prType,omitempty"` - Price float64 `json:"price"` - Volume int `json:"volume"` - QuickTrade int `json:"quickTrade,omitempty"` - StrategyName string `json:"strategyName,omitempty"` -} - -func (c *Client) Passorder(ctx context.Context, req PassorderRequest) (*OrderRefResult, error) { - var out OrderRefResult - if err := c.post(ctx, "/api/trade/passorder", req, &out); err != nil { - return nil, err - } - return &out, nil -} - -// PassorderLatest 按最新价下单,不附加策略订单号。 -func (c *Client) PassorderLatest(ctx context.Context, side int, stock string, volume int) (*OrderRefResult, error) { - return c.PassorderLatestTagged(ctx, side, stock, volume, "") -} - -// PassorderLatestTagged 使用 strategyName 将本地唯一订单号传给 QMT。 -func (c *Client) PassorderLatestTagged(ctx context.Context, side int, stock string, volume int, orderID string) (*OrderRefResult, error) { - return c.Passorder(ctx, PassorderRequest{ - OpType: side, - OrderType: OrderTypeVolume, - Stock: stock, - PrType: PrTypeLatest, - Price: -1, - Volume: volume, - QuickTrade: QuickTradeNow, - StrategyName: orderID, - }) -} - -type AlgoPassorderRequest struct { - OpType int `json:"opType"` - OrderType int `json:"orderType,omitempty"` - Stock string `json:"stock"` - PrType int `json:"prType"` - Price float64 `json:"price"` - Volume int `json:"volume"` - StrategyName string `json:"strategyName,omitempty"` - QuickTrade int `json:"quickTrade,omitempty"` - UserOrderID string `json:"userOrderId,omitempty"` - UserOrderParam map[string]any `json:"userOrderParam,omitempty"` -} - -func (c *Client) AlgoPassorder(ctx context.Context, req AlgoPassorderRequest) (*OrderRefResult, error) { - var out OrderRefResult - if err := c.post(ctx, "/api/trade/algo_passorder", req, &out); err != nil { - return nil, err - } - return &out, nil -} - -type SmartAlgoPassorderRequest struct { - OpType int `json:"opType"` - OrderType int `json:"orderType,omitempty"` - Stock string `json:"stock"` - PrType int `json:"prType"` - Price float64 `json:"price"` - Volume int `json:"volume"` - SmartAlgoType string `json:"smartAlgoType"` - LimitOverRate int `json:"limitOverRate"` - MinAmountPerOrder int `json:"minAmountPerOrder"` - StartTime string `json:"startTime,omitempty"` - EndTime string `json:"endTime,omitempty"` -} - -func (c *Client) SmartAlgoPassorder(ctx context.Context, req SmartAlgoPassorderRequest) (*OrderRefResult, error) { - var out OrderRefResult - if err := c.post(ctx, "/api/trade/smart_algo_passorder", req, &out); err != nil { - return nil, err - } - return &out, nil -} - -type StyleOrderResult struct { - Status string `json:"status"` - Action string `json:"action"` - Stock string `json:"stock"` -} - -func (c *Client) styleOrder(ctx context.Context, path string, body map[string]any) (*StyleOrderResult, error) { - var out StyleOrderResult - if err := c.post(ctx, path, body, &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) OrderLots(ctx context.Context, stock string, lots int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/order_lots", map[string]any{"stock": stock, "lots": lots, "style": style, "price": price}) -} - -func (c *Client) OrderValue(ctx context.Context, stock string, value float64, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/order_value", map[string]any{"stock": stock, "value": value, "style": style, "price": price}) -} - -func (c *Client) OrderPercent(ctx context.Context, stock string, percent float64, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/order_percent", map[string]any{"stock": stock, "percent": percent, "style": style, "price": price}) -} - -func (c *Client) OrderTargetValue(ctx context.Context, stock string, tarValue float64, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/order_target_value", map[string]any{"stock": stock, "tar_value": tarValue, "style": style, "price": price}) -} - -func (c *Client) OrderTargetPercent(ctx context.Context, stock string, tarPercent float64, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/order_target_percent", map[string]any{"stock": stock, "tar_percent": tarPercent, "style": style, "price": price}) -} - -func (c *Client) OrderShares(ctx context.Context, stock string, shares int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/order_shares", map[string]any{"stock": stock, "shares": shares, "style": style, "price": price}) -} - -func (c *Client) FuturesBuyOpen(ctx context.Context, stock string, amount int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/futures/buy_open", map[string]any{"stock": stock, "amount": amount, "style": style, "price": price}) -} -func (c *Client) FuturesBuyCloseTdayFirst(ctx context.Context, stock string, amount int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/futures/buy_close_tdayfirst", map[string]any{"stock": stock, "amount": amount, "style": style, "price": price}) -} -func (c *Client) FuturesBuyCloseYdayFirst(ctx context.Context, stock string, amount int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/futures/buy_close_ydayfirst", map[string]any{"stock": stock, "amount": amount, "style": style, "price": price}) -} -func (c *Client) FuturesSellOpen(ctx context.Context, stock string, amount int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/futures/sell_open", map[string]any{"stock": stock, "amount": amount, "style": style, "price": price}) -} -func (c *Client) FuturesSellCloseTdayFirst(ctx context.Context, stock string, amount int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/futures/sell_close_tdayfirst", map[string]any{"stock": stock, "amount": amount, "style": style, "price": price}) -} -func (c *Client) FuturesSellCloseYdayFirst(ctx context.Context, stock string, amount int, style string, price float64) (*StyleOrderResult, error) { - return c.styleOrder(ctx, "/api/trade/futures/sell_close_ydayfirst", map[string]any{"stock": stock, "amount": amount, "style": style, "price": price}) -} - -type TaskResult struct { - Status string `json:"status"` - TaskID any `json:"taskId"` -} - -func (c *Client) CancelTask(ctx context.Context, taskID string) (*TaskResult, error) { - return c.task(ctx, "/api/trade/cancel_task", taskID) -} -func (c *Client) PauseTask(ctx context.Context, taskID string) (*TaskResult, error) { - return c.task(ctx, "/api/trade/pause_task", taskID) -} -func (c *Client) ResumeTask(ctx context.Context, taskID string) (*TaskResult, error) { - return c.task(ctx, "/api/trade/resume_task", taskID) -} - -func (c *Client) task(ctx context.Context, path, taskID string) (*TaskResult, error) { - var out TaskResult - if err := c.post(ctx, path, map[string]any{"taskId": taskID, "accountType": c.accountType}, &out); err != nil { - return nil, err - } - return &out, nil -} - -func (c *Client) DoOrder(ctx context.Context) (map[string]any, error) { - var out map[string]any - if err := c.post(ctx, "/api/trade/do_order", nil, &out); err != nil { - return nil, err - } - return out, nil -} - -func (c *Client) TradeDetailData(ctx context.Context, datatype string) ([]map[string]string, error) { - var out struct { - Data []map[string]string `json:"data"` - } - if err := c.post(ctx, "/api/trade/trade_detail_data", map[string]any{"account": c.accountType, "datatype": datatype}, &out); err != nil { - return nil, err - } - if out.Data == nil { - return []map[string]string{}, nil - } - return out.Data, nil -} - -func (c *Client) ValueByOrderID(ctx context.Context, orderID, datatype string) (map[string]string, error) { - var out struct { - Data map[string]string `json:"data"` - } - body := map[string]any{"orderId": orderID, "accountType": c.accountType, "datatype": datatype} - if err := c.post(ctx, "/api/trade/value_by_order_id", body, &out); err != nil { - return nil, err - } - return out.Data, nil -} - -func (c *Client) LastOrderID(ctx context.Context, datatype string) (any, error) { - var out struct { - LastOrderID any `json:"last_order_id"` - } - if err := c.post(ctx, "/api/trade/last_order_id", map[string]any{"account": c.accountType, "datatype": datatype}, &out); err != nil { - return nil, err - } - return out.LastOrderID, nil -} - -func (c *Client) CanCancelOrder(ctx context.Context, orderID string) (any, error) { - var out struct { - CanCancel any `json:"can_cancel"` - } - if err := c.post(ctx, "/api/trade/can_cancel_order", map[string]any{"orderId": orderID, "accountType": c.accountType}, &out); err != nil { - return nil, err - } - return out.CanCancel, nil -} - -func (c *Client) contractList(ctx context.Context, path string) ([]map[string]string, error) { - var out struct { - Data []map[string]string `json:"data"` - } - if err := c.post(ctx, path, nil, &out); err != nil { - return nil, err - } - return out.Data, nil -} - -func (c *Client) DebtContract(ctx context.Context) ([]map[string]string, error) { - return c.contractList(ctx, "/api/trade/debt_contract") -} -func (c *Client) AssureContract(ctx context.Context) ([]map[string]string, error) { - return c.contractList(ctx, "/api/trade/assure_contract") -} -func (c *Client) EnableShortContract(ctx context.Context) ([]map[string]string, error) { - return c.contractList(ctx, "/api/trade/enable_short_contract") -} - -func (c *Client) IPOData(ctx context.Context, typ string) (any, error) { - return c.postField(ctx, "/api/trade/ipo_data", map[string]any{"type": typ}, "data") -} - -func (c *Client) NewPurchaseLimit(ctx context.Context) (any, error) { - return c.postField(ctx, "/api/trade/new_purchase_limit", nil, "data") -} diff --git a/py-client/__pycache__/main.cpython-311.pyc b/py-client/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bac70a41d675fd79d291190f59754466188b9488 GIT binary patch literal 6151 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 tuple[GlobalConfig, AccountConfig]: + """加载公共配置以及当前主机对应的账户配置。 + + Args: + etc_dir: 配置文件目录,其中必须包含 ``_global.yaml``;为空时 + 默认使用 py-client 下的 ``etc`` 目录。 + hostname: 指定要加载的主机名;为空时使用当前计算机名。 + + Returns: + 由全局配置和账户配置组成的二元组。 + + Raises: + ValueError: 配置缺失、格式错误或策略参数不合法。 + """ + global global_config, account_config + + root = Path(etc_dir) if etc_dir is not None else Path(__file__).parent.parent / "etc" + raw = _yaml(root / "_global.yaml") + + # 将原始字典转换为带类型的信号配置,方便业务代码使用属性访问。 + signals = { + key: SignalConfig(**(value or {})) + for key, value in (raw.get("signals") or {}).items() + } + values = { + key: raw.get(key, "") + for key in ("qmt_base_url", "qmt_token", "api_host", "qmt_data_dir") + } + + current = hostname or socket.gethostname() + hosts = raw.get("hosts") or {} + account_file = next( + ( + value + for key, value in hosts.items() + if key.strip().lower() == current.strip().lower() + ), + "", + ) + + # QMT 地址、外部 API 地址和数据目录是启动策略的必要参数。 + if ( + not values["qmt_base_url"] + or not values["api_host"] + or values["qmt_data_dir"] == "." + ): + raise ValueError("Global 配置缺少必要参数") + + if not account_file: + raise ValueError(f'_global.yaml 未配置计算机 "{current}"') + if not Path(account_file).suffix: + account_file += ".yaml" + + global_config = GlobalConfig(**values, hosts=hosts, signals=signals) + + # 策略状态文件写入该目录,启动时提前确保目录存在。 + Path(global_config.qmt_data_dir).mkdir(parents=True, exist_ok=True) + + account_config = AccountConfig(**_yaml(root / account_file)) + if account_config.buy_value <= 0 or account_config.grid_step_pct <= 0: + raise ValueError("buy_value、grid_step_pct 必须大于 0") + if not account_config.strategy.strip(): + raise ValueError("strategy 不能为空") + + # host_key 统一为小写,避免不同模块比较时受大小写影响。 + account_config.host_key = account_config.host_key.lower() + account_config.strategy = account_config.strategy.lower() + return global_config, account_config + + +def _yaml(path: Path) -> dict: + """读取 YAML 文件,并将空文件转换为空字典。""" + try: + with path.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + except (OSError, yaml.YAMLError) as exc: + raise ValueError(f"读取或解析配置 {path} 失败: {exc}") from exc diff --git a/py-client/config/__pycache__/__init__.cpython-311.pyc b/py-client/config/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbf86775636fe05320a2b8aed276a31c954de141 GIT binary patch literal 7650 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 literal 0 HcmV?d00001 diff --git a/py-client/etc/_global.yaml b/py-client/etc/_global.yaml new file mode 100644 index 0000000..14dcd85 --- /dev/null +++ b/py-client/etc/_global.yaml @@ -0,0 +1,11 @@ +qmt_base_url: http://127.0.0.1:10086 +qmt_token: QMTbyYanweidong +api_host: http://139.224.247.176:13499 +qmt_data_dir: D:/qmt_strategy_data +hosts: + DESKTOP-39H91QV: dev.yaml +signals: + dcm: {url: /a/dcm_signal, timezone: "*", gt_last_price_is_open: false} + morning: {url: /a/morning_signal, timezone: "9:30-10:30", gt_last_price_is_open: true} + tail: {url: /a/tail_signal, timezone: "14:30-14:55", gt_last_price_is_open: false} + arbitrage: {url: /a/arbitrage_signal, timezone: "*", gt_last_price_is_open: false} diff --git a/py-client/etc/dev.yaml b/py-client/etc/dev.yaml new file mode 100644 index 0000000..6d9f9e8 --- /dev/null +++ b/py-client/etc/dev.yaml @@ -0,0 +1,11 @@ +account_id: 86037237 +host_key: dev +buy_value: 5000 +min_cash_ratio: 0.10 +loss_trigger_pct: -30 +grid_step_pct: 1 +min_profit_pct: 2 +strategy: trend +enable_loss_add_position: True +excluded_codes: + - "00000.SZ" diff --git a/py-client/libs/__init__.py b/py-client/libs/__init__.py new file mode 100644 index 0000000..aeba29b --- /dev/null +++ b/py-client/libs/__init__.py @@ -0,0 +1,5 @@ +from .calc import calc_buy_volume, trading_time +from .market import market_allow_open, status +from .signal import SignalItem, SignalResult, fetch_signal, init_signals + +__all__ = ["calc_buy_volume", "trading_time", "market_allow_open", "status", "SignalItem", "SignalResult", "fetch_signal", "init_signals"] diff --git a/py-client/libs/__pycache__/__init__.cpython-311.pyc b/py-client/libs/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cc46b6ca53760fa4c1f630a0e207a3cf93dafb9 GIT binary patch literal 522 zcmaiwy-ve06orrT-!v7_x5z*lSP>%vAu-U+s$`{&DFHhvi394!Bd{ScvLN0jV`WAr zwo2VPA-6?cDq&xJx<}T%J~|(rP6z;RUoUsZ1o$@M?^v5=aR%TODN-UJ2_@2^7BSiq zwsfc?UFxp1Ej;N{Uj{UgE!vVH4GG3bop(apsoO{F{SW}LrT0dO7_nhJWsilZCD)NE zlQhf6Ol6YCmOhY4dCwI~geaa^G2yxPDwU|ZiX9#HvvHn?3&o|5*8MfFYN2#=!`0}P zRjYR$WqGDHqKbpk22eT%u7PLZ8w3U|1Cz&99BIcG+zx1O?Poivz4a%%7HFRtiwFL~|F zi(R`6YpJf4RikX!>6qCMYf5zngt=j-Qw06_TB?-5K+vYkuNC@lzvSMRq)DAWygbgi z=iGD8Irn_$yf2%Y+yDT7y!p|YF&^MADpZ$P*=#ES@CjhRNQVR{kU$DTROWOp!7Drh z%xi!JO^6BDF)b(#ENUWlY9baiXH3M>w4g{#=hB>5(8QRC-P3~N!g2`wo-z&O=ZX?m zP0fraw4y+%9~B)j-7rXzPa0!Ik!WU`B%`RB9{@N2fCqr>E<{tzK(6j^XDtB0Qcc8Y znBD`zVJ-wFy!u*6k08OYVgtF18(+|;>u&QG78N2|^@%h{Ie%yr(ATe2wIJ==S+cfRBE1D_ta z(si}Vl6x$fg?zB!UG#kAv1R+rl9fBct>A5GS-Si3ug88q_S5lSj$20*>xi<- z=JgOC4gX!_eTdJPcqP1QC0IAwTPs}T5wenQhIQ=esQO`|(bJ@RBCwt}`n9Csg_7JTeU(*V8T1QPQR<+n!-T9^TP5KO<&*v-UZkOh6{QdoR zrQGuUA1`fucCB=Ea-;C!#>`^r=ECL&7u?LOj|`;(Y)2OZIN-ZKb$#=TynVOstoELO z&-Su0Iaj(|D8D6=RrmSD)#WSs|rWQ%WlaWgCiE3)Z2LR2~{xyc|)?SdB zviwwLGPB;YC)+#wwk7RfcY6M%ay4f=-rqN~Z^!D-nmxm~-|r?gU-lO$3JKLb&6=(# z3>t>crkVYvRkO&)lO}61mJMmAw`iNNW_&2UkT7ss*Ipu>06-f#^(_Ew@d%-H=&&C3 zt;1fc@!JwPbP{DxZvo}G9-BXcx;Ye>f1Vx$@&k+&QjF~;k{7k-2DZS9+HbWocJ~R! J{*$mr{R1A-#WMf^ literal 0 HcmV?d00001 diff --git a/py-client/libs/__pycache__/grid_take_profit.cpython-311.pyc b/py-client/libs/__pycache__/grid_take_profit.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e8bb89328af68a9662bfc1743059280c69c3fa6 GIT binary patch literal 5131 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 HvOE3{7&-6@L3iQcF^bCBxr-HNNKsz? zcz0-7DiyPU4k}<23W}m^jiNlHu3QJGZe@x96?&R_qr#;x)0)XEE0wfra7g9oAOo;-s zMMFwSNJ6q9=arNq02V}eACR&N$n#JHcr5NO_7DIb?;DWvv^~A6fP7!;=|v}yihanB zeJFr?P!M}FKGeIaq*T<0{n+kV}J4*t9)He3k_%UEe2?$bR5|EIRu#6QXjsq~u!!4<8 zdATLGEg!cOZh2BYSj7tRGD`&P$AQm$4+Vh7>_fPuf;fO>tY*ZQ+rbLz;q%o5MEs7L z$Yl$<@gsus4mhgCE$tRwa^!RoVMi|LdF;ptTWOPp!BGv}B3jxgTKJLR^dX$l%LdUh zdYTl?C8w9*^t_fUpj=ufMKd1hb$pt}7SS|E)wFyOl?^ryXxiFC zqH?Gyhw5{+a9yfJYti3FH|I9Oo6<&fBl;-1HP;AlNsVYD`gxT8Tzhhs{`6;bN~2R+ zb4uGCeWzY-+@Pbe=4i|wjaB38leK{wuAR0A!&Ht@Io9Nw$}#iw{`VY>pyjVekd*RZ zFIJAij0m^~tDkuepu~?=EaSidlyF=jAr<79^Ks1dBp{+X0T(kLVykQrlM&nv632%^ zibOXYZ@GkYf|1$h#z-f+qmxX%$>ESI5c3Q(S)9!f zQ$tMgc)i6&4A$;(eS?j@xvv;ptosK8`C5Nj z=r2BQ0a(JM*zTTpS?!1Jhq7<{uS?IttBy}E<+S;tMI5hHp3};v;dn_dkBepEcuY*n zW+AZlxV^Zcia;&d< z(D$JN@VIk+EQQ9Gr5}(h&lu#=or4DehEfLby(s5v4k~c7e^vxo5mv-XSHKYaSNv4? zweTD9{y+jE0!Ma4;82f9ju`z8r2+mt9GDAZ{9P}^Lah3qn#e8XEc_@QPdR}X% zwsE7F)(vYS`qG1u&3cCug9c{Y_UPMM#qT9@me@-Vq!T#QoMx=4A7RI!KQ~3I*W;;@Evd(L_B7gtun&Yx3~u*fMn{6 z<^}elY}ODLzEvmi>h{d<0su?<*5CPlc7v}|bz*&ysuNV5`12)u_;RDaIXrF;kGqem z*Pg2V>lbU@raEM+L)B}$`~(^U|2*}NQ(GrLKl8;I`n~v**v{qT_T^;r^0a+Ry-LoM!LvP!mw>J`hoBr$c=5#}Ep1W$FySg>}@6msaK8eue zbTc+%$7X0sqdMBr=ePBFnlmZ6)6^GjeX;4kYy0m~`7S?NN6`(VxR~x-a{z!rj1#@Z z(UXN0u>uSGe1ZC_NbAT!%H8~k<4szTD^f+c1LjY(5MvgR0fHF>piBzCot(wVHuSa29&xk8S# z`D2+A0Km$Qwc18lU-gH(gKyb`k)6Tw+k@wugBR?<3oQ_ptD>ua!VV>9;2sU!TbF5efhIq)lZYDTj&~e^~W485y2d0YzsR6&LtFECX1z8DOtjC*&cWZe- z^tVJ&xF)pVX#Jc&|BCD95{UlQH`)_?e6GN&>(Tl-fBqHMow?lACksnL3z)85bo-3~ ox8E$g{Z_^8kI=n}&|fDT*FIfpfg<#8-e{b)$FB47v5JT2|BW;NX#fBK literal 0 HcmV?d00001 diff --git a/py-client/libs/calc.py b/py-client/libs/calc.py new file mode 100644 index 0000000..34d53a3 --- /dev/null +++ b/py-client/libs/calc.py @@ -0,0 +1,32 @@ +from datetime import datetime, time +from math import floor + + +def trading_time(now: datetime) -> bool: + if now.weekday() >= 5: return False + return time(9, 30) <= now.time() <= time(11, 30) or time(13) <= now.time() <= time(15) + + +def calc_buy_volume(price: float, buy_value: float) -> int: + if price <= 0 or buy_value <= 0: return 0 + return max(1, floor(buy_value / (price * 100))) * 100 + +def calculate_min_profit_rate(price: float, profit_mult: int) -> float: + """ + 根据价格返回最小利润率 + + Args: + price: 股票价格 + profit_mult: 利润倍数配置 + + Returns: + float: 最小利润率(百分比) + """ + if price >= 300: + return 3 * profit_mult # 3% + if price >= 200: + return 5 * profit_mult # 5% + elif price >= 100: + return 7 * profit_mult # 7% + else: + return 9 * profit_mult # 9% \ No newline at end of file diff --git a/py-client/libs/grid_take_profit.py b/py-client/libs/grid_take_profit.py new file mode 100644 index 0000000..60de290 --- /dev/null +++ b/py-client/libs/grid_take_profit.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""网格回撤止盈状态机。 + +该模块只负责记录每个持仓的最高盈利网格,并判断当前盈亏率是否从 +峰值网格回撤。它不包含下单逻辑,由主策略和 Upmax 根据返回的状态决定是否卖出。 +""" + +from dataclasses import dataclass +from enum import Enum +import math +import threading + + +class GridState(str, Enum): + """单次盈亏率观察后的网格状态。""" + + ARMED = "armed" # 首次记录该持仓的峰值网格 + RAISED = "raised" # 盈利继续上升,峰值网格已抬高 + RETREAT = "retreat" # 从峰值网格回撤,应由调用方执行止盈 + STEADY = "steady" # 仍处于当前峰值网格,继续持有 + + +@dataclass(frozen=True) +class GridObservation: + """一次网格观察的不可变结果。""" + + state: GridState + current_grid: int # 当前盈亏率所处的网格 + peak_grid: int # 该持仓自观察以来的最高网格 + + +class GridTrailingTracker: + """按持仓键隔离、线程安全的峰值网格跟踪器。""" + + def __init__(self, step: float = 1.0): + """ + Args: + step: 单个网格的盈亏率跨度(百分点),必须大于 0。 + """ + if step <= 0: + raise ValueError("grid step must be positive") + self._step = step + # key 由调用方组成“账户 + 股票代码”,防止多账户状态串扰。 + self._peaks: dict[str, int] = {} + # 主策略和回调线程可能并发访问,所有峰值读写均在同一把锁内。 + self._lock = threading.Lock() + + def observe(self, position_key: str, pnl_rate: float) -> GridObservation: + """记录当前盈亏率,并返回相对于历史峰值的状态。""" + # floor 保证负数盈亏率也按完整网格向下归档。 + current_grid = math.floor(pnl_rate / self._step) + with self._lock: + peak_grid = self._peaks.get(position_key) + + # 第一次看到该持仓:建立基准,不触发止盈。 + if peak_grid is None: + self._peaks[position_key] = current_grid + return GridObservation(GridState.ARMED, current_grid, current_grid) + + # 进入更高网格:更新峰值,继续持有。 + if current_grid > peak_grid: + self._peaks[position_key] = current_grid + return GridObservation(GridState.RAISED, current_grid, current_grid) + + # 跌破峰值网格:报告回撤,但保留峰值直到卖出成功后 clear。 + if current_grid < peak_grid: + return GridObservation(GridState.RETREAT, current_grid, peak_grid) + + return GridObservation(GridState.STEADY, current_grid, peak_grid) + + def clear(self, position_key: str) -> None: + """持仓卖出成功后删除峰值,使下次建仓从新状态开始。""" + with self._lock: + self._peaks.pop(position_key, None) + + def retain(self, position_keys) -> None: + """删除已不在券商持仓中的峰值,避免同代码重新开仓继承旧状态。""" + active = set(position_keys) + with self._lock: + self._peaks = {key: value for key, value in self._peaks.items() if key in active} diff --git a/py-client/libs/http.py b/py-client/libs/http.py new file mode 100644 index 0000000..4574114 --- /dev/null +++ b/py-client/libs/http.py @@ -0,0 +1,8 @@ +import json +from urllib.request import Request, urlopen + + +def get_json(url: str, timeout: float = 5.0): + request = Request(url, headers={"Accept": "application/json", "User-Agent": "big-qmt-python/1"}) + with urlopen(request, timeout=timeout) as response: + return json.load(response) diff --git a/py-client/libs/market.py b/py-client/libs/market.py new file mode 100644 index 0000000..21192e9 --- /dev/null +++ b/py-client/libs/market.py @@ -0,0 +1,24 @@ +import logging +import secrets + +from .http import get_json + +API_HOST = "http://139.224.247.176:13499" +MARKET_URL, PERIOD, HTTP_TIMEOUT = "/a/market", "60m", 5.0 + + +def status(payload) -> str: + value = payload.get("data", payload) if isinstance(payload, dict) else payload + if isinstance(value, list): value = value[-1] if value else None + if isinstance(value, dict): value = value.get("action", value.get("status", value.get("signal"))) + result = str(value).strip().upper() + return result if result in {"UP", "DOWN", "NEUTRAL"} else "UNKNOWN" + + +def market_allow_open(api_host: str = API_HOST) -> bool: + url = f"{api_host}{MARKET_URL}?period={PERIOD}&t={secrets.token_urlsafe(12)}" + try: result = status(get_json(url, HTTP_TIMEOUT)) + except Exception as exc: + logging.error("获取大盘指数失败: %s %s", url, exc); return False + logging.info("大盘信号: url=%s status=%s", url, result) + return result == "UP" diff --git a/py-client/libs/signal.py b/py-client/libs/signal.py new file mode 100644 index 0000000..aedbbe8 --- /dev/null +++ b/py-client/libs/signal.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass, field +import secrets + +from .http import get_json + + +@dataclass +class SignalItem: + signal_key: str = ""; code: str = ""; name: str = ""; desc: str = ""; last_close: float = 0 + tech_indicator: dict[str, float] = field(default_factory=dict) + +@dataclass +class SignalResult: + code: str = ""; total: int = 0; updated: str = ""; data: dict[str, SignalItem] = field(default_factory=dict); message: str = "" + + +def fetch_signal(api_host: str, sub_url: str, timeout: float = 5.0) -> SignalResult: + url = f"{api_host}{sub_url}?t={secrets.token_urlsafe(12)}" + raw = get_json(url, timeout) + items = {code: SignalItem(**item) for code, item in (raw.get("data") or {}).items()} + return SignalResult(raw.get("code", ""), raw.get("total", 0), raw.get("updated", ""), items, raw.get("message", "")) + + +def init_signals(global_config, allow: list[str]) -> list[SignalItem]: + result = [] + for key, cfg in global_config.signals.items(): + if key in allow: + for item in fetch_signal(global_config.api_host, cfg.url).data.values(): item.signal_key = key; result.append(item) + return result diff --git a/py-client/main.py b/py-client/main.py new file mode 100644 index 0000000..beea784 --- /dev/null +++ b/py-client/main.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging as log +import os +import sys +import time +import config +from dataclasses import dataclass +import yaml + +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +GLOBAL_CONFIG_PATH = os.path.join(PROJECT_ROOT, "etc", "_global.yaml") +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + +from sdk import Client +from strategy.trend.boot import StartTrend + + +@dataclass(frozen=True) +class StrategyDefinition: + mutex_scope: str + start_strategy: object + + +STRATEGIES = { + "trend": StrategyDefinition("Trend", StartTrend), +} + +def require_windows() -> bool: + return os.name == "nt" + +def check_single_instance(project_root: str) -> bool: + """使用 Windows 命名互斥锁保证单实例。""" + try: + import ctypes + + error_already_exists = 183 + invalid_handle_value = -1 + safe_path = project_root.replace(":", "_").replace("\\", "_") + mutex_name = f"Global\\QMT_System_{safe_path}" + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + handle = kernel32.CreateMutexW(None, True, mutex_name) + if not handle or handle == invalid_handle_value: + log.error(f"无法创建互斥锁,错误代码:{ctypes.get_last_error()}") + return False + if ctypes.get_last_error() == error_already_exists: + log.error("程序已在运行中,无法启动多个实例") + kernel32.CloseHandle(handle) + return False + log.info(f"成功获取互斥锁:{mutex_name}") + return True + except Exception as exc: + log.error(f"单实例检测失败:{exc}", exc_info=True) + return False + + +def ping_api_host( + rpc_host: str, + retry_interval: float = 5.0, + connect_timeout: float = 3.0, +) -> None: + """循环检查 API 地址,连通后才返回。""" + client = Client(config.global_config.qmt_base_url, config.global_config.qmt_token, config.HTTP_TIMEOUT) + + while True: + try: + assets = client.assets() + log.info(f"API 服务已连通:{config.global_config.qmt_base_url}") + return + except: + log.warning( + f"API 服务未就绪:{config.global_config.qmt_base_url},{retry_interval:g} 秒后重试" + ) + time.sleep(retry_interval) + +def wait_for_any_key() -> None: + print("按任意键退出...", flush=True) + if os.name == "nt": + import msvcrt + + msvcrt.getch() + elif sys.stdin.isatty(): + sys.stdin.read(1) + + +def main() -> int: + try: + if not require_windows(): + log.error("本程序仅支持 Windows 环境运行") + return 1 + if not check_single_instance(PROJECT_ROOT): + return 1 + + config.load() + if config.global_config is None or config.account_config is None: + raise RuntimeError("配置尚未加载,请先调用 config.load()") + + ping_api_host(config.global_config.qmt_base_url) + + STRATEGIES[config.account_config.strategy].start_strategy() + return 0 + except (OSError, yaml.YAMLError, ValueError) as exc: + print(f"启动失败: {exc}", file=sys.stderr, flush=True) + wait_for_any_key() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/py-client/sdk/__init__.py b/py-client/sdk/__init__.py new file mode 100644 index 0000000..6ea2685 --- /dev/null +++ b/py-client/sdk/__init__.py @@ -0,0 +1,14 @@ +from .account import AccountMixin +from .client import Client as _HTTPClient +from .data import DataMixin +from .errors import APIError, BusinessError +from .misc import MiscMixin +from .models import * +from .trade import * + + +class Client(AccountMixin, DataMixin, TradeMixin, MiscMixin, _HTTPClient): + """big-qmt 同步 HTTP 客户端。""" + + +__all__ = ["Client", "APIError", "BusinessError", "OP_BUY", "OP_SELL", "ORDER_TYPE_VOLUME", "PR_TYPE_LATEST", "QUICK_TRADE_NOW"] diff --git a/py-client/sdk/__pycache__/__init__.cpython-311.pyc b/py-client/sdk/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..842d9ff5fb7ec023bcb3e0f8daf2412177cbb667 GIT binary patch literal 924 zcmZuuy>HV{5WiIp@Uv1$=pP??y}dx*3c-~~{igh51HLWHd_B~o2Y+A1qVx~ofDV@71UW>j>G(l4-5RCdeK*4c<7 zYIRm|K!KY;jb}j3fCPFl8k7U=jRCN4oO$47hz$D;F}iJ7&8K3qc#+hK}0^XQ4} zDV5k)av1=&{+*sSmF0-X0S1YdIVjS_h2WIyjV8{nta78##FkyJ^NGgtyuFOhYSYHM zjrz)>%}1LAeQwF0B4OqepY3$K}mmKkt+Y!uoj3LHjVhTCigkZ?LKA`eO zJULuh@)ZF3Dj}o?li6wXz{*ae2UoJw=)rV$9?r|Wl;!Bb#Vki(CuF)0XVuQy%WJPT aK3QMx{}R#PCn+YBYf2wZ^p+ux9P}@uF!=fa literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/account.cpython-311.pyc b/py-client/sdk/__pycache__/account.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5596a6fcb32caaf77d4a7ad054e97ac68a80a3ff GIT binary patch literal 5343 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 literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/client.cpython-311.pyc b/py-client/sdk/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..605885d0abf73cd279dea6d8c11050ef88b8e572 GIT binary patch literal 5675 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 XbMT|uvxT53mao6#{XaZmlxyJsZ*I#n literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/data.cpython-311.pyc b/py-client/sdk/__pycache__/data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc6d313669e3ce3c4e6c535f0cf90f3daedb1d99 GIT binary patch literal 17201 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 literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/models.cpython-311.pyc b/py-client/sdk/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd061601227960c773d72057463464e36c534631 GIT binary patch literal 8956 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 literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/trade.cpython-311.pyc b/py-client/sdk/__pycache__/trade.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d01233701372fd15081fea6dcbac1312c658f32 GIT binary patch literal 10098 zcmcIqTWl0pn*Qr_b^8kDV(t(KF@`1tPq+q#yWIvG+y>e z#IxJl%|xRKyILvP-OR2Pt%!k0puEIdjkLV1#N)B$kz2J7yVAZ4Z_N^ES9!_mKh>A& z3nrOB`RG6GbL;be|LvT=`m1HjY5@TL^dEOdzg`LO-}q1Nk+UD3p80EH+n>gsm^D6-;KJOs`Hl~X|7cL~4)l3PfrqMiVFtDa(mSF&8EBa>e8bR7UZ zU5`z!P?rqw)Qt@|E0~*!k1PAn^FN7g=%7dx6w(X2OP8s13;^g(DyJmHP2DL_Jk*oI zudGUeQcbH3(=!z65E9uG< zD68nI6ez3d>J%uAv@r$B8oDM0%38WM1-%5efM4M8e?4UbRpfuCw z0C;y%vdd>t$NhwK^o}`OPU&yy> zC`rmggu0UMAwuP3)excXWc3gklH1_@dX(ht+S_%>I_6H+C2Nwk2`nyC*@`n7RA(b%HJLMAw6RD8+5E+EWS53Y1QO+ooFlwa&NvB&CZAe zz!ao6zB%KY^*!n14L!V}=VLj&dFRYVzUjcDTaVZALmf{pa7E|mhj{3N3_wCUOELf< z=^R#@I!Fejnq7kYrt{<0J8RMnjl6#6^o~39^ZkDl`oDC;>Q57&B>2W(JZhS2IK0qs zm{%Wuj^YgF3>+U%PJ#B>3iJZu3&nC#>8=d$AU{6XjKN@piFRe~?OG)mmqDo~tlTRe7k{@7K1?R~CNJ{0DW z*xa#+g<})^-~`)_{GyTM9>iZ5%+(*kubFpc0A`+R04kY?TlPuHcdG#&ROkK|s674Q zhaZ0M%Ir;KZe{o7Hs58n=h{V|+Sh%dx65ZOznK01!s#AgpvP*s)Z5W|UhTPjy3MEh zyRUfN0yVY+6}+J3)wY(0sDVrgH5K@=CIComUjnkMG*%*V&9?qEtp4;@pZw}`I_;Y| z^vC^e{~qCeSO2Jl_?R)*U@kP6yxOz@lya;Ai0FV4f4+A~0I8Ie%De^fCpI_?Tni6J z)x6x<8UVOQY-RC`eW0;n!$1M#RAhPa_{0VWx5ZtJ$RsM=6wH_p6ZaGRXfMC={#?tz zLdyW(HSo&0+UnrS7}uDo7H>%K#t^Y0jv6bCO>A}WRcQd?l;!SG&$r4q3O}XsU})~( z=)%EKzIW8-uF4@Zl$TKY6HCf0DN&3cuB1#!(oJ1SPe?A4q3jpxh|Yd8Ys7IwD%3l%&Vhil14U|h^U9m}B=LFE z^tzcHk2cKu9v^<<;{k>DYkb?_Ttjf7A;_zPj8L>H~0W} zUH-%xhs1503`b4Fl9Aq%*kSaESrZo4d!!hEV(ZNXFPxlM<6yq9@&;rbw>%|0Uf2Aw zF7DU)aF|C-elw8)xGkN*$k0YQ$TNWBo#Z<~GXRG=$!o)5Z5a+i=(fO*qD4DVC{cqF z6zXht#R7@7WiW{LjSd{>;)(A06W#pc2p^5|iCaAJQ3l`)5rMLU_z{Pb-H5}Mk6~rvAztCY|kb{1ecnE$4!f80`q4AyeGnA z@eIH%=`=FlN; zab&1OHCEYRIOip{lp*5m-t2&-XVKR;@9X1NZt`2VGXST^dE~i^T*j)02n4Q^8wg9g zq#-O=WdKfUuMZYy&?sUrr#W>9j8l8q=?c&^R+?KBL@Eu~f-Cui);wuk>`>-A6n;IK z0r*I2L&n=l0IOcokJSJPVx^N&gs4**Lx`|K<≀A4coSRL2<6m~L1eyA~Q#EHlpSr{^kMVKFO&cN~GJcj^z{*bo2+C=x2SLFq z18}q_i%C9g02D$GF8vw2=&X&Q=O5wsBuS_U#z}Xj0JPeH%uw8nGu>#hJlZ&37j0BL z*`t&eG(*vd9frD!(ZSu>ao7&SRjR#IVwJERO32G3Cfp8;mwyFo?lWH`kNA5|i+i`l7%g&A~vJk1c8AaEN(oQ_I8|YW3U?x06zYfnH<$JrH z$b4^Cw(RVxe1`l9n_rDv35KJFu9{Sv911bRWS3Ds0sMOB$#;`;gC$cE%??p5F%EA+ z&(Z3qV0QCt;~zHj_UrsekPi$ZwadQjdFf|}#KG0%PvO}wQ$TXuE->|HmCuPafB z7cj87h=GzhiJdRvrDPypNOO9$XSV4Nd-&P+`B9zUph)eqUw@^H<)!@-7|YLj8Dsf@ zcqwL>fwcFIcZA%7Zc^MC+JA)OrsVgoBuT<(UKy$D3sT`8^?jYggCJ zP^&JR`@_tcm$0?CGt6FiUBa(3KV}xHa4@C=aC^CA3JYN}V1+}VV(ne1kgc{?>v^c$ zSNMB@$3y(RK(^c%=C_jH0ZdBbwAd$!LlRcc0QO!$-@R8vyqy=auG^6w5qSZD(VvwU zuslI664b-@B(eEy8i0jMJz;-R=CX z_E)}J57UkK_=~n%XZM!F>u7%)8O3ySxs9Xzg%0d_iJjb)03PJ+LnO?K4m31sX9Db; zkYr!{aeVfiQk@%+SnbgATqAzFfjOWl_}u*G#6KkX?vry{Pc3Xc#n+!IM>KEULoeC2 z)21;yP-@lFPOZv6)HMthtUs~YVf`X~tWw?7LSdC^rgo25RayJDs<8oI09dlz_oUwdVs(dTUmMw}{Wxem<0s{iKM@`@E{$@8new~NL7eaWl>##461CqEiVf74P ze*nG0{s8%3fc;m<{|K4^xJm}FxJ(rl6&yfB=qK`5-64dq2vo! zF}5#~L7B$M#S*YnRfChZ-l-w#by-#7PS8Qw7-~Dh%chY0+Tn#!*KFjGft&}nR+g<- z1btbrNDU4O=x5vht@B?y`B|MCABs!rB}A{wMl?)J1a?G<=PJzg+{6*bZ=w{ACr}E< z6WIMBIqkykGcN2t^B(a zoQaX5Ni8`1iX8{&!_oVr{78Un`hvzX0H+DQL4m$ptm{M|Th1epE$0!)mTm;HMI??c z5><5H5ZyO$;;tJNVqo5rycEbHJ^t~Yq!N&u9DXFIMbvC06uRpr?5_|xRA8&N&}*x( zoolzYlXxGAEw377F_gc@rZ)dx*A7>$@r3!3*N zt0w;sOCURiP{V##s$)fS-^J0Z1@qHwU-`fEKRP+r)Vk2r%D1$>+W2TSXoeAIdSR!Z zqGU_A`?t8=jgFvLG7DexWn<$s{VMWhgdc35Yd*Wse3oxL`-*qGMvrKNVO=#wqO1US z<$eDh?t7bK-xcTXFYJD%?VS(e_?;NUwz`cW~Yg#oHNIZoC8vSyVzw4Vd z`MW;;u5YfneWAI1vAJ`;x%0^(9_XEG?ptW?<6Ha6dB+5*;2jrSD%nIyQVL1bb=L*( z0B49N02m>iNv2RNDvlA-x`0e-OaNgwaM`lO3r*XEiA7n%n7mcerM%rl8jXh|l4k}?Ke{-V@$ zME^jI#aVDfGjuf^8V{Lvxc~bKc4nbf z8=muyEO&l0syY$Z!l-GgYPk%P zSq-YHMIupC!~CD2s^Vod6Zc$9P6*G<&>@J|RqU7e3EvPh{NWG7w2E1<#Sw=wA{``akQM`qTqhS zLA^i;xW(DAKnb|TA*etJxJ9ha)3n^mMM=wNxhQElz(q;ReO#2ZJjg{!%Ol)Tgb@gd z`@Pkc$DE9XBEt+_ljZ5YsGjM)W>s}xR0F;X7Z_gl8Q!6pxR(lD`4{{b^MOKtzOO!J z8_5Ya0|4mN4NQ<^T#_WEVJ$B%|FvrUuN$U3A6KOho}7by99G^rF?}-QX^}!w1}f8M MqW!xU^op(gKUef;o&W#< literal 0 HcmV?d00001 diff --git a/py-client/sdk/account.py b/py-client/sdk/account.py new file mode 100644 index 0000000..ccc02e5 --- /dev/null +++ b/py-client/sdk/account.py @@ -0,0 +1,33 @@ +from typing import Any + +from .models import Assets, Position + + +class AccountMixin: + account_type: str + + def _positions(self, path: str) -> tuple[list[str], list[Position]]: + raw = self._post(path, {"account": self.account_type}) or {} + return list(raw), [Position.from_dict(value, code) for code, value in raw.items()] + + def positions(self): return self._positions("/api/v2/positions") + def holding(self): return self._positions("/api/holding") + + def assets(self) -> Assets: + data = self._post("/api/v2/assets", {"account": self.account_type}) + return Assets(float(data.get("total", 0)), float(data.get("available", 0))) + + def total_money(self) -> float: return float(self._post("/api/money/total", {"account": self.account_type}).get("total_money", 0)) + def available_money(self) -> float: return float(self._post("/api/money/available", {"account": self.account_type}).get("available_money", 0)) + def buy(self, stock: str, price: float, volume: int, pr_type: int = 0): return self._order("/api/order/buy", stock, price, volume, pr_type) + def sell(self, stock: str, price: float, volume: int, pr_type: int = 0): return self._order("/api/order/sell", stock, price, volume, pr_type) + + def _order(self, path, stock, price, volume, pr_type): + body = {"stock": stock, "price": price, "volume": volume} + if pr_type: body["prType"] = pr_type + return self._post(path, body) + + def order_status_list(self): return self._post("/api/order/status", {"account": self.account_type}).get("orders", []) + def cancel_all(self): return self._post("/api/order/cancel_all", {"account": self.account_type}) + def cancel_by_rule(self, stock: str, volume: int): return self._post("/api/order/cancel_order", {"stock": stock, "volume": volume, "account": self.account_type}) + def deals(self): return self._post("/api/order/deal", {"account": self.account_type}).get("deals", []) diff --git a/py-client/sdk/client.py b/py-client/sdk/client.py new file mode 100644 index 0000000..8d9dfcb --- /dev/null +++ b/py-client/sdk/client.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import json +from dataclasses import asdict, is_dataclass +from typing import Any +from urllib.error import HTTPError +from urllib.request import Request, urlopen + +from .errors import APIError, BusinessError + + +def csv_join(items: list[str]) -> str: + return ",".join(item.strip() for item in items if item.strip()) + + +class Client: + def __init__(self, base_url: str, token: str, timeout: float = 15.0) -> None: + self.base_url = base_url.rstrip("/") + self.token = token + self.timeout = timeout if timeout > 0 else 15.0 + self.account_type = "stock" + + def set_account_type(self, account_type: str) -> "Client": + if account_type.strip(): + self.account_type = account_type + return self + + def _request(self, method: str, path: str, body: Any = None) -> Any: + data = None + headers = {"X-Token": self.token, "Accept": "application/json"} + if method != "GET": + if body is None: body = {} + if is_dataclass(body): body = asdict(body) + data = json.dumps(body, ensure_ascii=False).encode() + headers["Content-Type"] = "application/json" + request = Request(self.base_url + path, data=data, headers=headers, method=method) + try: + with urlopen(request, timeout=self.timeout) as response: + raw = response.read() + except HTTPError as exc: + raw = exc.read() + try: message = json.loads(raw).get("error", raw.decode(errors="replace")) + except (ValueError, AttributeError): message = raw.decode(errors="replace").strip() + raise APIError(exc.code, str(message)) from exc + if not raw: return None + try: return json.loads(raw) + except ValueError as exc: raise ValueError(f"invalid JSON from {path}: {raw[:512]!r}") from exc + + def _get(self, path: str) -> Any: return self._request("GET", path) + def _post(self, path: str, body: Any = None) -> Any: return self._request("POST", path, body) + + def _get_field(self, path: str, key: str) -> Any: + return self._get(path).get(key) + + def _post_field(self, path: str, body: Any, key: str) -> Any: + result = self._post(path, body) + if isinstance(result, dict) and result.get("error"): + raise BusinessError(result["error"]) + return result.get(key, result) if key and isinstance(result, dict) else result diff --git a/py-client/sdk/data.py b/py-client/sdk/data.py new file mode 100644 index 0000000..e454300 --- /dev/null +++ b/py-client/sdk/data.py @@ -0,0 +1,83 @@ +from dataclasses import asdict +from typing import Any + +from .client import csv_join +from .models import * + + +class DataMixin: + def _one(self, endpoint, arg, value, key): return self._post_field(f"/api/data/{endpoint}", {arg: value}, key) + + def stock_name(self, code): return self._one("stock_name", "stockcode", code, "name") + def open_date(self, code): return self._one("open_date", "stockcode", code, "open_date") + def last_volume(self, code): return self._one("last_volume", "stockcode", code, "last_volume") + def bar_timetag(self, index): return self._one("bar_timetag", "index", index, "timetag") + def tick_timetag(self): return self._get_field("/api/data/tick_timetag", "timetag") + def sector(self, sector, realtime): return self._post("/api/data/sector", {"sector": sector, "realtime": realtime}).get("stocks", []) + def industry(self, industry): return self._post("/api/data/industry", {"industry": industry}).get("stocks", []) + def stock_list_in_sector(self, name): return self._post("/api/data/stock_list_in_sector", {"sectorname": name}).get("stocks", []) + def weight_in_index(self, indexcode, stockcode): return self._post_field("/api/data/weight_in_index", locals_body(indexcode=indexcode, stockcode=stockcode), "weight") + def contract_multiplier(self, code): return self._one("contract_multiplier", "contractcode", code, "multiplier") + def risk_free_rate(self, index): return self._one("risk_free_rate", "index", index, "risk_free_rate") + def date_location(self, date): return self._one("date_location", "strdate", date, "location") + + def history_data(self, req: HistoryDataRequest): + return self._post_field("/api/data/history_data", {"len": req.length or 10, "period": req.period, "field": req.field, "dividend_type": req.dividend_type, "skip_paused": str(req.skip_paused).lower()}, "data") + def _market_body(self, req): return {"fields": csv_join(req.fields), "stock_code": csv_join(req.stocks), "start_time": req.start_time, "end_time": req.end_time, "period": req.period, "dividend_type": req.dividend_type, "count": req.count} + def market_data(self, req): return self._post_field("/api/data/market_data", self._market_body(req), "data") + def market_data_ex(self, req): return self._post_field("/api/data/market_data_ex", self._market_body(req), "data") + + def full_tick(self, stocks): + raw = self._post("/api/data/full_tick", {"stocks": stocks}) or {} + def number(data, *names): + for name in names: + try: return float(data[name]) + except (KeyError, TypeError, ValueError): pass + return 0.0 + return {code: Tick(number(value, "lastPrice", "last_price", "LastPrice"), number(value, "lastClose", "last_close", "LastClose"), value if isinstance(value, dict) else {}) for code, value in raw.items()} + + def divid_factors(self, code): return self._one("divid_factors", "stockcode", code, "factors") + def main_contract(self, code): return self._one("main_contract", "codemarket", code, "main_contract") + def timetag_to_datetime(self, timetag, format=""): + body = {"timetag": timetag} + if format: body["format"] = format + return self._post_field("/api/data/timetag_to_datetime", body, "datetime") + def total_share(self, code): return self._one("total_share", "stockcode", code, "total_share") + def trading_dates(self, stockcode, start_date, end_date, period, count=0): + body = locals_body(stockcode=stockcode, start_date=start_date, end_date=end_date, period=period) + if count: body["count"] = count + return self._post("/api/data/trading_dates", body).get("dates", []) + def svol(self, code): return self._one("svol", "stockcode", code, "svol") + def bvol(self, code): return self._one("bvol", "stockcode", code, "bvol") + def longhubang(self, stocks, start, end): return self._post_field("/api/data/longhubang", {"stock_list": csv_join(stocks), "startTime": start, "endTime": end}, "data") + def top10_share_holder(self, stocks, name, start, end): return self._post_field("/api/data/top10_share_holder", {"stock_list": csv_join(stocks), "data_name": name, "start_time": start, "end_time": end}, "data") + def option_detail(self, code): return self._one("option_detail", "optioncode", code, "detail") + def turnover_rate(self, stocks, start, end): return self._post_field("/api/data/turnover_rate", {"stock_list": csv_join(stocks), "startTime": start, "endTime": end}, "data") + def etf_info(self, code): return self._one("etf_info", "stockcode", code, "info") + def etf_iopv(self, code): return self._one("etf_iopv", "stockcode", code, "iopv") + def instrument_detail(self, code): return self._one("instrumentdetail", "stockcode", code, "detail") + def contract_expire_date(self, code): return self._one("contract_expire_date", "codemarket", code, "expire_date") + def option_undl_data(self, code): return self._one("option_undl_data", "undl_code_ref", code, "data") + + def financial_data(self, req): + return self._post_field("/api/data/financial_data", {"tabname": req.tabname, "colname": req.colname, "market": req.market, "code": req.code, "report_type": req.report_type, "barpos": req.barpos, "fieldList": csv_join(req.field_list), "stockList": csv_join(req.stock_list), "startDate": req.start_date, "endDate": req.end_date}, "data") + def factor_data(self, req): return self._post_field("/api/data/factor_data", {"fieldList": csv_join(req.field_list), "stockList": csv_join(req.stock_list), "stockCode": req.stock_code, "startDate": req.start_date, "endDate": req.end_date}, "data") + def his_st_data(self, code): return self._one("his_st_data", "stockCode", code, "data") + def his_index_data(self, index): return self._one("his_index_data", "index", index, "data") + def all_subscription(self): return self._get_field("/api/data/all_subscription", "subscriptions") + def option_list(self, code, dedate, opttype, available): return self._post_field("/api/data/option_list", {"undl_code": code, "dedate": dedate, "opttype": opttype, "isavailable": str(available).lower()}, "option_list") + def his_contract_list(self, market): return self._one("his_contract_list", "market", market, "contracts") + def option_iv(self, code): return self._one("option_iv", "optioncode", code, "iv") + def bsm_price(self, req): + prices = ",".join(str(v) for v in req.object_prices) if isinstance(req.object_prices, list) else req.object_prices + return self._post_field("/api/data/bsm_price", {"optionType": req.option_type, "objectPrices": prices, "strikePrice": req.strike_price, "riskFree": req.risk_free, "sigma": req.sigma, "days": req.days, "dividend": req.dividend}, "price") + def bsm_iv(self, req): return self._post_field("/api/data/bsm_iv", camel_request(req), "iv") + def local_data(self, req): return self._post_field("/api/data/local_data", {"stock_code": req.stock_code, "start_time": req.start_time, "end_time": req.end_time, "period": req.period, "divid_type": req.divid_type, "count": req.count}, "data") + def subscribe_quote(self, code, period, dividend_type): return self._post("/api/data/subscribe_quote", {"stock_code": code, "period": period, "dividend_type": dividend_type}) + def unsubscribe_quote(self, sub_id): return self._post("/api/data/unsubscribe_quote", {"sub_id": sub_id}) + + +def locals_body(**kwargs): return kwargs +def camel_request(req): + data = asdict(req) + return {"optionType": data["option_type"], "objectPrices": data["object_prices"], "strikePrice": data["strike_price"], "optionPrice": data["option_price"], "riskFree": data["risk_free"], "days": data["days"], "dividend": data["dividend"]} diff --git a/py-client/sdk/errors.py b/py-client/sdk/errors.py new file mode 100644 index 0000000..8d7dfdb --- /dev/null +++ b/py-client/sdk/errors.py @@ -0,0 +1,14 @@ +class APIError(RuntimeError): + def __init__(self, status_code: int, message: str = "") -> None: + self.status_code = status_code + self.message = message + text = f"qmt api: http {status_code}" + super().__init__(f"{text}: {message}" if message else text) + + @property + def unauthorized(self) -> bool: + return self.status_code == 401 + + +class BusinessError(RuntimeError): + pass diff --git a/py-client/sdk/misc.py b/py-client/sdk/misc.py new file mode 100644 index 0000000..e3c4969 --- /dev/null +++ b/py-client/sdk/misc.py @@ -0,0 +1,31 @@ +from typing import Any + + +class MiscMixin: + def context_period(self): return self._get_field("/api/context/period", "period") + def context_barpos(self): return self._get_field("/api/context/barpos", "barpos") + def context_time_tick_size(self): return self._get_field("/api/context/time_tick_size", "time_tick_size") + def context_stockcode(self): return self._get_field("/api/context/stockcode", "stockcode") + def context_dividend_type(self): return self._get_field("/api/context/dividend_type", "dividend_type") + def context_market(self): return self._get_field("/api/context/market", "market") + def context_do_back_test(self): return self._get_field("/api/context/do_back_test", "do_back_test") + def context_benchmark(self): return self._get_field("/api/context/benchmark", "benchmark") + def context_capital(self): return self._get_field("/api/context/capital", "capital") + def context_universe(self): + value = self._get_field("/api/context/universe", "universe") + if value is None: return [] + return [str(v) for v in value if str(v)] if isinstance(value, list) else [str(value)] + + def is_last_bar(self): return self._get_field("/api/check/is_last_bar", "is_last_bar") + def is_new_bar(self): return self._get_field("/api/check/is_new_bar", "is_new_bar") + def is_suspended_stock(self, stockcode): return self._post_field("/api/check/is_suspended_stock", {"stockcode": stockcode}, "is_suspended") + def is_sector_stock(self, sectorname, market, stockcode): return self._post_field("/api/check/is_sector_stock", {"sectorname": sectorname, "market": market, "stockcode": stockcode}, "is_in_sector") + def is_typed_stock(self, stocktypenum, market, stockcode): return self._post_field("/api/check/is_typed_stock", {"stocktypenum": stocktypenum, "market": market, "stockcode": stockcode}, "result") + def industry_name_of_stock(self, industry_type, stockcode): return self._post_field("/api/check/get_industry_name_of_stock", {"industryType": industry_type, "stockcode": stockcode}, "industry_name") + + def ext_data(self, name, stockcode, deviation): return self._post_field("/api/ext/ext_data", {"extdataname": name, "stockcode": stockcode, "deviation": deviation}, "value") + def ext_data_rank(self, name, stockcode, deviation): return self._post_field("/api/ext/ext_data_rank", {"extdataname": name, "stockcode": stockcode, "deviation": deviation}, "rank") + def get_factor_value(self, name, stockcode, deviation): return self._post_field("/api/ext/get_factor_value", {"factorname": name, "stockcode": stockcode, "deviation": deviation}, "value") + def get_factor_rank(self, name, stockcode, deviation): return self._post_field("/api/ext/get_factor_rank", {"factorname": name, "stockcode": stockcode, "deviation": deviation}, "rank") + def python_version(self): return self._get("/api/sys/python_version") + def shutdown(self): return self._post("/api/sys/shutdown", {}) diff --git a/py-client/sdk/models.py b/py-client/sdk/models.py new file mode 100644 index 0000000..9ae8e8b --- /dev/null +++ b/py-client/sdk/models.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +def _number(value: Any, kind: type = float) -> Any: + try: + return kind(value or 0) + except (TypeError, ValueError): + return kind() + + +@dataclass +class Position: + stock_code: str = "" + stock_name: str = "" + direction: Any = None + volume: int = 0 + open_price: float = 0.0 + float_profit: float = 0.0 + market_value: float = 0.0 + stock_holder: str = "" + frozen_volume: int = 0 + can_use_volume: int = 0 + on_road_volume: int = 0 + yesterday_volume: int = 0 + last_price: float = 0.0 + profit_rate: float = 0.0 + future_trade_type: Any = None + expire_date: str = "" + + @classmethod + def from_dict(cls, data: dict[str, Any], code: str = "") -> "Position": + return cls( + stock_code=str(data.get("StockCode") or code), stock_name=str(data.get("StockName") or ""), + direction=data.get("Direction"), volume=_number(data.get("Volume"), int), + open_price=_number(data.get("OpenPrice")), float_profit=_number(data.get("FloatProfit")), + market_value=_number(data.get("MarketValue")), stock_holder=str(data.get("StockHolder") or ""), + frozen_volume=_number(data.get("FrozenVolume"), int), can_use_volume=_number(data.get("CanUseVolume"), int), + on_road_volume=_number(data.get("OnRoadVolume"), int), yesterday_volume=_number(data.get("YesterdayVolume"), int), + last_price=_number(data.get("LastPrice")), profit_rate=_number(data.get("ProfitRate")), + future_trade_type=data.get("FutureTradeType"), expire_date=str(data.get("ExpireDate") or ""), + ) + + +@dataclass +class Assets: + total: float = 0.0 + available: float = 0.0 + + +@dataclass +class Tick: + last_price: float = 0.0 + last_close: float = 0.0 + raw: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class HistoryDataRequest: + length: int = 10 + period: str = "" + field: str = "" + dividend_type: int = 0 + skip_paused: bool = True + + +@dataclass +class MarketDataRequest: + fields: list[str] = field(default_factory=list) + stocks: list[str] = field(default_factory=list) + start_time: str = "" + end_time: str = "" + period: str = "" + dividend_type: str = "" + count: int = 0 + + +@dataclass +class FinancialDataRequest: + tabname: str = ""; colname: str = ""; market: str = ""; code: str = "" + report_type: str = ""; barpos: int = 0 + field_list: list[str] = field(default_factory=list); stock_list: list[str] = field(default_factory=list) + start_date: str = ""; end_date: str = "" + + +@dataclass +class FactorDataRequest: + field_list: list[str] = field(default_factory=list); stock_list: list[str] = field(default_factory=list) + stock_code: str = ""; start_date: str = ""; end_date: str = "" + + +@dataclass +class BSMPriceRequest: + option_type: str; object_prices: Any; strike_price: float; risk_free: float; sigma: float; days: int; dividend: float + + +@dataclass +class BSMIVRequest: + option_type: str; object_prices: float; strike_price: float; option_price: float; risk_free: float; days: int; dividend: float + + +@dataclass +class LocalDataRequest: + stock_code: str; start_time: str = ""; end_time: str = ""; period: str = ""; divid_type: str = ""; count: int = 0 diff --git a/py-client/sdk/trade.py b/py-client/sdk/trade.py new file mode 100644 index 0000000..3bb1aa1 --- /dev/null +++ b/py-client/sdk/trade.py @@ -0,0 +1,54 @@ +from typing import Any + +OP_BUY, OP_SELL = 23, 24 +ORDER_TYPE_VOLUME, PR_TYPE_LATEST, QUICK_TRADE_NOW = 1101, 5, 2 + + +class TradeMixin: + account_type: str + + def passorder(self, op_type, stock, volume, order_type=0, pr_type=0, price=0, quick_trade=0, strategy_name=""): + body = {"opType": op_type, "stock": stock, "price": price, "volume": volume} + for key, value in (("orderType", order_type), ("prType", pr_type), ("quickTrade", quick_trade), ("strategyName", strategy_name)): + if value: body[key] = value + return self._post("/api/trade/passorder", body) + + def passorder_latest(self, side, stock, volume): return self.passorder_latest_tagged(side, stock, volume, "") + def passorder_latest_tagged(self, side, stock, volume, order_id): + return self.passorder(side, stock, volume, ORDER_TYPE_VOLUME, PR_TYPE_LATEST, -1, QUICK_TRADE_NOW, order_id) + + def algo_passorder(self, **kwargs): return self._post("/api/trade/algo_passorder", kwargs) + def smart_algo_passorder(self, **kwargs): return self._post("/api/trade/smart_algo_passorder", kwargs) + + def _style_order(self, path, stock, value_key, value, style, price): + return self._post(path, {"stock": stock, value_key: value, "style": style, "price": price}) + def order_lots(self, stock, lots, style, price): return self._style_order("/api/trade/order_lots", stock, "lots", lots, style, price) + def order_value(self, stock, value, style, price): return self._style_order("/api/trade/order_value", stock, "value", value, style, price) + def order_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_percent", stock, "percent", percent, style, price) + def order_target_value(self, stock, value, style, price): return self._style_order("/api/trade/order_target_value", stock, "tar_value", value, style, price) + def order_target_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_target_percent", stock, "tar_percent", percent, style, price) + def order_shares(self, stock, shares, style, price): return self._style_order("/api/trade/order_shares", stock, "shares", shares, style, price) + + def _future(self, action, stock, amount, style, price): return self._style_order(f"/api/trade/futures/{action}", stock, "amount", amount, style, price) + def futures_buy_open(self, *args): return self._future("buy_open", *args) + def futures_buy_close_tdayfirst(self, *args): return self._future("buy_close_tdayfirst", *args) + def futures_buy_close_ydayfirst(self, *args): return self._future("buy_close_ydayfirst", *args) + def futures_sell_open(self, *args): return self._future("sell_open", *args) + def futures_sell_close_tdayfirst(self, *args): return self._future("sell_close_tdayfirst", *args) + def futures_sell_close_ydayfirst(self, *args): return self._future("sell_close_ydayfirst", *args) + + def _task(self, action, task_id): return self._post(f"/api/trade/{action}_task", {"taskId": task_id, "accountType": self.account_type}) + def cancel_task(self, task_id): return self._task("cancel", task_id) + def pause_task(self, task_id): return self._task("pause", task_id) + def resume_task(self, task_id): return self._task("resume", task_id) + def do_order(self): return self._post("/api/trade/do_order") + def trade_detail_data(self, datatype): return self._post("/api/trade/trade_detail_data", {"account": self.account_type, "datatype": datatype}).get("data", []) + def value_by_order_id(self, order_id, datatype): return self._post("/api/trade/value_by_order_id", {"orderId": order_id, "accountType": self.account_type, "datatype": datatype}).get("data") + def last_order_id(self, datatype): return self._post("/api/trade/last_order_id", {"account": self.account_type, "datatype": datatype}).get("last_order_id") + def can_cancel_order(self, order_id): return self._post("/api/trade/can_cancel_order", {"orderId": order_id, "accountType": self.account_type}).get("can_cancel") + def debt_contract(self): return self._contract("debt_contract") + def assure_contract(self): return self._contract("assure_contract") + def enable_short_contract(self): return self._contract("enable_short_contract") + def _contract(self, name): return self._post(f"/api/trade/{name}").get("data", []) + def ipo_data(self, typ): return self._post_field("/api/trade/ipo_data", {"type": typ}, "data") + def new_purchase_limit(self): return self._post_field("/api/trade/new_purchase_limit", None, "data") diff --git a/py-client/strategy/__init__.py b/py-client/strategy/__init__.py new file mode 100644 index 0000000..0622d5d --- /dev/null +++ b/py-client/strategy/__init__.py @@ -0,0 +1,5 @@ +"""交易策略启动入口。""" + +from .trend.boot import StartTrend + +__all__ = ["StartTrend"] diff --git a/py-client/strategy/__pycache__/__init__.cpython-311.pyc b/py-client/strategy/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b66374fa710fa368b473fbbb703eb426fe990c1 GIT binary patch literal 264 zcmZ3^%ge>Uz`)QUJuxeRfq~&Mhyw#mP{wB&1_p-d3@Hpz3@MB$OgW6XOi@gX3@Oa% zj8V)fEWr$#tfkTn3=B_pEqOL$^7FORo=;u+bi(?lT`QipE`8d+_;EwiOGX9;22I9W zT)`!YMI|9csd*_y%nS?+ewr+|*yH0<@{{A^Z*i4?#PyQ$^Gm>zAeqFRocQ>a44*+x z`6W_ZQj}PdnqCC%vZ!joc aU{HL(pmYHhJ>VAa&}(1^!6FU@1_l5K5=~J6 literal 0 HcmV?d00001 diff --git a/py-client/strategy/__pycache__/boot.cpython-311.pyc b/py-client/strategy/__pycache__/boot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..156af06f8fef7fa141d84c724ff55b1e38c4b1b5 GIT binary patch literal 2852 zcmb7GZ*1Gv5kHE5M9~!eM{#2(v7^ONI<<-WC%^_|O`2_S+OA1D%pQhn?FdGsH7OG5 zkrWcI5?0a=RG&ibkQECm`OJU~DIo3{MP3X%drKMjux zWhq;M?f7uIdw0Bd_xSGjH1GFw0093AoKJYY0Lxf$$x{=E`zioj02v}66S5~sq&yK1 zfn_pDMo8zEicrok9ie4Hrjks`8}T|mbdpW^B0d6SM)oGTR7<2K#Ygy*KjL@h*rbqZ zjkFRV^K#2L6={>nK?wWPqX1C3`HnsH#oE;m*51EtpSxyHU9msBvTU8@xXQK5t5+`B zA77~4_(SE!AJ-Pg?Q;{s!6UCOTVv~Q|90)xr_0vZ+SJWeYs|hnW1qcZzn5!_wzfFF zvUqc2DYx>E-v?7tT!UW>uYTOi!cubX& zMwmd%50i04HP;F3P9pY5eB@`68T}>ly{5~G{sT=rh3G+3GLb zx{9b1nyN(I{lH1_n8P6>1YK=4p01NerESd4lHmtcx&3&p6kkYQ)HxzNK1oUx(Ag|B_qA_iGLy%QgA-xbe+{t^^=^Wj4cM?>P^^LcMz)&mcDy_N5O1 zfIacCJ^zlK`=Aj(HkNYs-*2ugUJmYmHn=wb-ui_b>lde2b7O0ZS63(JE0=Q1*4Yzj z{C#S8WM99y`q5wPsoBb{TN_Kc%C*nzwGxAB$F z=MRSZpC8^>%B|+cRu+GEFeGC{{Nj@R?q>%>@=&_>u>h+deNwsgX{|i_Pk*i4xEyo$ z^^tJsaCj$TYV{z$o4!*X66zJybp9u4)r_Z<0bSR09itH9f0Z)Dlair`X+4R2STnU# zii-G`hlYME4!v@G;I*F)Ax1I`#Wav_L^I+JIO`Z*5I?NxqT!;f6HJy&NtEL{V(asq zn@3zmf-$N_#mz9dCv*(LI>t2}wq~8z(i*>>n9L+#r5!NLN%@-wyx* z05Co$04%WEw-(z&){n{^UB!+ktyfFl_Pn=y=I8mIXUoF2{Ptr7;doIvo@bBS{y>TC zD6%^pzN5?vMRxl=FC{P!fD#z%Xq5wD%e!w*y*Vl7MC)iN(0wP+cRSEm2!x7((AY8S z`_`zd=N8*>lXHs%3l z*2`aQ0h%8_R$>QAtdQf&Y|F$elR{3w!b_8Uj=$%lY32dYG*fk$;G_4IJG-ZUQRoa6 zJ3|v()kFCX6D5Aj9lqx_-!l^{@Oz5uGvVTndNib`}@xsGe>5>`{9W~;OS!E=>ogA z$nMRvd#eE-z0c?XfG@eufgpSt>^(}5{{-SFL54j#J`Os*C&TT?D~hrf6-C5};&9qb z>xwAq`1aQEb*AH6O2-!qqLT5GhI952X~?H)y-1tRiNP&UYX+yq1ZTG2J!G|B!Kdoa za35oV{yYGD&AyP*D<+OYjj3>6Wv{bN>MXC}XmJiC_P(tMH{Cg{DA)UK$K%Cur* znem9&b&<(zOw&?T%rGUZ(`aY|?LFJCb~2Bj>Eq! z?Yeu$r|ao!|9C%{D?Lf3vrL-ghK)y4VUlt&$~mLbB$d&bq2onjG?hh?rc5%Ern}|h wtA8L=G2`@5Zvt@5RC53y9YRQhy?U7%Y}d=wV5|PG2BUhJ8tl}|q&EP313E{WNdN!< literal 0 HcmV?d00001 diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b79c5af8e43f86cc5fb1bde530289e22b68d0016 GIT binary patch literal 9042 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

*PUieyj8L+-(fuJ4t;l zXQZ>{qi!oEoa)!JQ^wek)&1|fh>{L$=B({K%4z+~N8Ltx0jD#>xZZ;kqj+%)cg-EV zKSFvr+RRy38!vLWf6mGo4<<^>aoo3NjIQH>`{iNU4;2<7onlz{h4gzfXN+d7lIt6r z=I|V8q}-}8k;kbd&aandyYTWjet76wKSGta^=!&Wj+Z8<_1yn(&?)+F2pw_|MVmRF z&k9t}{ccXgf2Dqs=1LC#cGu_Qa9_qqtL6DI^Kl>UduDFjHzx9gnz0F->8@}gy8u9Q zTKytd5ykzs(z25nJO_#;dx*tBxtGbj)tnZUat(d=Ek9cIezL7zhvpK5usPKGPwzUxaa6sm zUXEWbvd0Tlw%FrEwy{8Ei)}2jCkj-yDnS0Y51NjssLlD|yN=z07K_+8R^XyLy zF)Fj``>Xs*HoOOPQ9BkvA;O?AjTIDZ=>zoZp5sg~PH9XAAF>Z8%qWpA?~?a9FmXsc=||&{#MuMfj+2 tSazLY^Qb2WJ}fW|TTH`WOvA426w|m1?=G*2?H{tdwu9}&yGJPn{{blpDChtH literal 0 HcmV?d00001 diff --git a/py-client/strategy/trend/__pycache__/state.cpython-311.pyc b/py-client/strategy/trend/__pycache__/state.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f92ba3993c0f8bccab21da55284b8325efe0969a GIT binary patch literal 8725 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 G1o*#kcRd3D literal 0 HcmV?d00001 diff --git a/py-client/strategy/trend/__pycache__/strategy.cpython-311.pyc b/py-client/strategy/trend/__pycache__/strategy.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae3a455e2553365b4bdc8a5e3d21ec22fdbfb431 GIT binary patch literal 6449 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 literal 0 HcmV?d00001 diff --git a/py-client/strategy/trend/__pycache__/watch.cpython-311.pyc b/py-client/strategy/trend/__pycache__/watch.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87c983845a3d61d4ddf014dfb338aec288ae5189 GIT binary patch literal 3219 zcmd58U1%F;a`sm%$+GOka+J17lr+ZKqZgm!#38*lq10)v#W5W2(MEB)sQT85~=|c)FhzH96@DUIooB@kmmgA7- z4Stc&@{2-NU~9n;7p1JkXwi_fvd-~v28gr*L|*3suJeK5bpW^?EFc?U=OQaWl-_e$ zg^07DM(^QSmGhz`XXo-p&azbAQ%KI%?c$>DMG+I-uyZQsiPL6&VJ06+2LPe7cyN%} zp+lArAm?NmX@<; zRM}HBt(;rbHO-4@+M-FS2GV_+_UmfS=z5k;3sR2N80mx;U_0U&WbG9M#- zD^gY=i5193i98GHfTv6qD{r9Y_nzYb{`LNUetC26``@4P`hAyamYz4u#PSBI{FOg)wELn>x@NUk+&KZUzKq z;WKVVmFOcu3K-V!3E*+VWsv_2Gck zzjarw$tm}hH(X_+CN`C)>#sNWOtePTns_DpmHov#U%s<(^z#!NCpJ#7yPEL?J6G_; zyh(IVs?cIy_k^;!9z<~oLl*?c(^jSAJ{5l74J(PG^a-`qH9nq0Wp@s{LqCdA2TTeQIBihNX zHI&c_2A>PPGO^n9)`4!l96w930zGny17^M8NTp~Xdw~)EAG$2Z0J?Qsjsp;JSpv8$ zJ92P`R@{*tD6y)H(6OLD$}Ij7o4fAK;a>zBgS!KzzJMYU=943@ivZWd+vVU#CCjt$ zER2ICF7cOybFjp5a1J>1Gt7b-nV}dwuc(|C>7@5P0sz%#0O0(H_0xZUx_MScq6ZThJiG0Z}t zST1;Cu{>|85xNJr_94n3+K&judJ?OnUZhiREUXS0AN(wi+O$|G=u{`mJHj*$d6?+d zKRK+qt@yy|OCLO6d%mvM^;Z9&+yCqz-n=$<^{s1f)r3}J@XEP1#G+}2T0`k;uYPp) z_h;7*HikxjSc@|v(#ZH^wllR55Yj(>aL&_?Qp@Ky1)*!;zLcXF;VIoF(= zYy51kao|Gpz=c)uTC{%fYO);(#4_3U@huO-S8cttt-wHPwc6Y@dM7#RCP$xGe*6jL z_d$A3z1rM2*4Q<6<5+Xz<;L*KxAKkQS8Hcl$>I9`51g7)U)mUIs;BPkJLT>>bz5pD zpHTJy45rZB)#SH*JFiT;yN}=8d6V2a)99OS_D#Ee)2*Gm+NmcLwvBT(H#fIqZ}BW_ z3jXxc@YI;_cjd&?nDCGO(Wx=vYd(W`Oq`mCd_BocO+@lxsRRK0TCfuA_#Y}L1z8pp z(y&CXbw3w^!)=qw+ZQ$XUuj66!q!OWPNRxhQF%WzFexm2fAH;+=S^DB?d4<`@;(C- z$2!k?3>MO^GVUtlb@AiqCy75M*2u9k`o!ku<|7+3>`-P1B&y;m#PM z0Yt3f!K|f20YU-RZ+rTtHqT2Z+rUdF*Q#u~^=>$7ay5ZH9%G@EWI zw~ziU$w>FS#^y)SUQRmFhR2bwC#tAMy?EHp>K4Ul^2;I?J}-(5QjQeM1=g9+L%7Xa zLS|pD%|9qF`2FWedK3UK^sE~I&=xq3Yr%fEceLP;yWMEPQ*Q5QLH~Q~*Oq~gw|So1 k-G;}}edIkvbYB2IzOvgt8B1|T+VD8KkGzN5Y%uTq7gRC4A^-pY literal 0 HcmV?d00001 diff --git a/py-client/strategy/trend/boot.py b/py-client/strategy/trend/boot.py new file mode 100644 index 0000000..15566e8 --- /dev/null +++ b/py-client/strategy/trend/boot.py @@ -0,0 +1,176 @@ +"""趋势策略启动器。 + +该模块负责组合 SDK、配置、状态存储和趋势策略组件,供 main.py 调用。 +""" + +from __future__ import annotations + +import logging +import time +from datetime import datetime + +import config +from libs import init_signals, market_allow_open, trading_time +from sdk import Client +from .state import State +from .order import OrderBook +from .watch import DipWatch +from .runtime import Runtime +from .open import open_signal +from .positions import manage_positions + + +def Overview(assets, positions, account_cfg=None) -> None: + """打印策略启动时的账户、资金和持仓概览。 + + 该函数对应 Go 客户端 ``logic.Overview``。为便于单独测试,可以 + 显式传入账户配置;未传入时使用 ``config.account_config``。 + """ + account_cfg = account_cfg or config.account_config + + print("\n" + "=" * 80) + print(f"【时间】{datetime.now():%Y-%m-%d %H:%M:%S}") + if account_cfg is not None: + print( + "【配置】" + f"account_id: {account_cfg.account_id} " + f"host_key: {account_cfg.host_key} " + f"buy_value: {account_cfg.buy_value:.0f}" + ) + + if assets is not None: + print( + f"【资金】总资产:{assets.total:.2f}元," + f"可用资金:{assets.available:.2f}元" + ) + else: + print("【资金】查询失败") + + print(f"【持仓】{len(positions)}只") + print("=" * 80) + for position in positions: + if position.volume <= 0: + continue + print( + f"【持仓】{position.stock_code} {position.stock_name} " + f"持仓={position.volume} 可用={position.can_use_volume} " + f"冻结={position.frozen_volume} 在途={position.on_road_volume} " + f"昨仓={position.yesterday_volume} 成本={position.open_price:.3f} " + f"现价={position.last_price:.3f} 市值={position.market_value:.2f} " + f"浮盈={position.float_profit:.2f} " + f"盈亏比例={position.profit_rate * 100:.2f}%" + ) + + + +def StartTrend() -> None: + """初始化趋势策略,并以 30 秒间隔持续执行。""" + client = Client( + config.global_config.qmt_base_url, + config.global_config.qmt_token, + config.HTTP_TIMEOUT, + ) + assets = client.assets() + _, positions = client.positions() + + storeState = State.for_strategy( + config.global_config.qmt_data_dir, + config.account_config.strategy, + config.account_config.account_id, + ) + storeState.sync_positions(positions) + + # 获取本策略的信号开仓数据 + signals = init_signals(config.global_config,["morning","tail","arbitrage"]) + run = Runtime( + client=client, + global_cfg=config.global_config, + account_cfg=config.account_config, + state=storeState, + orders=OrderBook(), + open_watch=DipWatch(), + add_watch=DipWatch(), + ) + + logging.info( + "趋势策略启动:总资产=%.2f,持仓=%d,信号=%d", + assets.total, + len(positions), + len(signals), + ) + Overview(assets, positions, config.account_config) + + while True: + started_at = time.monotonic() + try: + RunOnce(run, signals) + except Exception: + # 单轮错误只记录日志,下一轮仍继续运行。 + logging.exception("趋势策略本轮执行失败") + + elapsed = time.monotonic() - started_at + time.sleep(max(0.0, 30.0 - elapsed)) + + +def RunOnce(run: Runtime, signals) -> None: + """按固定步骤执行一轮趋势策略, ``RunOnce``。""" + if not trading_time(datetime.now()): + return + + # 1. 取消超过有效期仍未完成的委托订单。 + try: + run.orders.cancel_expired(run.client) + except Exception: + logging.exception("取消过期订单失败") + + # 2. 验证可用资金;低于资金安全线时禁止开新仓。 + try: + assets = run.client.assets() + except Exception: + logging.exception("获取资产失败") + return + if assets.available < assets.total * run.account_cfg.min_cash_ratio: + logging.info("资金总闸:可用金额太少,禁止开新仓") + return + + # 3. 获取大盘状态,只有大盘信号允许时才执行开仓。 + market_ok = market_allow_open(run.global_cfg.api_host) + + # 4. 获取当前持仓及持仓证券代码。 + try: + position_codes, positions = run.client.positions() + except Exception: + logging.exception("获取持仓失败") + return + + # 5. 验证有效开仓信号:排除已有持仓,并按 signal_allow 过滤。 + position_code_set = set(position_codes) + allow_open = [ + signal for signal in signals if signal.code not in position_code_set + ] + + # 6. 获取持仓和待开仓证券的实时行情 tick。 + all_codes = list(position_codes) + all_codes.extend( + signal.code for signal in allow_open if signal.code not in position_code_set + ) + try: + ticks = run.client.full_tick(list(dict.fromkeys(all_codes))) + except Exception: + logging.exception("获取行情失败") + return + + # 7. 执行开仓:必须同时存在有效信号且大盘允许开仓。 + if allow_open and market_ok: + open_signal(run, ticks, allow_open) + + # 8. 持仓计算。当前 Go 版本的 managePositions 为空,保留扩展入口。 + manage_positions(run, ticks, positions, market_ok,assets.available) + + +def SignalFilter(signals, allowed_names): + """只保留账户配置明确允许使用的信号。""" + if not allowed_names: + return [] + allowed = set(allowed_names) + return [signal for signal in signals if signal.signal_key in allowed] diff --git a/py-client/strategy/trend/open.py b/py-client/strategy/trend/open.py new file mode 100644 index 0000000..485c187 --- /dev/null +++ b/py-client/strategy/trend/open.py @@ -0,0 +1,105 @@ +"""趋势策略开仓逻辑,对应 Go 版本的 ``logic/open.go``。""" + +from __future__ import annotations + +import logging +from datetime import datetime + +from libs import calc_buy_volume +from sdk import OP_BUY + +from .order import PlaceOrderRequest +from .state import STATUS_ING, StateItem + + +def open_signal(run, ticks, open_signals) -> None: + """逐个验证开仓信号并提交买入委托。""" + for item in open_signals: + # 1. 验证信号配置允许开仓的时间区间。 + signal_config = run.global_cfg.signals.get(item.signal_key) + if signal_config is None or not check_timezone(signal_config.timezone): + continue + + # 2. 检查该证券是否已有买入委托锁,防止重复下单。 + if run.orders.busy(item.code,"BUY"): + continue + + # 3. 验证行情和最新价格是否有效。 + tick = ticks.get(item.code) + price = tick.last_price if tick is not None else 0 + if price <= 0: + continue + + # 4. 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。 + if not run.open_watch.triggered("开仓", item.code, price): + continue + + # 5. 根据单笔买入金额计算整手开仓数量。 + volume = calc_buy_volume(price, run.account_cfg.buy_value) + if volume <= 0: + continue + + # 6. 生成本地订单号并按最新价提交开仓委托。 + order_id = run.orders.new_order_id("base") + request = PlaceOrderRequest(run.client, OP_BUY, item.code, volume, order_id) + if not run.orders.place(request): + continue + + # 7. 保存底仓订单、数量、成本和处理中状态。 + run.state.set( + StateItem( + code=item.code, + base_order_id=order_id, + base_qty=volume, + base_cost=price, + base_status=STATUS_ING, + ) + ) + try: + run.state.save() + except OSError: + logging.exception("[状态] %s 开仓状态保存失败", item.code) + run.open_watch.forget(item.code) + logging.info("[ZT][开仓] %s 买入 %d 股", item.code, volume) + + +def check_timezone(timezone: str, now: datetime | None = None) -> bool: + """验证当前时间是否处于配置区间。 + + ``*`` 表示全天允许;多个区间用逗号分隔,例如 + ``9:30-10:30,13:30-14:30``。同时支持跨午夜区间。 + """ + timezone = str(timezone or "").strip() + if timezone == "*": + return True + + current = now or datetime.now() + current_minutes = current.hour * 60 + current.minute + + for section in timezone.split(","): + bounds = section.strip().split("-") + if len(bounds) != 2: + continue + start = _parse_minutes(bounds[0]) + end = _parse_minutes(bounds[1]) + if start is None or end is None: + continue + + if start <= end and start <= current_minutes <= end: + return True + if start > end and (current_minutes >= start or current_minutes <= end): + return True + + return False + + +def _parse_minutes(value: str) -> int | None: + """把 ``时:分`` 转换为当天分钟数,无效值返回 None。""" + try: + hour_text, minute_text = value.strip().split(":") + hour, minute = int(hour_text), int(minute_text) + except (TypeError, ValueError): + return None + if not 0 <= hour <= 23 or not 0 <= minute <= 59: + return None + return hour * 60 + minute diff --git a/py-client/strategy/trend/order.py b/py-client/strategy/trend/order.py new file mode 100644 index 0000000..dcfccb3 --- /dev/null +++ b/py-client/strategy/trend/order.py @@ -0,0 +1,148 @@ +"""趋势策略委托簿,对应 Go 客户端的 ``logic/order.go``。""" + +from __future__ import annotations + +import secrets +from dataclasses import dataclass +from datetime import datetime, timedelta +from threading import Lock +from typing import Any + +# QMT 开平方向字段到本地买卖方向的映射。 +OFFSET_FLAG = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"} + +# 表示委托仍在处理、可能继续成交的 QMT 状态。 +BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"} + + +@dataclass(frozen=True) +class PlaceOrderRequest: + """``OrderBook.place`` 提交委托所需的全部参数。""" + + client: Any + op: int + code: str + volume: int + order_id: str + + +@dataclass +class OrderItem: + """从 QMT 委托明细转换得到的本地订单记录。""" + + id: str + code: str + side: str + remark: str + status: str + created_at: datetime | None + volume: int + + +class OrderBook: + """线程安全的活动委托缓存。""" + + def __init__(self, timeout_seconds: float = 300) -> None: + self.timeout = timedelta(seconds=timeout_seconds) + self.data: dict[str, OrderItem] = {} + self.index: list[str] = [] + self.lock = Lock() + + @staticmethod + def new_order_id(leg: str) -> str: + """生成不超过 24 个字符的策略订单号。""" + return f"zt-{leg}-{secrets.token_hex(6)}"[:24] + + def is_lock(self, side: str, code: str) -> bool: + """判断证券在指定买卖方向上是否已经被委托锁定。""" + with self.lock: + return f"{side}-{code}" in self.index + + def busy(self, code: str, side: str) -> bool: + """判断证券是否存在仍在处理中的同方向委托。""" + with self.lock: + order = self.data.get(f"{side}-{code}") + return bool(order and order.status in BUSY_STATUSES) + + def refresh(self, client: Any) -> None: + """从 QMT 刷新当前委托明细和方向索引。""" + parsed_orders = [ + parse_order(row) for row in client.trade_detail_data("order") + ] + with self.lock: + self.data = {key: item for key, item in parsed_orders} + self.index = [key for key, _ in parsed_orders] + + def cancel_expired(self, client: Any, now: datetime | None = None) -> None: + """尝试撤销超过有效期且具有委托编号的订单。""" + self.refresh(client) + current = now or datetime.now() + + # 使用快照遍历,避免网络调用期间长期持有互斥锁。 + for order in list(self.data.values()): + if ( + order.created_at is not None + and current - order.created_at > self.timeout + and order.id + ): + client.can_cancel_order(order.id) + + def place(self, request: PlaceOrderRequest) -> bool: + """按最新价提交委托,并立即写入本地方向锁。""" + request.client.passorder_latest_tagged( + request.op, + request.code, + request.volume, + request.order_id, + ) + + side = OFFSET_FLAG.get(str(request.op), "") + with self.lock: + self.index.append(f"{side}-{request.code}") + return True + + +def parse_order(row: dict[str, Any]) -> tuple[str, OrderItem]: + """把 QMT 原始委托字段转换为本地订单及其索引键。""" + volume = _as_int(row.get("m_nVolumeTotal")) + _as_int( + row.get("m_nVolumeTraded") + ) + + timestamp = _as_int(row.get("m_nOrderTime")) + if timestamp > 100_000_000_000: + # QMT 某些版本返回毫秒时间戳。 + timestamp /= 1000 + created_at = ( + datetime.fromtimestamp(timestamp) + if timestamp + else _parse_insert_datetime(row) + ) + + item = OrderItem( + id=str(row.get("m_strOrderSysID") or ""), + code=str(row.get("m_strInstrumentID") or ""), + side=OFFSET_FLAG.get(str(row.get("m_nOffsetFlag")), ""), + remark=str(row.get("m_strRemark") or ""), + status=str(row.get("m_nOrderStatus") or ""), + created_at=created_at, + volume=volume, + ) + return f"{item.side}-{item.code}", item + + +def _as_int(value: Any) -> int: + """安全转换整数,无效值按 0 处理。""" + try: + return int(value or 0) + except (TypeError, ValueError): + return 0 + + +def _parse_insert_datetime(row: dict[str, Any]) -> datetime | None: + """使用委托日期和时间字段构造本地时间。""" + date = str(row.get("m_strInsertDate") or "") + clock = str(row.get("m_strInsertTime") or "").replace(":", "").zfill(6) + try: + return datetime.strptime(date + clock, "%Y%m%d%H%M%S") + except ValueError: + return None diff --git a/py-client/strategy/trend/positions.py b/py-client/strategy/trend/positions.py new file mode 100644 index 0000000..d99f41a --- /dev/null +++ b/py-client/strategy/trend/positions.py @@ -0,0 +1,167 @@ +"""趋势策略持仓管理逻辑,对应 Go 版本的 ``logic/positions.go``。""" + +from __future__ import annotations + +import logging +from math import floor + +from libs.calc import calc_buy_volume,calculate_min_profit_rate +from libs.grid_take_profit import GridState, GridTrailingTracker +from sdk import OP_BUY, OP_SELL +import config +from .order import PlaceOrderRequest +from .state import STATUS_ING, STATUS_NONE, STATUS_OK +from .runtime import Runtime + +LEG_BASE = "base" +LEG_ADDED = "add" + +# 止盈网格跟踪器延迟初始化,避免导入模块时账户配置尚未加载。 +profit_tracker = None + +# 分级补仓档位(百分比) +LOSS_TIERS = [-30, -50] +# 补仓反弹确认阈值(百分比) +LOSS_REBOUND_THRESHOLD = 0.5 + +def manage_positions(run:Runtime, ticks, positions, market_ok: bool,available:float) -> None: + """执行持仓计算。""" + logging.info(f"持仓:{len(positions)} 支股票,开始处理") + global profit_tracker + profit_tracker = GridTrailingTracker(step=run.account_cfg.grid_step_pct) + for idx,pos in positions: + code = pos['stock_code'] + avg_price = pos.get('avg_price', 0) + volume = pos.get('volume', 0) + can_use_volume = pos.get('can_use_volume', 0) + current_price = ticks.get(code, {}).get('lastPrice', 0) + strategy_name = pos.get('strategy_name', '') + market_value = pos.get('market_value',0) + profit = pos.get('profit_rate', 0) + + # 排除指定股票 + if code in config.account_config.excluded_codes: + continue + + # 过滤无效仓位 + if avg_price == 0 or can_use_volume == 0 or current_price == 0 or volume == 0: + continue + + # 计算盈亏率(百分比) + pnl_ratio = (current_price - avg_price) / avg_price * 100 if avg_price != 0 else 0 + pnl_ratio = round(pnl_ratio, 2) + + # 计算最小利润率:1倍 + min_profit_rate_val = calculate_min_profit_rate(avg_price, 1) + + # 盈利处理 + is_closed, message = handle_profit(run,code,avg_price, pnl_ratio, min_profit_rate_val, can_use_volume, strategy_name) + if is_closed: + logging.info("profit", code, f"止盈执行 | {message}") + if message != "": + logging.info("profit", code, message) + + # 补仓处理 + if config.account_config.enable_loss_add_position and market_ok: + is_replenished, message = handle_loss(run,code,current_price,pnl_ratio,market_value,market_ok,available) + if is_replenished: + logging.info("loss", code, f"补仓执行 | {message}") + if message != "": + logging.info("loss", code, message) + +# 盈利处理 +def handle_profit(run:Runtime, code: str, pnl_rate: float, + min_profit_rate: float, vol: int) -> tuple[bool, str]: + """ + 盈利处理 - 基于网格的止盈策略 + + Args: + code: 股票代码 + open_price: 开仓价格 + pnl_rate: 当前盈亏率(百分比) + min_profit_rate: 最小利润率阈值 + vol: 可用股数 + strategy_name: str + + Returns: + tuple[bool, str]: (是否执行平仓, 操作说明) + """ + # 预检查:未达到最小利润率 + if pnl_rate < min_profit_rate: + return False, "" + + position_key = f"{run.account_cfg.account_id}:{code}" + observation = profit_tracker.observe(position_key, pnl_rate) + + if observation.state == GridState.ARMED: + msg = f"首次达到{pnl_rate}%,设置峰值网格{observation.current_grid}" + return False, msg + + if observation.state == GridState.RAISED: + return False, f"上涨至{pnl_rate}%,更新峰值网格{observation.current_grid}" + + # 执行平仓 + if observation.state == GridState.RETREAT: + order_id = run.orders.new_order_id(LEG_BASE) + request = PlaceOrderRequest(run.client, OP_SELL, code, vol, order_id) + result = run.orders.place(request) + if result : + success_msg = f"✓ 委托成功 | {vol}股 订单号:{result} 等待成交" + logging.info("profit", code, success_msg) + return True, success_msg + else: + fail_msg = f"止盈委托失败: {code}" + logging.error("profit", code, "✗ 止盈委托失败") + return False, fail_msg + + +def handle_loss(run:Runtime, code: str, current_price,pnl_rate,market_value: float,market_ok: bool, available: float) -> tuple[bool, str]: + """满足条件时提交补仓委托,并返回扣减后的剩余预算。""" + state = run.state.get(code) + added_num = state.get('added_num',0) + # 预检查:未达到最低补仓阈值 + if pnl_rate > LOSS_TIERS[added_num]: + return False, "" + + # 强制条件 + if current_price>200 or market_value>=60000: + return False, f"成本价{current_price}>200,仓位价值{market_value}>=60000, 不补仓" + + # 1. 大盘必须允许开仓,且价格已从观察低点达到反弹阈值。 + if not market_ok or not run.add_watch.triggered("补仓", code, current_price): + return False + + # 2. 计算补仓数量和预计占用金额。 + volume = calc_buy_volume(current_price, run.account_cfg.buy_value) + amount = current_price * volume + + # 3. 检查预算。 + if amount > available: + return False, f"f{code} f{amount} 仓位资金不够补仓" + + # 是否已有未完成的买入委托 + if run.orders.busy(run, code, "BUY"): + return False, f"{code}订单锁定中" + + # 4. 生成补仓订单号并提交买入委托。 + order_id = run.orders.new_order_id(LEG_ADDED) + request = PlaceOrderRequest(run.client, OP_BUY, code, volume, order_id) + result = run.orders.place(request) + if result : + state.added_num = +1 + state.added_status = run.state.STATUS_ING + state.added_order_id = order_id + run.state.set(state) + run.state.save() + run.add_watch.forget(code) + return True,f"补仓委托成功: {code} {volume}手, 等待成交确认" + else: + return False,f"补仓失败: {code}" + + +def forget(run, code: str) -> None: + """持仓退出后清理开仓、补仓观察记录和止盈峰值。""" + + + + run.peak_grids.pop(f"{code}|{LEG_ADDED}", None) diff --git a/py-client/strategy/trend/runtime.py b/py-client/strategy/trend/runtime.py new file mode 100644 index 0000000..1b7f98b --- /dev/null +++ b/py-client/strategy/trend/runtime.py @@ -0,0 +1,43 @@ +"""趋势策略单次运行所需的上下文对象。""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from config import AccountConfig, GlobalConfig +from sdk import Client + +from .order import OrderBook +from .state import State +from .watch import DipWatch + + +@dataclass(slots=True) +class Runtime: + """集中保存趋势策略运行期间共享的依赖和状态。 + + 将这些对象集中到一个 dataclass 后,开仓、持仓管理和单轮调度函数 + 只需接收一个 ``Runtime``,无需重复传递大量参数。 + + Attributes: + client: QMT HTTP 客户端,用于查询账户、行情和提交委托。 + global_cfg: 公共配置,包含 QMT、外部 API 和信号配置。 + account_cfg: 当前主机的账户及交易策略配置。 + state: 策略持仓状态的本地持久化存储。 + orders: 当前活动委托和证券方向锁。 + open_watch: 新开仓使用的价格反弹观察器。 + add_watch: 亏损补仓使用的价格反弹观察器。 + peak_grids: ``证券代码|仓位类型`` 到最高盈利网格的映射。 + """ + + # 外部服务与账户配置。 + client: Client + global_cfg: GlobalConfig + account_cfg: AccountConfig + + # 策略运行过程中共享的状态组件。 + state: State + orders: OrderBook + open_watch: DipWatch + add_watch: DipWatch + diff --git a/py-client/strategy/trend/state.py b/py-client/strategy/trend/state.py new file mode 100644 index 0000000..263cad4 --- /dev/null +++ b/py-client/strategy/trend/state.py @@ -0,0 +1,146 @@ +"""趋势策略持仓状态的内存管理与 JSON 持久化。""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from pathlib import Path +from threading import Lock +from typing import Iterable + +from sdk import Position + + +# 委托状态:无操作、处理中、已完成。 +STATUS_NONE = "" +STATUS_ING = "ING" +STATUS_OK = "OK" + + +@dataclass(slots=True) +class StateItem: + """单只证券的底仓和补仓状态。""" + + # 证券代码。 + code: str + + # 底仓订单、数量、成本和处理状态。 + base_order_id: str = "" + base_qty: int = 0 + base_cost: float = 0.0 + base_status: str = STATUS_NONE + + # 补仓订单、补仓次数、数量、成本和处理状态。 + added_order_id: str = "" + added_num: int = 0 + added_qty: int = 0 + added_cost: float = 0.0 + added_status: str = STATUS_NONE + + +class State: + """线程安全的策略状态存储。 + + 状态以内存字典提供快速访问,并通过临时文件替换的方式写入 JSON, + 防止程序在写入过程中退出而破坏原状态文件。 + """ + + def __init__(self, path: str | Path) -> None: + self.path = Path(path) + self.lock = Lock() + self.items = self._load() + + @classmethod + def for_strategy( + cls, + data_dir: str | Path, + strategy: str, + account_id: str, + ) -> "State": + """根据数据目录、策略名称和账户生成独立状态文件。""" + state_path = Path(data_dir) / f"{strategy}_{account_id}_state.json" + return cls(state_path) + + @property + def codes(self) -> list[str]: + """返回当前已经接管的全部证券代码快照。""" + with self.lock: + return list(self.items) + + def get(self, code: str) -> StateItem: + """获取指定证券的状态;不存在时抛出 KeyError。""" + with self.lock: + return self.items[code] + + def set(self, item: StateItem) -> None: + """新增或覆盖一只证券的状态。""" + with self.lock: + self.items[item.code] = item + + def delete(self, code: str) -> None: + """删除证券状态;证券不存在时不报错。""" + with self.lock: + self.items.pop(code, None) + + def sync_positions(self, positions: Iterable[Position]) -> None: + """把尚未接管的真实持仓初始化为已完成底仓。 + + 无证券代码、无持仓数量或成本无效的记录会被忽略。同步结束后 + 立即保存,确保首次接管的持仓在程序重启后仍可恢复。 + """ + known_codes = set(self.codes) + for position in positions: + if ( + not position.stock_code + or position.volume <= 0 + or position.open_price <= 0 + or position.stock_code in known_codes + ): + continue + + self.set( + StateItem( + code=position.stock_code, + base_qty=position.volume, + base_cost=position.open_price, + base_status=STATUS_OK, + ) + ) + known_codes.add(position.stock_code) + + self.save() + + def save(self) -> None: + """将内存状态格式化写入 JSON,并原子替换正式文件。""" + with self.lock: + self.path.parent.mkdir(parents=True, exist_ok=True) + temporary_path = self.path.with_suffix(self.path.suffix + ".tmp") + payload = { + code: asdict(item) + for code, item in self.items.items() + } + temporary_path.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + temporary_path.replace(self.path) + + def _load(self) -> dict[str, StateItem]: + """读取已有状态文件;文件不存在时从空状态开始。""" + try: + raw = json.loads(self.path.read_text(encoding="utf-8")) + except FileNotFoundError: + return {} + except (OSError, json.JSONDecodeError) as exc: + raise ValueError(f"[状态] 读取或解析失败: {exc}") from exc + + if not isinstance(raw, dict): + raise ValueError("[状态] 状态文件根节点必须是 JSON 对象") + + try: + return { + code: StateItem(**item) + for code, item in raw.items() + } + except (TypeError, ValueError) as exc: + raise ValueError(f"[状态] 状态字段无效: {exc}") from exc diff --git a/py-client/strategy/trend/watch.py b/py-client/strategy/trend/watch.py new file mode 100644 index 0000000..95e0ed0 --- /dev/null +++ b/py-client/strategy/trend/watch.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from datetime import datetime, timedelta +from threading import Lock +import logging + + +@dataclass +class _Entry: + last_close: float + expires_at: datetime + + +class DipWatch: + def __init__(self, expire_seconds: float = 300, rebound_threshold: float = 0.61): + self.expire_seconds, self.rebound_threshold = expire_seconds, rebound_threshold + self.data: dict[str, _Entry] = {}; self.lock = Lock() + + def triggered(self, tag: str, code: str, price: float, now: datetime | None = None) -> bool: + if price <= 0: return False + now = now or datetime.now() + with self.lock: + watch = self.data.get(code) + if watch is None or now >= watch.expires_at: + self.data[code] = _Entry(price, now + timedelta(seconds=self.expire_seconds)); return False + if price < watch.last_close: + self.data[code] = _Entry(price, now + timedelta(seconds=self.expire_seconds)); return False + rebound = (price - watch.last_close) / watch.last_close * 100 + if rebound <= 0 or rebound < self.rebound_threshold: return False + del self.data[code] + logging.info("[%s-触发] %s 反弹=%.2f%%", tag, code, rebound) + return True + + def forget(self, code): + with self.lock: self.data.pop(code, None) diff --git a/py-client/test.py b/py-client/test.py new file mode 100644 index 0000000..b878c21 --- /dev/null +++ b/py-client/test.py @@ -0,0 +1,25 @@ +import json +import os +from pathlib import Path + +from sdk import Client + +BASE_URL = "http://127.0.0.1:10086" +TOKEN = "QMTbyYanweidong" + + +def main(): + client = Client(BASE_URL, TOKEN).set_account_type("stock") + assets = client.assets(); + _, positions = client.positions() + print(f"总资产:{assets.total:.2f}元,可用资金:{assets.available:.2f}元") + for p in sorted(positions, key=lambda item: item.stock_code): + if p.volume > 0: print(f"{p.stock_code} {p.stock_name} 持仓={p.volume} 可用={p.can_use_volume} 成本={p.open_price:.3f} 现价={p.last_price:.3f}") + data_dir = os.environ.get("QMT_DATA_DIR", "").strip() + if not data_dir: raise SystemExit("环境变量 QMT_DATA_DIR 为空") + codes = json.loads((Path(data_dir) / "pass_codes.json").read_text(encoding="utf-8")) + for code, tick in sorted(client.full_tick(codes).items()): + print(f"{code} last={tick.last_price:.3f} close={tick.last_close:.3f}") + + +if __name__ == "__main__": main() diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..b1cf477 --- /dev/null +++ b/run.bat @@ -0,0 +1,7 @@ +git pull +set PYTHONUTF8=1 +taskkill /IM collector.exe /F +taskkill /IM python.exe /F +start python main.py +start bin\collector.exe +exit diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000..2eb5f4d --- /dev/null +++ b/setup.bat @@ -0,0 +1,4 @@ +set PYTHONUTF8=1 +pip config set global.index-url https://mirrors.aliyun.com/pypi/simple +python.exe -m pip install --upgrade pip +pip install -r requirements.txt \ No newline at end of file