This commit is contained in:
2026-08-25 16:40:18 +08:00
parent 4514646c6a
commit fa62436a73
28 changed files with 6401 additions and 0 deletions

117
go-client/apps/cmd/main.go Normal file
View File

@@ -0,0 +1,117 @@
package main
import (
"context"
"fmt"
"os"
"sort"
"strings"
"time"
"big-qmt/go-client/sdk"
)
var (
BaseURL = "http://127.0.0.1:10086"
Token = "QMTbyYanweidong"
AccountType = "stock"
PassCodes = []string{}
Timeout = 15 * time.Second
)
func main() {
client := sdk.New(BaseURL, Token, AccountType, Timeout)
ctx, cancel := context.WithTimeout(context.Background(), Timeout)
defer cancel()
assets, err := client.Assets(ctx, AccountType)
if err != nil {
fatal("获取资产失败: %v", err)
}
positions, err := client.Positions(ctx, AccountType)
if err != nil {
fatal("获取持仓失败: %v", err)
}
fmt.Println(strings.Repeat("=", 80))
fmt.Printf("【时间】%s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("【服务】%s accountType=%s\n", BaseURL, AccountType)
fmt.Printf("【资金】总资产:%.2f元,可用资金:%.2f元\n", assets.Total, assets.Available)
fmt.Printf("【持仓】%d只\n", len(positions))
fmt.Println(strings.Repeat("=", 80))
sort.Slice(positions, func(i, j int) bool {
return positions[i].StockCode < positions[j].StockCode
})
for _, p := range positions {
if p.Volume <= 0 {
continue
}
fmt.Printf(
"【持仓】%s %s 持仓=%d 可用=%d 冻结=%d 在途=%d 昨仓=%d 成本=%.3f 现价=%.3f 市值=%.2f 浮盈=%.2f 盈亏比例=%.2f%%\n",
p.StockCode, p.StockName, p.Volume, p.CanUseVolume, p.FrozenVolume, p.OnRoadVolume, p.YesterdayVolume,
p.OpenPrice, p.LastPrice, p.MarketValue, p.FloatProfit, p.ProfitRate*100,
)
}
printTicks(client, Timeout, PassCodes)
}
func printTicks(client *sdk.Client, timeout time.Duration, codes []string) {
fmt.Println(strings.Repeat("-", 80))
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ticks, err := client.FullTick(ctx, codes)
if err != nil {
fatal("获取行情失败: %v", err)
}
fmt.Printf("【行情】请求 %d 只,返回 %d 只\n", len(codes), len(ticks))
keys := make([]string, 0, len(ticks))
for code := range ticks {
keys = append(keys, code)
}
sort.Strings(keys)
for _, code := range keys {
t := ticks[code]
fmt.Printf("【Tick】%s last=%.3f close=%.3f open=%s high=%s low=%s volume=%s\n",
code, t.LastPrice, t.LastClose,
rawStr(t.Raw, "open", "lastOpen", "Open"),
rawStr(t.Raw, "high", "High"),
rawStr(t.Raw, "low", "Low"),
rawStr(t.Raw, "volume", "Volume"),
)
}
}
func splitCSV(s string) []string {
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func rawStr(m map[string]any, names ...string) string {
for _, name := range names {
if v, ok := m[name]; ok && v != nil {
return fmt.Sprint(v)
}
}
return "-"
}
func envOr(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
func fatal(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}

70
go-client/apps/zt/boot.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"strings"
"time"
"big-qmt/go-client/sdk"
)
func overview(cfg Config, assets *sdk.Assets, positions []sdk.Position) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Printf("【时间】%s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("【配置】account_id: %s host_key: %s open_money: %.0f\n", cfg.AccountID, cfg.HostKey, cfg.OpenMoney)
if assets != nil {
fmt.Printf("【资金】总资产:%.2f元,可用资金:%.2f元\n", assets.Total, assets.Available)
} else {
fmt.Println("【资金】查询失败")
}
fmt.Printf("【持仓】%d只\n", len(positions))
fmt.Println(strings.Repeat("=", 80))
for _, p := range positions {
if p.Volume <= 0 {
continue
}
code := normalizeCode(p.StockCode, "")
fmt.Printf("【持仓】%s %s 持仓=%d 可用=%d 冻结=%d 在途=%d 昨仓=%d 成本=%.3f 现价=%.3f 市值=%.2f 浮盈=%.2f 盈亏比例=%.2f%%\n",
code, p.StockName, p.Volume, p.CanUseVolume, p.FrozenVolume, p.OnRoadVolume, p.YesterdayVolume,
p.OpenPrice, p.LastPrice, p.MarketValue, p.FloatProfit, p.ProfitRate*100)
}
}
func runRound(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, assets *sdk.Assets, ticks map[string]sdk.Tick, positions []sdk.Position) {
signals := fetchSignal(cfg, "dcm_signal")
books.cancelExpired(ctx, client, cfg)
hold := positionCodes(positions)
openSignals := map[string]map[string]any{}
for code, signal := range signals {
norm := normalizeCode(code, "")
if norm == "" {
norm = code
}
if _, held := hold[norm]; held {
continue
}
openSignals[norm] = signal
}
if len(openSignals) > 0 {
if books.refresh(ctx, client, cfg) {
buys, _, ok := books.activeSets(ctx, client, cfg)
if ok {
filtered := map[string]map[string]any{}
for code, signal := range openSignals {
if _, buying := buys[code]; buying {
continue
}
filtered[code] = signal
}
openSignals = filtered
}
}
}
marketOK := marketAllowOpen(cfg)
if len(openSignals) > 0 {
openSignal(ctx, client, books, cfg, assets, ticks, openSignals, marketOK)
}
managePositions(ctx, client, books, cfg, ticks, positions, marketOK)
}

118
go-client/apps/zt/config.go Normal file
View File

@@ -0,0 +1,118 @@
package main
import (
"os"
"strconv"
"strings"
"time"
)
type Config struct {
QMTBaseURL string
QMTToken string
AccountType string
AccountID string
HostKey string
APIHost string
DataDir string
HTTPTimeout time.Duration
OrderTimeout time.Duration
LoopInterval time.Duration
OpenMoney float64
MinCashRatio float64
LossTriggerPct float64
GridStepPct float64
MinProfitPct float64
AdoptExisting bool
ReadyCacheStart int
WatchTimeout time.Duration
ReboundThreshold float64
}
func loadConfig() Config {
cfg := Config{
QMTBaseURL: env("QMT_BASE_URL", "http://127.0.0.1:10086"),
QMTToken: env("QMT_TOKEN", "QMTbyYanweidong"),
AccountType: env("QMT_ACCOUNT", "stock"),
AccountID: env("ACCOUNT_ID", ""),
HostKey: env("HOST_KEY", ""),
APIHost: strings.TrimRight(env("API_HOST", "http://139.224.247.176:13499"), "/"),
DataDir: env("DATA_DIR", "D:/qmt_strategy_state"),
HTTPTimeout: durationEnv("HTTP_TIMEOUT_SEC", 5) * time.Second,
OrderTimeout: durationEnv("ORDER_TIMEOUT_SEC", 60) * time.Second,
LoopInterval: durationEnv("LOOP_INTERVAL_SEC", 30) * time.Second,
OpenMoney: floatEnv("OPEN_MONEY", 5000),
MinCashRatio: floatEnv("MIN_CASH_RATIO", 0.1),
LossTriggerPct: floatEnv("LOSS_TRIGGER_PCT", -30),
GridStepPct: floatEnv("GRID_STEP_PCT", 1),
MinProfitPct: floatEnv("MIN_PROFIT_PCT", 2),
AdoptExisting: boolEnv("ADOPT_EXISTING_POSITIONS", true),
ReadyCacheStart: intEnv("READY_CACHE_START", 925),
WatchTimeout: durationEnv("WATCH_TIMEOUT_SEC", 300) * time.Second,
ReboundThreshold: floatEnv("REBOUND_THRESHOLD", 0.61),
}
if strings.TrimSpace(cfg.AccountID) == "" {
logf("ERROR", "ACCOUNT_ID 为空")
os.Exit(1)
}
if strings.TrimSpace(cfg.HostKey) == "" {
logf("ERROR", "HOST_KEY 为空")
os.Exit(1)
}
if cfg.MinCashRatio < 0 || cfg.MinCashRatio >= 1 {
logf("ERROR", "MIN_CASH_RATIO 必须在 [0, 1)")
os.Exit(1)
}
if cfg.OpenMoney <= 0 {
logf("ERROR", "OPEN_MONEY 必须大于 0")
os.Exit(1)
}
if err := os.MkdirAll(cfg.DataDir, 0o755); err != nil {
logf("ERROR", "创建 DATA_DIR 失败: %v", err)
os.Exit(1)
}
return cfg
}
func env(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
func intEnv(key string, fallback int) int {
v := strings.TrimSpace(os.Getenv(key))
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
return fallback
}
return n
}
func floatEnv(key string, fallback float64) float64 {
v := strings.TrimSpace(os.Getenv(key))
if v == "" {
return fallback
}
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return fallback
}
return f
}
func durationEnv(key string, fallbackSec int) time.Duration {
return time.Duration(intEnv(key, fallbackSec))
}
func boolEnv(key string, fallback bool) bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
if v == "" {
return fallback
}
return v == "1" || v == "true" || v == "yes"
}

10
go-client/apps/zt/log.go Normal file
View File

@@ -0,0 +1,10 @@
package main
import (
"fmt"
"log"
)
func logf(level, format string, args ...any) {
log.Printf("[%s] %s", level, fmt.Sprintf(format, args...))
}

105
go-client/apps/zt/main.go Normal file
View File

@@ -0,0 +1,105 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"big-qmt/go-client/sdk"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
cfg := loadConfig()
client := sdk.New(cfg.QMTBaseURL, cfg.QMTToken, cfg.AccountType, cfg.HTTPTimeout)
books := newOrderBook()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
startup := context.Background()
assets, err := client.Assets(startup, cfg.AccountType)
if err != nil {
logf("ERROR", "启动获取资产失败: %v", err)
}
positions, err := client.Positions(startup, cfg.AccountType)
if err != nil {
logf("ERROR", "启动获取持仓失败: %v", err)
positions = []sdk.Position{}
}
overview(cfg, assets, positions)
logf("INFO", "[ZT] host_key=%s interval=%s", cfg.HostKey, cfg.LoopInterval)
logf("INFO", "[ZT] Init Success, waiting trading session")
ticker := time.NewTicker(cfg.LoopInterval)
defer ticker.Stop()
runOnce(ctx, client, books, cfg)
for {
select {
case <-ctx.Done():
logf("INFO", "[ZT] 停止")
return
case <-ticker.C:
runOnce(ctx, client, books, cfg)
}
}
}
func runOnce(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config) {
if !tradingTime(time.Now()) {
return
}
roundCtx, cancel := context.WithTimeout(ctx, cfg.HTTPTimeout*4)
defer cancel()
assets, err := client.Assets(roundCtx, cfg.AccountType)
if err != nil {
logf("ERROR", "获取资产失败: %v", err)
return
}
positions, err := client.Positions(roundCtx, cfg.AccountType)
if err != nil {
logf("ERROR", "获取持仓失败: %v", err)
return
}
codes := passCodes(cfg)
seen := map[string]struct{}{}
stockList := make([]string, 0, len(codes)+len(positions))
addCode := func(code string) {
n := normalizeCode(code, "")
if n == "" {
n = strings.ToUpper(strings.TrimSpace(code))
}
if n == "" {
return
}
if _, ok := seen[n]; ok {
return
}
seen[n] = struct{}{}
stockList = append(stockList, n)
}
for _, code := range codes {
addCode(code)
}
for _, p := range positions {
addCode(p.StockCode)
}
ticks := map[string]sdk.Tick{}
if len(stockList) > 0 {
raw, err := client.FullTick(roundCtx, stockList)
if err != nil {
logf("ERROR", "获取行情失败: %v", err)
return
}
for code, tick := range raw {
ticks[normalizeCode(code, "")] = tick
ticks[code] = tick
}
}
runRound(roundCtx, client, books, cfg, assets, ticks, positions)
}

114
go-client/apps/zt/open.go Normal file
View File

@@ -0,0 +1,114 @@
package main
import (
"context"
"math"
"sync"
"time"
"big-qmt/go-client/sdk"
)
type dipWatch struct {
LastClose float64
ExpiresAt time.Time
}
var openDip = struct {
mu sync.Mutex
store map[string]dipWatch
}{store: map[string]dipWatch{}}
func openSignal(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, assets *sdk.Assets, ticks map[string]sdk.Tick, openSignals map[string]map[string]any, marketOK bool) {
if !marketOK {
return
}
if assets == nil {
return
}
if assets.Available < assets.Total*cfg.MinCashRatio {
return
}
state := getState(cfg)
if state.LoadError != "" {
logf("ERROR", "[ZT][开仓] 状态文件异常,禁止新开仓: %s", state.LoadError)
return
}
for signalCode, signal := range openSignals {
code := normalizeCode(signalCode, "")
if code == "" {
if c, ok := signal["code"].(string); ok {
code = normalizeCode(c, "")
}
}
if code == "" {
logf("ERROR", "[ZT][开仓] 无效股票代码=%s", signalCode)
continue
}
if state.Get(code) != nil {
continue
}
price := ticks[code].LastPrice
if price <= 0 {
continue
}
if !dipTriggered(&openDip.mu, openDip.store, cfg, "开仓", code, price) {
continue
}
volume := calcOpenVolume(price, cfg.OpenMoney)
if volume <= 0 {
continue
}
if !books.place(ctx, client, cfg, "buy", code, volume, newOrderTag("base")) {
continue
}
state.Ensure(code).Pending = "base_opening"
state.Save()
logf("INFO", "[ZT][开仓] %s 买入 %d 股", code, volume)
}
state.Save()
}
func calcOpenVolume(price, openMoney float64) int {
if price <= 0 || openMoney <= 0 {
return 0
}
hands := int(math.Floor(openMoney / (price * 100)))
if hands == 0 {
hands = 1
}
return hands * 100
}
func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, cfg Config, tag, code string, price float64) bool {
if price <= 0 {
return false
}
mu.Lock()
defer mu.Unlock()
now := time.Now()
watch, ok := store[code]
if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) {
store[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(cfg.WatchTimeout)}
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
return false
}
if price < watch.LastClose {
watch.LastClose = price
watch.ExpiresAt = now.Add(cfg.WatchTimeout)
store[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 < cfg.ReboundThreshold {
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, cfg.ReboundThreshold)
return false
}
delete(store, code)
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
return true
}

374
go-client/apps/zt/order.go Normal file
View File

@@ -0,0 +1,374 @@
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strconv"
"strings"
"sync"
"time"
"big-qmt/go-client/sdk"
)
const (
opBuyStock = 23
opBuyAlt = 48
)
var activeStatuses = map[int]struct{}{
48: {}, 49: {}, 50: {}, 51: {}, 52: {}, 55: {},
}
type parsedOrder struct {
OrderID string
StockCode string
Side string
Active bool
OrderTime int64
RemarkOwned bool
VolumeOrig int
VolumeLeft int
VolumeTraded int
Tag string
}
func (o parsedOrder) cancelVolume() int {
n := o.VolumeLeft + o.VolumeTraded
if n > 0 {
return n
}
return o.VolumeOrig
}
type submission struct {
Code string
Side string
Volume int
At time.Time
Tag string
}
type orderBook struct {
mu sync.Mutex
cached []parsedOrder
hasCache bool
buyLocks map[string]time.Time
sellLocks map[string]time.Time
subs []submission
}
func newOrderBook() *orderBook {
return &orderBook{
buyLocks: map[string]time.Time{},
sellLocks: map[string]time.Time{},
}
}
func (o *orderBook) invalidate() {
o.mu.Lock()
defer o.mu.Unlock()
o.hasCache = false
o.cached = nil
}
func (o *orderBook) query(ctx context.Context, client *sdk.Client, cfg Config) ([]parsedOrder, error) {
o.mu.Lock()
if o.hasCache {
out := append([]parsedOrder(nil), o.cached...)
o.mu.Unlock()
return out, nil
}
o.mu.Unlock()
raw, err := client.TradeDetailData(ctx, cfg.AccountType, "order")
if err != nil {
logf("ERROR", "[ZT][委托] 查询失败: %v", err)
return nil, err
}
orders := make([]parsedOrder, 0, len(raw))
for _, item := range raw {
orders = append(orders, parseOrder(item))
}
o.mu.Lock()
o.cached = orders
o.hasCache = true
o.mu.Unlock()
return orders, nil
}
func (o *orderBook) refresh(ctx context.Context, client *sdk.Client, cfg Config) bool {
o.invalidate()
_, err := o.query(ctx, client, cfg)
return err == nil
}
func (o *orderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Config) (buys, sells map[string]struct{}, ok bool) {
orders, err := o.query(ctx, client, cfg)
if err != nil {
return nil, nil, false
}
buys, sells = map[string]struct{}{}, map[string]struct{}{}
for _, item := range orders {
if !item.Active || item.StockCode == "" {
continue
}
if item.Side == "buy" {
buys[item.StockCode] = struct{}{}
} else {
sells[item.StockCode] = struct{}{}
}
}
return buys, sells, true
}
func (o *orderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg Config) bool {
o.invalidate()
orders, err := o.query(ctx, client, cfg)
if err != nil {
return false
}
state := getState(cfg)
now := time.Now()
timeout := cfg.OrderTimeout
seen := map[string]struct{}{}
for _, order := range orders {
if !order.Active || order.StockCode == "" {
continue
}
if !o.claimed(state, order) {
continue
}
if order.OrderTime <= 0 || now.Sub(time.Unix(order.OrderTime, 0)) <= timeout {
continue
}
vol := order.cancelVolume()
if vol <= 0 {
logf("WARNING", "[ZT][委托] 超时单缺少数量,跳过 %s %s", order.OrderID, order.StockCode)
continue
}
key := order.StockCode + "|" + strconv.Itoa(vol)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if order.OrderID != "" {
can, err := client.CanCancelOrder(ctx, order.OrderID, cfg.AccountType)
if err != nil {
logf("ERROR", "[ZT][委托] 查询是否可撤失败 %s: %v", order.OrderID, err)
continue
}
if !truthy(can) {
logf("INFO", "[ZT][委托] 不可撤 %s %s", order.OrderID, order.StockCode)
continue
}
}
ret, err := client.CancelByRule(ctx, order.StockCode, vol, cfg.AccountType)
if err != nil {
logf("ERROR", "[ZT][委托] 撤单失败 %s %s: %v", order.OrderID, order.StockCode, err)
continue
}
if ret == nil || ret.Status != "success" {
msg := ""
if ret != nil {
msg = ret.Message
}
logf("WARNING", "[ZT][委托] 规则撤单未命中 %s %s volume=%d %s", order.OrderID, order.StockCode, vol, msg)
continue
}
o.unlockSide(order.StockCode, order.Side)
logf("INFO", "[ZT][委托] 撤销超时单 %s %s %s volume=%d", order.OrderID, order.StockCode, order.Side, vol)
}
return true
}
func (o *orderBook) claimed(state *ZTState, order parsedOrder) bool {
if order.RemarkOwned {
return true
}
o.mu.Lock()
for _, s := range o.subs {
if s.Code == order.StockCode && s.Side == order.Side {
o.mu.Unlock()
return true
}
}
o.mu.Unlock()
if state == nil {
return false
}
item := state.Get(order.StockCode)
if item == nil || item.Pending == "" {
return false
}
switch item.Pending {
case "base_opening", "add":
return order.Side == "buy"
case "sell_add", "sell_base":
return order.Side == "sell"
default:
return false
}
}
func (o *orderBook) unlockSide(code, side string) {
o.mu.Lock()
defer o.mu.Unlock()
delete(o.locks(side), code)
n := 0
for _, s := range o.subs {
if s.Code == code && s.Side == side {
continue
}
o.subs[n] = s
n++
}
o.subs = o.subs[:n]
}
func (o *orderBook) sideBusy(cfg Config, code, side string, active map[string]struct{}) bool {
if _, ok := active[code]; ok {
return true
}
return o.locked(cfg, code, side)
}
func (o *orderBook) locked(cfg Config, code, side string) bool {
o.mu.Lock()
defer o.mu.Unlock()
ts, ok := o.locks(side)[code]
return ok && time.Since(ts) < cfg.OrderTimeout
}
func (o *orderBook) locks(side string) map[string]time.Time {
if side == "buy" {
return o.buyLocks
}
return o.sellLocks
}
func (o *orderBook) hasActive(ctx context.Context, client *sdk.Client, cfg Config, code, side string) bool {
orders, err := o.query(ctx, client, cfg)
if err != nil {
return true
}
for _, item := range orders {
if item.StockCode == code && item.Active && item.Side == side {
return true
}
}
return false
}
func (o *orderBook) place(ctx context.Context, client *sdk.Client, cfg Config, side, code string, volume int, tag string) bool {
if volume <= 0 || volume%100 != 0 {
logf("ERROR", "[ZT][委托] %s 拒绝非整手数量=%d", code, volume)
return false
}
if o.locked(cfg, code, side) {
logf("INFO", "[ZT][委托] %s %s锁定中", code, side)
return false
}
if o.hasActive(ctx, client, cfg, code, side) {
logf("INFO", "[ZT][委托] %s 已有%s在途委托", code, side)
return false
}
_, err := client.PassorderLatest(ctx, side == "buy", code, volume)
if err != nil {
logf("ERROR", "[ZT][委托] %s 异常: %v", code, err)
return false
}
o.mu.Lock()
o.locks(side)[code] = time.Now()
o.subs = append(o.subs, submission{Code: code, Side: side, Volume: volume, At: time.Now(), Tag: tag})
o.mu.Unlock()
logf("INFO", "[ZT][委托] 已提交 %s %s %d股 tag=%s", side, code, volume, tag)
return true
}
func parseOrder(item map[string]string) parsedOrder {
operation := asIntS(mapGet(item, "m_nOffsetFlag", "m_nOrderType", "order_type"))
status := asIntS(mapGet(item, "m_nOrderStatus", "order_status", "status"))
tag := mapGet(item, "m_strRemark", "m_strUserOrderId", "order_remark")
orderTime := int64(asIntS(mapGet(item, "m_nOrderTime", "order_time")))
if orderTime > 1e11 {
orderTime /= 1000
}
if orderTime <= 0 {
date := mapGet(item, "m_strInsertDate")
clock := strings.ReplaceAll(mapGet(item, "m_strInsertTime"), ":", "")
if date != "" {
if len(clock) < 6 {
clock = strings.Repeat("0", 6-len(clock)) + clock
}
if t, err := time.ParseInLocation("20060102150405", date+clock, time.Local); err == nil {
orderTime = t.Unix()
}
}
}
side := "sell"
if operation == opBuyStock || operation == opBuyAlt {
side = "buy"
}
left := asIntS(mapGet(item, "m_nVolumeTotal", "volume_left"))
traded := asIntS(mapGet(item, "m_nVolumeTraded", "volume_traded"))
orig := asIntS(mapGet(item, "m_nVolumeTotalOriginal", "volume"))
_, active := activeStatuses[status]
return parsedOrder{
OrderID: mapGet(item, "m_strOrderSysID", "m_nOrderID", "order_id"),
StockCode: stockCodeFromMap(item),
Side: side,
Active: active,
OrderTime: orderTime,
RemarkOwned: strings.HasPrefix(tag, "zt:"),
VolumeOrig: orig,
VolumeLeft: left,
VolumeTraded: traded,
Tag: tag,
}
}
func truthy(v any) bool {
if v == nil {
return false
}
switch x := v.(type) {
case bool:
return x
case string:
s := strings.ToLower(strings.TrimSpace(x))
return s == "true" || s == "1" || s == "yes"
case float64:
return x != 0
case int:
return x != 0
default:
s := strings.ToLower(strings.TrimSpace(fmt.Sprint(v)))
return s == "true" || s == "1"
}
}
func newOrderTag(leg string) string {
legCode := map[string]string{"base": "b", "add": "a", "take_profit": "t", "all": "s"}[leg]
if legCode == "" {
legCode = "x"
}
var buf [6]byte
_, _ = rand.Read(buf[:])
tag := fmt.Sprintf("zt:%s:%s", legCode, hex.EncodeToString(buf[:]))
if len(tag) > 24 {
return tag[:24]
}
return tag
}
func parseHM(now time.Time) int {
n, _ := strconv.Atoi(now.Format("1504"))
return n
}
func tradingTime(now time.Time) bool {
hm := parseHM(now)
return (hm >= 930 && hm <= 1130) || (hm >= 1300 && hm <= 1500)
}

View File

@@ -0,0 +1,298 @@
package main
import (
"context"
"math"
"sync"
"big-qmt/go-client/sdk"
)
var posDip = struct {
mu sync.Mutex
store map[string]dipWatch
}{store: map[string]dipWatch{}}
var peakMu sync.Mutex
var peakGrids = map[string]int{}
func peakKey(code, leg string) string { return code + "|" + leg }
func positionCodes(positions []sdk.Position) map[string]struct{} {
out := map[string]struct{}{}
for _, p := range positions {
if p.Volume <= 0 {
continue
}
code := normalizeCode(p.StockCode, "")
if code != "" {
out[code] = struct{}{}
}
}
return out
}
func managePositions(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
if positions == nil {
logf("ERROR", "[ZT][持仓] 持仓查询失败,本轮跳过")
return
}
state := getState(cfg)
if !books.cancelExpired(ctx, client, cfg) {
logf("ERROR", "[ZT][持仓] 委托查询失败,本轮跳过")
return
}
buys, sells, ok := books.activeSets(ctx, client, cfg)
if !ok {
return
}
before := map[string]struct{}{}
for _, code := range state.Codes() {
before[code] = struct{}{}
}
if ticks == nil {
ticks = map[string]sdk.Tick{}
}
logf("INFO", "[ZT][持仓] 开始处理 %d 只", len(positions))
type row struct {
volume, usable int
avg, price float64
stock string
item *SymbolState
}
rows := make([]row, 0, len(positions))
seen := map[string]struct{}{}
for _, pos := range positions {
code := normalizeCode(pos.StockCode, "")
if code == "" {
continue
}
seen[code] = struct{}{}
item := syncItem(cfg, state, code, pos.Volume, pos.OpenPrice, buys, sells, books)
if pos.Volume <= 0 {
continue
}
price := ticks[code].LastPrice
rows = append(rows, row{stock: code, volume: pos.Volume, usable: pos.CanUseVolume, avg: pos.OpenPrice, price: price, item: item})
}
for _, code := range state.Codes() {
if _, ok := seen[code]; !ok {
syncItem(cfg, state, code, 0, 0, buys, sells, books)
}
}
after := map[string]struct{}{}
for _, code := range state.Codes() {
after[code] = struct{}{}
}
for code := range before {
if _, ok := after[code]; !ok {
forget(code)
}
}
for _, r := range rows {
if r.item == nil || r.item.Pending != "" {
continue
}
if r.avg <= 0 || r.price <= 0 || r.volume%100 != 0 {
continue
}
if r.volume != r.item.BaseQty+r.item.AddQty {
logf("INFO", "[ZT][持仓] %s 数量异常,底仓=%d 补仓=%d 现有=%d", r.stock, r.item.BaseQty, r.item.AddQty, r.volume)
continue
}
holdingAdd := r.item.AddQty > 0
legName := "底仓"
if holdingAdd {
legName = "补仓腿"
}
logf("INFO", "[ZT][持仓] %s 现价=%.2f 成本=%.2f 可用=%d %s", r.stock, r.price, r.avg, r.usable, legName)
if holdingAdd {
addPnL := -999.0
if r.item.AddCost > 0 {
addPnL = (r.price - r.item.AddCost) / r.item.AddCost * 100
}
if retreated(cfg, r.item, "add", addPnL) {
sellLeg(ctx, client, books, cfg, r.item, r.usable, r.item.AddQty, "add", addPnL)
}
continue
}
basePnL := -999.0
if r.item.BaseCost > 0 {
basePnL = (r.price - r.item.BaseCost) / r.item.BaseCost * 100
}
if retreated(cfg, r.item, "base", basePnL) {
sellLeg(ctx, client, books, cfg, r.item, r.usable, r.item.BaseQty, "base", basePnL)
} else if r.item.AddQty <= 0 && r.item.AddCost <= 0 && basePnL <= cfg.LossTriggerPct {
addOnRebound(ctx, client, books, cfg, r.item, r.price, marketOK)
}
}
state.Save()
}
func syncItem(cfg Config, state *ZTState, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *orderBook) *SymbolState {
item := state.Get(code)
if item == nil {
if volume > 0 {
if cfg.AdoptExisting && avgPrice > 0 {
item = state.Ensure(code)
item.BaseQty, item.BaseCost, item.Pending = volume, avgPrice, ""
logf("WARNING", "[ZT][持仓] %s 接管为底仓", code)
return item
}
logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code)
}
return nil
}
switch item.Pending {
case "base_opening":
syncOpen(cfg, state, item, volume, avgPrice, buys, books)
case "add":
syncAdd(cfg, state, item, volume, avgPrice, buys, books)
case "sell_add":
syncSellAdd(cfg, state, item, volume, avgPrice, sells, books)
case "sell_base":
syncSellBase(cfg, state, item, volume, avgPrice, sells, books)
default:
if volume <= 0 {
state.Remove(code)
logf("INFO", "[ZT][持仓] %s 已无持仓,清除状态", code)
return nil
}
}
return state.Get(code)
}
func syncOpen(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *orderBook) {
if volume > 0 {
item.BaseQty, item.BaseCost = volume, avgPrice
}
if books.sideBusy(cfg, item.Code, "buy", buys) {
return
}
if volume <= 0 {
state.Remove(item.Code)
logf("INFO", "[ZT][委托] %s 开仓委托已失效,允许重新开仓", item.Code)
return
}
item.Pending = ""
logf("INFO", "[ZT][持仓] %s 开仓确认 数量=%d 成本=%.2f", item.Code, item.BaseQty, item.BaseCost)
}
func syncAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *orderBook) {
if volume > item.BaseQty {
item.AddQty = volume - item.BaseQty
if item.AddQty > 0 {
item.AddCost = math.Max(0, (avgPrice*float64(volume)-item.BaseCost*float64(item.BaseQty))/float64(item.AddQty))
}
}
if books.sideBusy(cfg, item.Code, "buy", buys) {
return
}
if volume <= 0 {
state.Remove(item.Code)
logf("INFO", "[ZT][委托] %s 补仓后无持仓,清除状态", item.Code)
return
}
if volume <= item.BaseQty {
item.AddQty = 0
item.AddCost = 0
logf("INFO", "[ZT][持仓] %s 补仓未成交,回退底仓", item.Code)
}
item.Pending = ""
}
func syncSellAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *orderBook) {
if volume <= 0 {
if !books.sideBusy(cfg, item.Code, "sell", sells) {
state.Remove(item.Code)
logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code)
}
return
}
if volume <= item.BaseQty {
item.BaseQty, item.BaseCost = volume, avgPrice
item.AddQty = 0
peakMu.Lock()
delete(peakGrids, peakKey(item.Code, "add"))
peakMu.Unlock()
} else {
item.AddQty = volume - item.BaseQty
}
if !books.sideBusy(cfg, item.Code, "sell", sells) {
item.Pending = ""
}
}
func syncSellBase(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *orderBook) {
if volume <= 0 {
if !books.sideBusy(cfg, item.Code, "sell", sells) {
state.Remove(item.Code)
logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code)
}
return
}
item.BaseQty, item.BaseCost = volume, avgPrice
if !books.sideBusy(cfg, item.Code, "sell", sells) {
item.Pending = ""
}
}
func addOnRebound(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, item *SymbolState, price float64, marketOK bool) {
if !marketOK || !dipTriggered(&posDip.mu, posDip.store, cfg, "补仓", item.Code, price) {
return
}
if books.place(ctx, client, cfg, "buy", item.Code, item.BaseQty, newOrderTag("add")) {
item.AddCost = price
item.Pending = "add"
getState(cfg).Save()
logf("INFO", "[ZT][补仓] %s 买入 %d 股", item.Code, item.BaseQty)
}
}
func retreated(cfg Config, item *SymbolState, leg string, pnl float64) bool {
if pnl < cfg.MinProfitPct {
return false
}
grid := int(math.Floor(pnl / cfg.GridStepPct))
key := peakKey(item.Code, leg)
peakMu.Lock()
defer peakMu.Unlock()
peak, ok := peakGrids[key]
if !ok || grid > peak {
peakGrids[key] = grid
logf("INFO", "[ZT][止盈] %s %s峰值网格=%d", item.Code, leg, grid)
return false
}
return grid < peak
}
func sellLeg(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, item *SymbolState, usable, volume int, leg string, pnl float64) {
volume -= volume % 100
if volume <= 0 || usable < volume {
logf("INFO", "[ZT][止盈] %s 可用股数不足,需要=%d 可用=%d", item.Code, volume, usable)
return
}
if !books.place(ctx, client, cfg, "sell", item.Code, volume, newOrderTag(leg)) {
return
}
if leg == "add" {
item.Pending = "sell_add"
} else {
item.Pending = "sell_base"
}
getState(cfg).Save()
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
}
func forget(code string) {
openDip.mu.Lock()
delete(openDip.store, code)
openDip.mu.Unlock()
posDip.mu.Lock()
delete(posDip.store, code)
posDip.mu.Unlock()
peakMu.Lock()
delete(peakGrids, peakKey(code, "base"))
delete(peakGrids, peakKey(code, "add"))
peakMu.Unlock()
}

278
go-client/apps/zt/remote.go Normal file
View File

@@ -0,0 +1,278 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type dailyCache struct {
Date string `json:"date"`
FetchedAt string `json:"fetched_at"`
OK bool `json:"ok"`
Data any `json:"data"`
}
var memCache sync.Map
func getJSON(rawURL string, params url.Values, timeout time.Duration) (map[string]any, error) {
if params != nil {
if strings.Contains(rawURL, "?") {
rawURL += "&" + params.Encode()
} else {
rawURL += "?" + params.Encode()
}
}
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-zt/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)))
}
out := map[string]any{}
if err := json.Unmarshal(body, &out); err != nil {
return nil, err
}
return out, nil
}
func daily(cfg Config, name, filename string, loader func() (any, error), now time.Time) *dailyCache {
if int(parseHM(now)) < cfg.ReadyCacheStart {
return nil
}
day := now.Format("20060102")
path := filepath.Join(cfg.DataDir, fmt.Sprintf(filename, day))
if v, ok := memCache.Load(path); ok {
if c, ok := v.(*dailyCache); ok && c.Date == day {
return c
}
}
cached := loadDailyFile(path)
if cached != nil && cached.Date == day {
memCache.Store(path, cached)
return cached
}
data, err := loader()
ok := err == nil
if err != nil {
logf("ERROR", "%s 当日请求失败: %v", name, err)
data = map[string]any{}
}
cached = &dailyCache{
Date: day,
FetchedAt: now.Format("2006-01-02 15:04:05"),
OK: ok,
Data: data,
}
raw, _ := json.MarshalIndent(map[string]any{"version": 1, "data": map[string]any{
"date": cached.Date, "fetched_at": cached.FetchedAt, "ok": cached.OK, "data": cached.Data,
}}, "", " ")
if err := os.WriteFile(path+".tmp", raw, 0o644); err == nil {
_ = os.Rename(path+".tmp", path)
}
memCache.Store(path, cached)
return cached
}
func loadDailyFile(path string) *dailyCache {
raw, err := os.ReadFile(path)
if err != nil {
return nil
}
var payload struct {
Version int `json:"version"`
Data map[string]any `json:"data"`
}
if json.Unmarshal(raw, &payload) != nil || payload.Version != 1 || payload.Data == nil {
return nil
}
c := &dailyCache{}
b, _ := json.Marshal(payload.Data)
if json.Unmarshal(b, c) != nil {
return nil
}
return c
}
func fetchSignal(cfg Config, name string) map[string]map[string]any {
cached := daily(cfg, name, "open_%s.json", func() (any, error) {
q := url.Values{"host_key": {cfg.HostKey}}
payload, err := getJSON(cfg.APIHost+"/a/"+name, q, cfg.HTTPTimeout)
if err != nil {
return nil, err
}
return normalizeZT(payload), nil
}, time.Now())
if cached == nil || !cached.OK {
return map[string]map[string]any{}
}
return asSignalMap(cached.Data)
}
func asSignalMap(data any) map[string]map[string]any {
out := map[string]map[string]any{}
switch v := data.(type) {
case map[string]map[string]any:
return v
case map[string]any:
for code, val := range v {
if m, ok := val.(map[string]any); ok {
out[code] = m
} else {
out[code] = map[string]any{"code": code}
}
}
}
return out
}
func normalizeZT(payload map[string]any) map[string]map[string]any {
data, _ := payload["data"]
out := map[string]map[string]any{}
switch v := data.(type) {
case []any:
for _, item := range v {
m, ok := item.(map[string]any)
if !ok {
continue
}
code, _ := m["code"].(string)
if code != "" {
out[code] = m
}
}
case map[string]any:
if code, _ := v["code"].(string); code != "" {
out[code] = v
return out
}
for code, val := range v {
if m, ok := val.(map[string]any); ok {
if _, has := m["code"]; !has {
m["code"] = code
}
out[code] = m
} else {
out[code] = map[string]any{"code": code}
}
}
}
return out
}
func passCodes(cfg Config) []string {
load := func() (any, error) {
payload, err := getJSON(cfg.APIHost+"/a/pass_codes", nil, cfg.HTTPTimeout)
if err != nil {
return nil, err
}
data, _ := payload["data"].([]any)
if data == nil {
return nil, fmt.Errorf("接口 data 不是数组")
}
codes := make([]string, 0, len(data))
for _, item := range data {
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(item)))
if s != "" && s != "<nil>" {
codes = append(codes, s)
}
}
return codes, nil
}
cached := daily(cfg, "pass_codes", "pass_codes_%s.json", load, time.Now())
codes := codesFromAny(cached)
if len(codes) > 0 {
return codes
}
logf("INFO", "pass_codes 为空,重新获取")
data, err := load()
if err != nil {
logf("ERROR", "pass_codes 重新获取失败: %v", err)
return nil
}
list, _ := data.([]string)
return list
}
func codesFromAny(cached *dailyCache) []string {
if cached == nil || !cached.OK {
return nil
}
switch v := cached.Data.(type) {
case []string:
return v
case []any:
out := make([]string, 0, len(v))
for _, item := range v {
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(item)))
if s != "" && s != "<nil>" {
out = append(out, s)
}
}
return out
}
return nil
}
func marketAllowOpen(cfg Config) bool {
payload, err := getJSON(cfg.APIHost+"/a/market", url.Values{"period": {"60m"}}, cfg.HTTPTimeout)
if err != nil {
logf("ERROR", "获取60m大盘信号失败: %s %v", cfg.APIHost+"/a/market", err)
return false
}
status := marketStatus(payload)
logf("INFO", "大盘信号: status=%s", status)
return status == "UP"
}
func marketStatus(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"
}
}

179
go-client/apps/zt/state.go Normal file
View File

@@ -0,0 +1,179 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
)
type SymbolState struct {
Code string `json:"code"`
BaseQty int `json:"base_qty"`
BaseCost float64 `json:"base_cost"`
AddQty int `json:"add_qty"`
AddCost float64 `json:"add_cost"`
Pending string `json:"pending"`
}
type filePayload struct {
Version int `json:"version"`
Data map[string]any `json:"data"`
}
type ZTState struct {
path string
Items map[string]*SymbolState
LoadError string
mu sync.Mutex
}
var (
statesMu sync.Mutex
states = map[string]*ZTState{}
)
func getState(cfg Config) *ZTState {
statesMu.Lock()
defer statesMu.Unlock()
if s, ok := states[cfg.AccountID]; ok {
return s
}
s := loadZTState(cfg.DataDir, cfg.AccountID)
states[cfg.AccountID] = s
return s
}
func loadZTState(dataDir, accountID string) *ZTState {
st := &ZTState{
path: filepath.Join(dataDir, fmt.Sprintf("zt_%s_state.json", accountID)),
Items: map[string]*SymbolState{},
}
raw, err := os.ReadFile(st.path)
if err != nil {
if os.IsNotExist(err) {
return st
}
st.rebuild(err)
return st
}
var payload filePayload
if err := json.Unmarshal(raw, &payload); err != nil || payload.Version != 1 {
st.rebuild(fmt.Errorf("状态文件版本无效"))
return st
}
data := payload.Data
if data == nil {
st.rebuild(fmt.Errorf("状态文件内容无效"))
return st
}
symbolsAny, _ := data["symbols"]
symbols, _ := symbolsAny.(map[string]any)
if symbols == nil {
if _, ok := data["code"]; ok {
symbols = map[string]any{}
} else {
symbols = data
}
}
for code, value := range symbols {
m, ok := value.(map[string]any)
if !ok {
continue
}
item := &SymbolState{Code: code}
b, _ := json.Marshal(m)
_ = json.Unmarshal(b, item)
item.Code = code
st.Items[code] = item
}
return st
}
func (s *ZTState) rebuild(err error) {
if err := os.Remove(s.path); err != nil && !os.IsNotExist(err) {
s.LoadError = err.Error()
logf("ERROR", "[ZT][状态] 状态文件重建失败: %s", s.LoadError)
return
}
s.Items = map[string]*SymbolState{}
if saveErr := s.saveUnlocked(); saveErr != nil {
s.LoadError = fmt.Sprintf("%v重建失败: %v", err, saveErr)
logf("ERROR", "[ZT][状态] 状态文件重建失败: %s", s.LoadError)
return
}
logf("WARNING", "[ZT][状态] 状态文件损坏,已删除并重建: %v", err)
}
func (s *ZTState) Get(code string) *SymbolState {
s.mu.Lock()
defer s.mu.Unlock()
return s.Items[code]
}
func (s *ZTState) Ensure(code string) *SymbolState {
s.mu.Lock()
defer s.mu.Unlock()
if item, ok := s.Items[code]; ok {
return item
}
item := &SymbolState{Code: code}
s.Items[code] = item
return item
}
func (s *ZTState) Remove(code string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.Items, code)
}
func (s *ZTState) Codes() []string {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]string, 0, len(s.Items))
for code := range s.Items {
out = append(out, code)
}
return out
}
func (s *ZTState) Save() {
s.mu.Lock()
defer s.mu.Unlock()
if s.LoadError != "" {
return
}
if err := s.saveUnlocked(); err != nil {
logf("ERROR", "[ZT][状态] 保存失败: %v", err)
}
}
func (s *ZTState) saveUnlocked() error {
symbols := map[string]any{}
for code, item := range s.Items {
symbols[code] = item
}
payload := filePayload{Version: 1, Data: map[string]any{"symbols": symbols}}
raw, err := json.Marshal(payload)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, raw, 0o644); err != nil {
return err
}
return replaceFile(tmp, s.path)
}
func replaceFile(tmp, dest string) error {
if err := os.Rename(tmp, dest); err == nil {
return nil
}
_ = os.Remove(dest)
return os.Rename(tmp, dest)
}

104
go-client/apps/zt/stock.go Normal file
View File

@@ -0,0 +1,104 @@
package main
import (
"fmt"
"strings"
"unicode"
)
var exchangeAlias = map[string]string{
"SSE": "SH", "SHSE": "SH", "XSHG": "SH",
"SZSE": "SZ", "XSHE": "SZ",
"BSE": "BJ", "BJSE": "BJ",
}
func stockCodeFromMap(item map[string]string) string {
code := strings.ToUpper(strings.TrimSpace(mapGet(item, "m_strInstrumentID", "StockCode", "stock_code", "code")))
ex := mapGet(item, "m_strExchangeID", "exchange", "exchange_id")
return normalizeCode(code, ex)
}
func normalizeCode(code, exchange string) string {
code = strings.ToUpper(strings.TrimSpace(code))
if code == "" {
return ""
}
if i := strings.LastIndex(code, "."); i >= 0 {
symbol, ex := code[:i], code[i+1:]
ex = canonExchange(ex)
if ex == "SH" || ex == "SZ" || ex == "BJ" {
return symbol + "." + ex
}
return ""
}
ex := canonExchange(exchange)
if ex == "" && looksDigits(code, 6) {
switch {
case strings.HasPrefix(code, "92") || code[0] == '4' || code[0] == '8':
ex = "BJ"
case code[0] == '5' || code[0] == '6' || code[0] == '9' || strings.HasPrefix(code, "11"):
ex = "SH"
case code[0] == '0' || code[0] == '1' || code[0] == '2' || code[0] == '3':
ex = "SZ"
}
}
if ex == "SH" || ex == "SZ" || ex == "BJ" {
return code + "." + ex
}
return ""
}
func canonExchange(ex string) string {
ex = strings.ToUpper(strings.TrimSpace(ex))
if v, ok := exchangeAlias[ex]; ok {
return v
}
return ex
}
func looksDigits(s string, n int) bool {
if len(s) != n {
return false
}
for _, r := range s {
if !unicode.IsDigit(r) {
return false
}
}
return true
}
func mapGet(item map[string]string, names ...string) string {
for _, name := range names {
if v := strings.TrimSpace(item[name]); v != "" {
return v
}
}
return ""
}
func asIntS(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
var n int
_, _ = fmt.Sscanf(s, "%d", &n)
if n == 0 {
var f float64
if _, err := fmt.Sscanf(s, "%f", &f); err == nil {
return int(f)
}
}
return n
}
func asFloatS(s string) float64 {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
var f float64
_, _ = fmt.Sscanf(s, "%f", &f)
return f
}

3
go-client/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module big-qmt/go-client
go 1.22

193
go-client/sdk/account.go Normal file
View File

@@ -0,0 +1,193 @@
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"`
}
// Assets 对应 /api/v2/assets。
type Assets struct {
Total float64 `json:"total"`
Available float64 `json:"available"`
}
type accountBody struct {
Account string `json:"account"`
}
func (c *Client) Positions(ctx context.Context, account string) ([]Position, error) {
raw := map[string]json.RawMessage{}
if err := c.post(ctx, "/api/v2/positions", accountBody{Account: c.Account(account)}, &raw); err != nil {
return nil, err
}
out := make([]Position, 0, len(raw))
for code, blob := range raw {
var p Position
if err := json.Unmarshal(blob, &p); err != nil {
return nil, fmt.Errorf("position %s: %w", code, err)
}
if p.StockCode == "" {
p.StockCode = code
}
out = append(out, p)
}
return out, nil
}
func (c *Client) Holding(ctx context.Context, account string) ([]Position, error) {
raw := map[string]json.RawMessage{}
if err := c.post(ctx, "/api/holding", accountBody{Account: c.Account(account)}, &raw); err != nil {
return nil, err
}
out := make([]Position, 0, len(raw))
for code, blob := range raw {
var p Position
if err := json.Unmarshal(blob, &p); err != nil {
return nil, fmt.Errorf("holding %s: %w", code, err)
}
if p.StockCode == "" {
p.StockCode = code
}
out = append(out, p)
}
return out, nil
}
func (c *Client) Assets(ctx context.Context, account string) (*Assets, error) {
var out Assets
if err := c.post(ctx, "/api/v2/assets", accountBody{Account: c.Account(account)}, &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) TotalMoney(ctx context.Context, account string) (float64, error) {
var out struct {
TotalMoney float64 `json:"total_money"`
}
if err := c.post(ctx, "/api/money/total", accountBody{Account: c.Account(account)}, &out); err != nil {
return 0, err
}
return out.TotalMoney, nil
}
func (c *Client) AvailableMoney(ctx context.Context, account string) (float64, error) {
var out struct {
AvailableMoney float64 `json:"available_money"`
}
if err := c.post(ctx, "/api/money/available", accountBody{Account: c.Account(account)}, &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, account string) ([]OrderStatus, error) {
var out struct {
Orders []OrderStatus `json:"orders"`
}
if err := c.post(ctx, "/api/order/status", accountBody{Account: c.Account(account)}, &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, account string) (*CancelAllResult, error) {
var out CancelAllResult
if err := c.post(ctx, "/api/order/cancel_all", accountBody{Account: c.Account(account)}, &out); err != nil {
return nil, err
}
return &out, nil
}
// CancelByRule 按「代码.市场 + (剩余+已成)」匹配撤单。HTTP 没有按委托号撤单ZT 用它代替 cancel(orderId)。
func (c *Client) CancelByRule(ctx context.Context, stock string, volume int, account string) (*CancelAllResult, error) {
var out CancelAllResult
body := map[string]any{"stock": stock, "volume": volume, "account": c.Account(account)}
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, account string) ([]map[string]string, error) {
var out struct {
Deals []map[string]string `json:"deals"`
}
if err := c.post(ctx, "/api/order/deal", accountBody{Account: c.Account(account)}, &out); err != nil {
return nil, err
}
return out.Deals, nil
}

67
go-client/sdk/check.go Normal file
View File

@@ -0,0 +1,67 @@
package sdk
import "context"
func (c *Client) IsLastBar(ctx context.Context) (any, error) {
var out struct {
IsLastBar any `json:"is_last_bar"`
}
if err := c.get(ctx, "/api/check/is_last_bar", &out); err != nil {
return nil, err
}
return out.IsLastBar, nil
}
func (c *Client) IsNewBar(ctx context.Context) (any, error) {
var out struct {
IsNewBar any `json:"is_new_bar"`
}
if err := c.get(ctx, "/api/check/is_new_bar", &out); err != nil {
return nil, err
}
return out.IsNewBar, nil
}
func (c *Client) IsSuspendedStock(ctx context.Context, stockcode string) (any, error) {
var out struct {
Stockcode string `json:"stockcode"`
IsSuspended any `json:"is_suspended"`
}
if err := c.post(ctx, "/api/check/is_suspended_stock", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.IsSuspended, nil
}
func (c *Client) IsSectorStock(ctx context.Context, sectorname, market, stockcode string) (any, error) {
var out struct {
IsInSector any `json:"is_in_sector"`
}
body := map[string]any{"sectorname": sectorname, "market": market, "stockcode": stockcode}
if err := c.post(ctx, "/api/check/is_sector_stock", body, &out); err != nil {
return nil, err
}
return out.IsInSector, nil
}
func (c *Client) IsTypedStock(ctx context.Context, stocktypenum int, market, stockcode string) (any, error) {
var out struct {
Result any `json:"result"`
}
body := map[string]any{"stocktypenum": stocktypenum, "market": market, "stockcode": stockcode}
if err := c.post(ctx, "/api/check/is_typed_stock", body, &out); err != nil {
return nil, err
}
return out.Result, nil
}
func (c *Client) IndustryNameOfStock(ctx context.Context, industryType, stockcode string) (any, error) {
var out struct {
IndustryName any `json:"industry_name"`
}
body := map[string]any{"industryType": industryType, "stockcode": stockcode}
if err := c.post(ctx, "/api/check/get_industry_name_of_stock", body, &out); err != nil {
return nil, err
}
return out.IndustryName, nil
}

111
go-client/sdk/client.go Normal file
View File

@@ -0,0 +1,111 @@
package sdk
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// Client 调用 QMT HTTP API。
type Client struct {
baseURL string
token string
accountType string
http *http.Client
}
func New(baseURL, token, accountType string, timeout time.Duration) *Client {
base := strings.TrimRight(baseURL, "/")
return &Client{baseURL: base, token: token, accountType: accountType, http: &http.Client{Timeout: timeout}}
}
func (c *Client) Account(override string) string {
if strings.TrimSpace(override) != "" {
return override
}
return c.accountType
}
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 truncate(b []byte, n int) string {
if len(b) <= n {
return string(b)
}
return string(b[:n]) + "..."
}
func ArrayJoin(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, ",")
}

94
go-client/sdk/coerce.go Normal file
View File

@@ -0,0 +1,94 @@
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()
case []byte:
return string(x)
default:
return strings.TrimSpace(fmtAny(v))
}
}
func fmtAny(v any) string {
b, err := json.Marshal(v)
if err != nil {
return ""
}
s := strings.Trim(string(b), `"`)
return s
}
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 asInt(v any) int {
return int(asFloat(v))
}
func asBool(v any) bool {
switch x := v.(type) {
case bool:
return x
case string:
s := strings.ToLower(strings.TrimSpace(x))
return s == "true" || s == "1" || s == "yes"
default:
return asFloat(v) != 0
}
}
func mapField(m map[string]any, names ...string) any {
for _, name := range names {
if name == "" {
continue
}
if v, ok := m[name]; ok && v != nil {
return v
}
}
return nil
}
func mapFieldS(m map[string]string, names ...string) string {
for _, name := range names {
if v, ok := m[name]; ok && strings.TrimSpace(v) != "" {
return v
}
}
return ""
}

136
go-client/sdk/context.go Normal file
View File

@@ -0,0 +1,136 @@
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) {
var out struct {
Period any `json:"period"`
}
if err := c.get(ctx, "/api/context/period", &out); err != nil {
return nil, err
}
return out.Period, nil
}
func (c *Client) ContextBarpos(ctx context.Context) (any, error) {
var out struct {
Barpos any `json:"barpos"`
}
if err := c.get(ctx, "/api/context/barpos", &out); err != nil {
return nil, err
}
return out.Barpos, nil
}
func (c *Client) ContextTimeTickSize(ctx context.Context) (any, error) {
var out struct {
TimeTickSize any `json:"time_tick_size"`
}
if err := c.get(ctx, "/api/context/time_tick_size", &out); err != nil {
return nil, err
}
return out.TimeTickSize, nil
}
func (c *Client) ContextStockcode(ctx context.Context) (any, error) {
var out struct {
Stockcode any `json:"stockcode"`
}
if err := c.get(ctx, "/api/context/stockcode", &out); err != nil {
return nil, err
}
return out.Stockcode, nil
}
func (c *Client) ContextDividendType(ctx context.Context) (any, error) {
var out struct {
DividendType any `json:"dividend_type"`
}
if err := c.get(ctx, "/api/context/dividend_type", &out); err != nil {
return nil, err
}
return out.DividendType, nil
}
func (c *Client) ContextMarket(ctx context.Context) (any, error) {
var out struct {
Market any `json:"market"`
}
if err := c.get(ctx, "/api/context/market", &out); err != nil {
return nil, err
}
return out.Market, nil
}
func (c *Client) ContextDoBackTest(ctx context.Context) (any, error) {
var out struct {
DoBackTest any `json:"do_back_test"`
}
if err := c.get(ctx, "/api/context/do_back_test", &out); err != nil {
return nil, err
}
return out.DoBackTest, nil
}
func (c *Client) ContextBenchmark(ctx context.Context) (any, error) {
var out struct {
Benchmark any `json:"benchmark"`
}
if err := c.get(ctx, "/api/context/benchmark", &out); err != nil {
return nil, err
}
return out.Benchmark, nil
}
func (c *Client) ContextCapital(ctx context.Context) (any, error) {
var out struct {
Capital any `json:"capital"`
}
if err := c.get(ctx, "/api/context/capital", &out); err != nil {
return nil, err
}
return out.Capital, nil
}
func (c *Client) ContextUniverse(ctx context.Context) ([]string, error) {
var out struct {
Universe any `json:"universe"`
}
if err := c.get(ctx, "/api/context/universe", &out); err != nil {
return nil, err
}
switch v := out.Universe.(type) {
case nil:
return nil, nil
case []any:
codes := make([]string, 0, len(v))
for _, item := range v {
s := asString(item)
if s != "" {
codes = append(codes, s)
}
}
return codes, nil
case []string:
return v, nil
default:
s := asString(v)
if s == "" {
return nil, nil
}
return []string{s}, nil
}
}

581
go-client/sdk/data.go Normal file
View File

@@ -0,0 +1,581 @@
package sdk
import (
"context"
"fmt"
)
type Tick struct {
LastPrice float64
LastClose float64
Raw map[string]any
}
type HistoryDataRequest struct {
Len int `json:"len"`
Period string `json:"period,omitempty"`
Field string `json:"field,omitempty"`
DividendType int `json:"dividend_type"`
SkipPaused string `json:"skip_paused,omitempty"`
}
type MarketDataRequest struct {
Fields string `json:"fields,omitempty"`
StockCode string `json:"stock_code,omitempty"`
StartTime string `json:"start_time,omitempty"`
EndTime string `json:"end_time,omitempty"`
Period string `json:"period,omitempty"`
DividendType string `json:"dividend_type,omitempty"`
Count int `json:"count"`
}
type SubscribeResult struct {
Status string `json:"status"`
SubID any `json:"sub_id"`
}
func (c *Client) StockName(ctx context.Context, stockcode string) (any, error) {
var out struct {
Name any `json:"name"`
}
if err := c.post(ctx, "/api/data/stock_name", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.Name, nil
}
func (c *Client) OpenDate(ctx context.Context, stockcode string) (any, error) {
var out struct {
OpenDate any `json:"open_date"`
}
if err := c.post(ctx, "/api/data/open_date", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.OpenDate, nil
}
func (c *Client) LastVolume(ctx context.Context, stockcode string) (any, error) {
var out struct {
LastVolume any `json:"last_volume"`
}
if err := c.post(ctx, "/api/data/last_volume", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.LastVolume, nil
}
func (c *Client) BarTimetag(ctx context.Context, index int) (any, error) {
var out struct {
Timetag any `json:"timetag"`
}
if err := c.post(ctx, "/api/data/bar_timetag", map[string]any{"index": index}, &out); err != nil {
return nil, err
}
return out.Timetag, nil
}
func (c *Client) TickTimetag(ctx context.Context) (any, error) {
var out struct {
Timetag any `json:"timetag"`
}
if err := c.get(ctx, "/api/data/tick_timetag", &out); err != nil {
return nil, err
}
return out.Timetag, nil
}
func (c *Client) Sector(ctx context.Context, sector string, realtime string) ([]any, error) {
body := map[string]any{"sector": sector}
if realtime != "" {
body["realtime"] = realtime
}
var out struct {
Stocks []any `json:"stocks"`
}
if err := c.post(ctx, "/api/data/sector", body, &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) {
var out struct {
Weight any `json:"weight"`
}
body := map[string]any{"indexcode": indexcode, "stockcode": stockcode}
if err := c.post(ctx, "/api/data/weight_in_index", body, &out); err != nil {
return nil, err
}
return out.Weight, nil
}
func (c *Client) ContractMultiplier(ctx context.Context, contractcode string) (any, error) {
var out struct {
Multiplier any `json:"multiplier"`
}
if err := c.post(ctx, "/api/data/contract_multiplier", map[string]any{"contractcode": contractcode}, &out); err != nil {
return nil, err
}
return out.Multiplier, nil
}
func (c *Client) RiskFreeRate(ctx context.Context, index int) (any, error) {
var out struct {
RiskFreeRate any `json:"risk_free_rate"`
}
if err := c.post(ctx, "/api/data/risk_free_rate", map[string]any{"index": index}, &out); err != nil {
return nil, err
}
return out.RiskFreeRate, nil
}
func (c *Client) DateLocation(ctx context.Context, strdate string) (any, error) {
var out struct {
Location any `json:"location"`
}
if err := c.post(ctx, "/api/data/date_location", map[string]any{"strdate": strdate}, &out); err != nil {
return nil, err
}
return out.Location, nil
}
func (c *Client) HistoryData(ctx context.Context, req HistoryDataRequest) (any, error) {
if req.Len == 0 {
req.Len = 10
}
if req.SkipPaused == "" {
req.SkipPaused = "true"
}
var out map[string]any
if err := c.post(ctx, "/api/data/history_data", req, &out); err != nil {
return nil, err
}
if msg, ok := out["error"].(string); ok && msg != "" {
return nil, &BusinessError{Message: msg}
}
return out["data"], nil
}
func (c *Client) MarketData(ctx context.Context, req MarketDataRequest) (any, error) {
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/data/market_data", req, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) MarketDataEx(ctx context.Context, req MarketDataRequest) (any, error) {
body := map[string]any{
"fields": req.Fields,
"stock_code": req.StockCode,
"period": req.Period,
"start_time": req.StartTime,
"end_time": req.EndTime,
"count": req.Count,
"dividend_type": req.DividendType,
}
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/data/market_data_ex", body, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) FullTick(ctx context.Context, stocks []string) (map[string]Tick, error) {
joined := ArrayJoin(stocks)
if joined == "" {
return nil, fmt.Errorf("full_tick: stocks empty")
}
raw := map[string]any{}
if err := c.post(ctx, "/api/data/full_tick", map[string]any{"stocks": joined}, &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) {
var out struct {
Factors any `json:"factors"`
}
if err := c.post(ctx, "/api/data/divid_factors", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.Factors, nil
}
func (c *Client) MainContract(ctx context.Context, codemarket string) (any, error) {
var out struct {
MainContract any `json:"main_contract"`
}
if err := c.post(ctx, "/api/data/main_contract", map[string]any{"codemarket": codemarket}, &out); err != nil {
return nil, err
}
return out.MainContract, nil
}
func (c *Client) TimetagToDatetime(ctx context.Context, timetag int64, format string) (any, error) {
body := map[string]any{"timetag": timetag}
if format != "" {
body["format"] = format
}
var out struct {
Datetime any `json:"datetime"`
}
if err := c.post(ctx, "/api/data/timetag_to_datetime", body, &out); err != nil {
return nil, err
}
return out.Datetime, nil
}
func (c *Client) TotalShare(ctx context.Context, stockcode string) (any, error) {
var out struct {
TotalShare any `json:"total_share"`
}
if err := c.post(ctx, "/api/data/total_share", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.TotalShare, nil
}
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) {
var out struct {
Svol any `json:"svol"`
}
if err := c.post(ctx, "/api/data/svol", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.Svol, nil
}
func (c *Client) Bvol(ctx context.Context, stockcode string) (any, error) {
var out struct {
Bvol any `json:"bvol"`
}
if err := c.post(ctx, "/api/data/bvol", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.Bvol, nil
}
func (c *Client) dataPayload(ctx context.Context, path string, body map[string]any) (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 v, ok := out["data"]; ok {
return v, nil
}
return out, nil
}
func (c *Client) Longhubang(ctx context.Context, stockList, startTime, endTime string) (any, error) {
return c.dataPayload(ctx, "/api/data/longhubang", map[string]any{
"stock_list": stockList, "startTime": startTime, "endTime": endTime,
})
}
func (c *Client) Top10ShareHolder(ctx context.Context, stockList, dataName, startTime, endTime string) (any, error) {
return c.dataPayload(ctx, "/api/data/top10_share_holder", map[string]any{
"stock_list": stockList, "data_name": dataName, "start_time": startTime, "end_time": endTime,
})
}
func (c *Client) OptionDetail(ctx context.Context, optioncode string) (any, error) {
var out struct {
Detail any `json:"detail"`
}
if err := c.post(ctx, "/api/data/option_detail", map[string]any{"optioncode": optioncode}, &out); err != nil {
return nil, err
}
return out.Detail, nil
}
func (c *Client) TurnoverRate(ctx context.Context, stockList, startTime, endTime string) (any, error) {
return c.dataPayload(ctx, "/api/data/turnover_rate", map[string]any{
"stock_list": stockList, "startTime": startTime, "endTime": endTime,
})
}
func (c *Client) ETFInfo(ctx context.Context, stockcode string) (any, error) {
var out struct {
Info any `json:"info"`
}
if err := c.post(ctx, "/api/data/etf_info", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.Info, nil
}
func (c *Client) ETFIOPV(ctx context.Context, stockcode string) (any, error) {
var out struct {
IOPV any `json:"iopv"`
}
if err := c.post(ctx, "/api/data/etf_iopv", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.IOPV, nil
}
func (c *Client) InstrumentDetail(ctx context.Context, stockcode string) (any, error) {
var out struct {
Detail any `json:"detail"`
}
if err := c.post(ctx, "/api/data/instrumentdetail", map[string]any{"stockcode": stockcode}, &out); err != nil {
return nil, err
}
return out.Detail, nil
}
func (c *Client) ContractExpireDate(ctx context.Context, codemarket string) (any, error) {
var out struct {
ExpireDate any `json:"expire_date"`
}
if err := c.post(ctx, "/api/data/contract_expire_date", map[string]any{"codemarket": codemarket}, &out); err != nil {
return nil, err
}
return out.ExpireDate, nil
}
func (c *Client) OptionUndlData(ctx context.Context, undlCodeRef string) (any, error) {
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/data/option_undl_data", map[string]any{"undl_code_ref": undlCodeRef}, &out); err != nil {
return nil, err
}
return out.Data, nil
}
type FinancialDataRequest struct {
Tabname string `json:"tabname,omitempty"`
Colname string `json:"colname,omitempty"`
Market string `json:"market,omitempty"`
Code string `json:"code,omitempty"`
ReportType string `json:"report_type,omitempty"`
Barpos int `json:"barpos"`
FieldList string `json:"fieldList,omitempty"`
StockList string `json:"stockList,omitempty"`
StartDate string `json:"startDate,omitempty"`
EndDate string `json:"endDate,omitempty"`
}
func (c *Client) FinancialData(ctx context.Context, req FinancialDataRequest) (any, error) {
var out map[string]any
if err := c.post(ctx, "/api/data/financial_data", req, &out); err != nil {
return nil, err
}
if msg, ok := out["error"].(string); ok && msg != "" {
return nil, &BusinessError{Message: msg}
}
return out["data"], nil
}
type FactorDataRequest struct {
FieldList string `json:"fieldList,omitempty"`
StockList string `json:"stockList,omitempty"`
StockCode string `json:"stockCode,omitempty"`
StartDate string `json:"startDate,omitempty"`
EndDate string `json:"endDate,omitempty"`
}
func (c *Client) FactorData(ctx context.Context, req FactorDataRequest) (any, error) {
var out map[string]any
if err := c.post(ctx, "/api/data/factor_data", req, &out); err != nil {
return nil, err
}
if msg, ok := out["error"].(string); ok && msg != "" {
return nil, &BusinessError{Message: msg}
}
return out["data"], nil
}
func (c *Client) HisSTData(ctx context.Context, stockCode string) (any, error) {
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/data/his_st_data", map[string]any{"stockCode": stockCode}, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) HisIndexData(ctx context.Context, index string) (any, error) {
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/data/his_index_data", map[string]any{"index": index}, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) AllSubscription(ctx context.Context) (any, error) {
var out struct {
Subscriptions any `json:"subscriptions"`
}
if err := c.get(ctx, "/api/data/all_subscription", &out); err != nil {
return nil, err
}
return out.Subscriptions, nil
}
func (c *Client) OptionList(ctx context.Context, undlCode, dedate, opttype, isavailable string) (any, error) {
body := map[string]any{"undl_code": undlCode, "dedate": dedate, "opttype": opttype}
if isavailable != "" {
body["isavailable"] = isavailable
}
var out struct {
OptionList any `json:"option_list"`
}
if err := c.post(ctx, "/api/data/option_list", body, &out); err != nil {
return nil, err
}
return out.OptionList, nil
}
func (c *Client) HisContractList(ctx context.Context, market string) (any, error) {
var out struct {
Contracts any `json:"contracts"`
}
if err := c.post(ctx, "/api/data/his_contract_list", map[string]any{"market": market}, &out); err != nil {
return nil, err
}
return out.Contracts, nil
}
func (c *Client) OptionIV(ctx context.Context, optioncode string) (any, error) {
var out struct {
IV any `json:"iv"`
}
if err := c.post(ctx, "/api/data/option_iv", map[string]any{"optioncode": optioncode}, &out); err != nil {
return nil, err
}
return out.IV, nil
}
type BSMPriceRequest struct {
OptionType string `json:"optionType"`
ObjectPrices string `json:"objectPrices"`
StrikePrice float64 `json:"strikePrice"`
RiskFree float64 `json:"riskFree"`
Sigma float64 `json:"sigma"`
Days int `json:"days"`
Dividend float64 `json:"dividend"`
}
func (c *Client) BSMPrice(ctx context.Context, req BSMPriceRequest) (any, error) {
var out struct {
Price any `json:"price"`
}
if err := c.post(ctx, "/api/data/bsm_price", req, &out); err != nil {
return nil, err
}
return out.Price, nil
}
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) {
var out struct {
IV any `json:"iv"`
}
if err := c.post(ctx, "/api/data/bsm_iv", req, &out); err != nil {
return nil, err
}
return out.IV, nil
}
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) {
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/data/local_data", req, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) SubscribeQuote(ctx context.Context, stockCode, period, dividendType string) (*SubscribeResult, error) {
body := map[string]any{"stock_code": stockCode, "period": period, "dividend_type": dividendType}
var out SubscribeResult
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
}

4
go-client/sdk/doc.go Normal file
View File

@@ -0,0 +1,4 @@
// Package sdk 是 QMT_API.py HTTP 服务的 Go 客户端。
//
// 默认地址 http://127.0.0.1:10086所有已注册接口都需要请求头 X-Token。
package sdk

38
go-client/sdk/error.go Normal file
View File

@@ -0,0 +1,38 @@
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
}

47
go-client/sdk/ext.go Normal file
View File

@@ -0,0 +1,47 @@
package sdk
import "context"
func (c *Client) ExtData(ctx context.Context, extdataname, stockcode string, deviation int) (any, error) {
var out struct {
Value any `json:"value"`
}
body := map[string]any{"extdataname": extdataname, "stockcode": stockcode, "deviation": deviation}
if err := c.post(ctx, "/api/ext/ext_data", body, &out); err != nil {
return nil, err
}
return out.Value, nil
}
func (c *Client) ExtDataRank(ctx context.Context, extdataname, stockcode string, deviation int) (any, error) {
var out struct {
Rank any `json:"rank"`
}
body := map[string]any{"extdataname": extdataname, "stockcode": stockcode, "deviation": deviation}
if err := c.post(ctx, "/api/ext/ext_data_rank", body, &out); err != nil {
return nil, err
}
return out.Rank, nil
}
func (c *Client) GetFactorValue(ctx context.Context, factorname, stockcode string, deviation int) (any, error) {
var out struct {
Value any `json:"value"`
}
body := map[string]any{"factorname": factorname, "stockcode": stockcode, "deviation": deviation}
if err := c.post(ctx, "/api/ext/get_factor_value", body, &out); err != nil {
return nil, err
}
return out.Value, nil
}
func (c *Client) GetFactorRank(ctx context.Context, factorname, stockcode string, deviation int) (any, error) {
var out struct {
Rank any `json:"rank"`
}
body := map[string]any{"factorname": factorname, "stockcode": stockcode, "deviation": deviation}
if err := c.post(ctx, "/api/ext/get_factor_rank", body, &out); err != nil {
return nil, err
}
return out.Rank, nil
}

30
go-client/sdk/sys.go Normal file
View File

@@ -0,0 +1,30 @@
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
}

302
go-client/sdk/trade.go Normal file
View File

@@ -0,0 +1,302 @@
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"`
}
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 按最新价下单。服务端策略名写死为 qmt无法传投资备注。
func (c *Client) PassorderLatest(ctx context.Context, buy bool, stock string, volume int) (*OrderRefResult, error) {
op := OpSell
if buy {
op = OpBuy
}
return c.Passorder(ctx, PassorderRequest{
OpType: op,
OrderType: OrderTypeVolume,
Stock: stock,
PrType: PrTypeLatest,
Price: -1,
Volume: volume,
QuickTrade: QuickTradeNow,
})
}
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, accID string) (*StyleOrderResult, error) {
return c.styleOrder(ctx, "/api/trade/order_lots", styleBody(stock, style, price, accID, "lots", lots))
}
func (c *Client) OrderValue(ctx context.Context, stock string, value float64, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.styleOrder(ctx, "/api/trade/order_value", styleBody(stock, style, price, accID, "value", value))
}
func (c *Client) OrderPercent(ctx context.Context, stock string, percent float64, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.styleOrder(ctx, "/api/trade/order_percent", styleBody(stock, style, price, accID, "percent", percent))
}
func (c *Client) OrderTargetValue(ctx context.Context, stock string, tarValue float64, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.styleOrder(ctx, "/api/trade/order_target_value", styleBody(stock, style, price, accID, "tar_value", tarValue))
}
func (c *Client) OrderTargetPercent(ctx context.Context, stock string, tarPercent float64, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.styleOrder(ctx, "/api/trade/order_target_percent", styleBody(stock, style, price, accID, "tar_percent", tarPercent))
}
func (c *Client) OrderShares(ctx context.Context, stock string, shares int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.styleOrder(ctx, "/api/trade/order_shares", styleBody(stock, style, price, accID, "shares", shares))
}
func styleBody(stock, style string, price float64, accID, key string, val any) map[string]any {
body := map[string]any{"stock": stock, key: val, "price": price}
if style != "" {
body["style"] = style
}
if accID != "" {
body["accId"] = accID
}
return body
}
func (c *Client) futures(ctx context.Context, path, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
body := map[string]any{"stock": stock, "amount": amount, "price": price}
if style != "" {
body["style"] = style
}
if accID != "" {
body["accId"] = accID
}
return c.styleOrder(ctx, path, body)
}
func (c *Client) FuturesBuyOpen(ctx context.Context, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.futures(ctx, "/api/trade/futures/buy_open", stock, amount, style, price, accID)
}
func (c *Client) FuturesBuyCloseTdayFirst(ctx context.Context, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.futures(ctx, "/api/trade/futures/buy_close_tdayfirst", stock, amount, style, price, accID)
}
func (c *Client) FuturesBuyCloseYdayFirst(ctx context.Context, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.futures(ctx, "/api/trade/futures/buy_close_ydayfirst", stock, amount, style, price, accID)
}
func (c *Client) FuturesSellOpen(ctx context.Context, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.futures(ctx, "/api/trade/futures/sell_open", stock, amount, style, price, accID)
}
func (c *Client) FuturesSellCloseTdayFirst(ctx context.Context, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.futures(ctx, "/api/trade/futures/sell_close_tdayfirst", stock, amount, style, price, accID)
}
func (c *Client) FuturesSellCloseYdayFirst(ctx context.Context, stock string, amount int, style string, price float64, accID string) (*StyleOrderResult, error) {
return c.futures(ctx, "/api/trade/futures/sell_close_ydayfirst", stock, amount, style, price, accID)
}
type TaskResult struct {
Status string `json:"status"`
TaskID any `json:"taskId"`
}
func (c *Client) task(ctx context.Context, path, taskID, accountType string) (*TaskResult, error) {
body := map[string]any{"taskId": taskID}
if accountType != "" {
body["accountType"] = accountType
}
var out TaskResult
if err := c.post(ctx, path, body, &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) CancelTask(ctx context.Context, taskID, accountType string) (*TaskResult, error) {
return c.task(ctx, "/api/trade/cancel_task", taskID, accountType)
}
func (c *Client) PauseTask(ctx context.Context, taskID, accountType string) (*TaskResult, error) {
return c.task(ctx, "/api/trade/pause_task", taskID, accountType)
}
func (c *Client) ResumeTask(ctx context.Context, taskID, accountType string) (*TaskResult, error) {
return c.task(ctx, "/api/trade/resume_task", taskID, accountType)
}
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", map[string]any{}, &out); err != nil {
return nil, err
}
return out, nil
}
func (c *Client) TradeDetailData(ctx context.Context, account, datatype string) ([]map[string]string, error) {
body := map[string]any{
"account": c.Account(account),
"datatype": datatype,
}
var out struct {
Data []map[string]string `json:"data"`
}
if err := c.post(ctx, "/api/trade/trade_detail_data", body, &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, accountType, datatype string) (map[string]string, error) {
body := map[string]any{"orderId": orderID, "accountType": accountType, "datatype": datatype}
var out struct {
OrderID string `json:"orderId"`
Data map[string]string `json:"data"`
}
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, account, datatype string) (any, error) {
body := map[string]any{"account": c.Account(account), "datatype": datatype}
var out struct {
LastOrderID any `json:"last_order_id"`
}
if err := c.post(ctx, "/api/trade/last_order_id", body, &out); err != nil {
return nil, err
}
return out.LastOrderID, nil
}
func (c *Client) CanCancelOrder(ctx context.Context, orderID, accountType string) (any, error) {
body := map[string]any{"orderId": orderID, "accountType": accountType}
var out struct {
CanCancel any `json:"can_cancel"`
}
if err := c.post(ctx, "/api/trade/can_cancel_order", body, &out); err != nil {
return nil, err
}
return out.CanCancel, nil
}
func (c *Client) contractList(ctx context.Context, path, accID string) ([]map[string]string, error) {
body := map[string]any{}
if accID != "" {
body["accId"] = accID
}
var out struct {
Data []map[string]string `json:"data"`
}
if err := c.post(ctx, path, body, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) DebtContract(ctx context.Context, accID string) ([]map[string]string, error) {
return c.contractList(ctx, "/api/trade/debt_contract", accID)
}
func (c *Client) AssureContract(ctx context.Context, accID string) ([]map[string]string, error) {
return c.contractList(ctx, "/api/trade/assure_contract", accID)
}
func (c *Client) EnableShortContract(ctx context.Context, accID string) ([]map[string]string, error) {
return c.contractList(ctx, "/api/trade/enable_short_contract", accID)
}
func (c *Client) IPOData(ctx context.Context, typ string) (any, error) {
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/trade/ipo_data", map[string]any{"type": typ}, &out); err != nil {
return nil, err
}
return out.Data, nil
}
func (c *Client) NewPurchaseLimit(ctx context.Context, accid string) (any, error) {
body := map[string]any{}
if accid != "" {
body["accid"] = accid
}
var out struct {
Data any `json:"data"`
}
if err := c.post(ctx, "/api/trade/new_purchase_limit", body, &out); err != nil {
return nil, err
}
return out.Data, nil
}