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

1537
api/QMT_API.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

257
docs/README.md Normal file
View File

@@ -0,0 +1,257 @@
# QMT HTTP API
将迅投 QMTMiniQMT / 投研版)策略进程内的 `ContextInfo`、行情、财务与交易函数,封装为 JSON HTTP 服务,供外部程序远程调用。
源码:[`api/QMT_API.py`](../api/QMT_API.py)
完整接口清单、请求/响应字段与 curl 示例见 [api.md](./api.md)。
---
## 1. 它是什么
`QMT_API.py` **不是**可独立 `python QMT_API.py` 启动的普通脚本。它是一份 QMT Python 策略:
- QMT 加载策略后调用 `init(ContextInfo)`
- `init` 绑定资金账号、加载股票池、创建 Tornado `Application`,并在当前进程里 `listen` + `IOLoop.start()`
- 此后外部 HTTP 客户端通过 `X-Token` 鉴权,调用本机(或同网段)上的 REST 接口。
- 接口内部再转调 QMT 内置对象:`ContextInfo.*``passorder``get_trade_detail_data` 等。
因此:服务生命周期 = 策略生命周期。策略停止HTTP 一并停止。
```
外部程序 --HTTP JSON--> Tornado (0.0.0.0:10086)
|
v
QMT 策略进程
ContextInfo / 交易账户
```
---
## 2. 运行环境
| 项 | 要求 |
| --- | --- |
| 宿主 | 迅投 QMT需启用 Python 策略) |
| 解释器 | QMT 自带的 Python源码文件编码为 **GBK** |
| 第三方库 | `tornado`(需在 QMT Python 环境中可用) |
| 标准库 | `json` / `os` / `datetime` / `pathlib` / `logging` / `locale` |
| 操作系统 | 源码调用 `locale.setlocale(locale.LC_CTYPE, 'chinese')`,面向 **Windows 中文环境** |
QMT 内置符号(由策略宿主注入,源码中未 import
- `ContextInfo` 及其方法(`get_market_data``get_universe` 等)
- 交易:`passorder``algo_passorder``smart_algo_passorder``order_*``buy_open` / `sell_open`
- 查询:`get_trade_detail_data``get_value_by_order_id``can_cancel_order``cancel`
- 其它:`get_open_date``timetag_to_datetime``ext_data``get_etf_info`
---
## 3. 配置
源码顶部与 `init()` 使用的配置如下。
| 名称 | 来源 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `QMT_ACCOUNT_ID` | 环境变量 | `''` | 资金账号,写入 `ContextInfo.accountID``set_account` |
| `QMT_DATA_DIR` | 环境变量 | `D:\qmt_strategy_data` | **意图**上的数据目录;见下方「已知问题」 |
| `TOKEN` | 源码硬编码 | `QMTbyYanweidong` | HTTP 鉴权口令,请求头 `X-Token` 必须与之相等 |
| `PORT` | 源码硬编码 | `10086` | 监听端口;绑定地址为 `0.0.0.0` |
启动时还会读取:
```
{数据目录}/pass_codes.json
```
内容须为 JSON 数组(股票代码列表),用于 `ContextInfo.set_universe(...)`。该文件缺失或无法解析会导致 `init` 失败HTTP 服务起不来。
---
## 4. 接入步骤
1. 在 QMT 中配置 Python 策略,入口文件指向 `api/QMT_API.py`
2. 准备数据目录,放入 `pass_codes.json`,例如:
```json
["000001.SZ", "600000.SH"]
```
3. 设置环境变量 `QMT_ACCOUNT_ID`(以及你实际使用的数据目录变量,见已知问题)。
4. 启动策略。日志出现类似:
```
QMT HTTP Server 启动于 http://0.0.0.0:10086 (全部API已加载)
```
5. 用任意 HTTP 客户端调用。所有业务接口默认需要鉴权:
```http
X-Token: < TOKEN >
Content-Type: application/json
```
快速探活(需 Token
```bash
curl -s -H "X-Token: QMTbyYanweidong" http://127.0.0.1:10086/api/context/period
```
关闭服务:
```bash
curl -s -X POST -H "X-Token: QMTbyYanweidong" http://127.0.0.1:10086/api/sys/shutdown
```
`ShutdownHandler` 会在响应后再 `IOLoop.stop()`Tornado 事件循环退出。
---
## 5. 鉴权与协议约定
### 5.1 鉴权
`BaseHandler.prepare()`
- 请求头 `X-Token` 必须等于源码中的 `TOKEN`
- 否则抛出 `HTTPError(401, "认证失败token 无效或缺失")`
- 源码定义了 `@no_auth` 装饰器,但 **没有任何 Handler 使用它**,包括 `/api/sys/python_version``/api/sys/shutdown`
### 5.2 请求
- GET无 Body参数都在路径中本服务 GET 接口目前均无 Query
- POSTBody 必须是 **合法 JSON 对象**。多数 POST 一上来就 `json.loads(self.request.body)`,空 Body 会直接异常。
- 多标的字段(如 `stock_code``stocks``stock_list``fieldList`)一般为 **逗号分隔字符串**,服务端再 `split(',')` + `strip()`
### 5.3 响应
- 默认 `Content-Type: application/json; charset=utf-8`
- 成功:各接口自定义 JSON见 [api.md](./api.md))。
- 失败:`write_error` 统一为:
```json
{"error": "<reason>", "status_code": 401}
```
常见状态码:
| 码 | 场景 |
| --- | --- |
| 400 | 缺参、下单参数不合法 |
| 401 | Token 缺失或错误 |
| 500 | QMT 调用失败(部分接口在 `safe_call` 返回 `None` 后主动抛出) |
`safe_call` 会吞掉底层异常并打日志,返回 `None`。调用方看到的可能是 `null` 字段,也可能是 500取决于该 Handler 有没有对 `None` 再处理。
### 5.4 HTTP 方法习惯
- 只读、无参的 Context / 判定 / 系统信息:多数为 **GET**
- 带 JSON Body 的查询与全部交易: **POST**
- 同一资源没有 REST 语义上的 PUT/PATCH/DELETE。
---
## 6. 接口分组
路由在 `make_app()` 中注册,当前约 **100+** 条。按前缀划分:
| 前缀 | 用途 | 文档 |
| --- | --- | --- |
| `/api/v2/*` | 持仓 / 资产(与旧接口共用 Handler | [api.md §1](./api.md#1-兼容层--v2) |
| `/api/holding` `/api/money/*` `/api/order/*` | 旧版买卖、资金、撤单、成交 | 同上 |
| `/api/context/*` | 策略上下文属性 | [§2](./api.md#2-策略上下文-apicontext) |
| `/api/data/*` | 行情、财务、期权、订阅 | [§3](./api.md#3-数据查询-apidata) |
| `/api/check/*` | 停牌、板块、K 线判定 | [§4](./api.md#4-判定-apicheck) |
| `/api/trade/*` | 股票/算法/期货下单、任务、账户查询 | [§5](./api.md#5-交易-apitrade) |
| `/api/ext/*` | 扩展数据与因子引用 | [§6](./api.md#6-扩展引用-apiext) |
| `/api/sys/*` | Python 版本、关停服务 | [§7](./api.md#7-系统-apisys) |
兼容层买卖是对 `passorder` 的薄封装:
- `POST /api/order/buy``passorder(23, 1101, ...)`(买入)
- `POST /api/order/sell``passorder(24, 1101, ...)`(卖出)
- 完整下单请用 `POST /api/trade/passorder`(可自定义 `opType` / `orderType` / `prType` / `quickTrade`
账户查询里的 `account` 字段默认 `"stock"`,也会传到 `get_trade_detail_data` 的账户类型参数。
---
## 7. 回调与落盘(当前未挂接)
源码后半定义了主推回调,用于把账户/委托/成交/持仓写成 JSON 文件:
| 函数 | 意图文件名 |
| --- | --- |
| `account_callback` | `acount_%s.json`(拼写为 acount |
| `order_callback` | `order_%s.json` |
| `deal_callback` | `deal_%s.json` |
| `position_callback` | `position_%s.json` |
| `orderError_callback` | 仅 `print` |
`init()` **没有** 调用 `ContextInfo` 的回调注册接口,因此这些函数默认不会执行。即便注册,`write_json` 本身也存在未定义变量问题(见下节),落盘路径目前不可靠。
---
## 8. 源码审视(使用前必读)
以下为对照 `QMT_API.py` 的事实,不是「建议优化清单」。接入前应按此理解行为边界。
### 8.1 数据目录变量不一致
```python
DATA_DIR = os.environ.get('QMT_DATA_DIR', 'D:\\qmt_strategy_data')
# ...
Path(QMT_DATA_DIR) / "pass_codes.json"
```
环境变量读入的是 `DATA_DIR``init` / `write_json` 使用的是 **从未赋值的** `QMT_DATA_DIR`。在普通 Python 里会 `NameError`。若你的 QMT 环境没有额外注入同名全局量,策略会在启动阶段失败。
### 8.2 `write_json` 不可用
- 使用未定义的 `current.strftime`(应为 `now`)。
- `order_id` 无默认值,但 `account_callback` / `position_callback` 只传了两个参数。
- `file_key` 模板与实参个数不一定匹配。
### 8.3 Token 硬编码且监听全网卡
`TOKEN` 写死在源码里;`listen(..., address='0.0.0.0')` 对所有网卡开放。任何能打到 `10086` 且知道 Token 的客户端都可以下单、撤单、关停服务。不要把该端口暴露到公网。
### 8.4 错误被吞掉
`safe_call` 捕获全部异常后返回 `None`。部分查询接口仍会把 `null` 当成功响应返回调用方不易区分「没数据」和「QMT 抛错」。
### 8.5 编码
文件头 `# -*- coding: gbk -*-`。用 UTF-8 无 BOM 保存可能在 QMT 中出现中文注释/字符串解码问题。
### 8.6 规则撤单语义很窄
`POST /api/order/cancel_order` 不是「按委托号撤单」,而是:
- 股票代码(`代码.市场`)完全匹配,且
- `m_nVolumeTotal + m_nVolumeTraded == volume`,且
- `can_cancel_order` 为真
才发出 `cancel`。按委托号查询/判断请用 `/api/trade/value_by_order_id``/api/trade/can_cancel_order`。源码里没有单独的「按 orderId 撤单」HTTP 封装(全部撤单走 `/api/order/cancel_all`)。
---
## 9. 仓库结构
```
big-qmt/
├── api/
│ └── QMT_API.py # QMT 策略 + HTTP 服务(唯一实现)
├── docs/
│ ├── README.md # 本文件:架构、接入、约定、风险
│ └── api.md # 全量 HTTP 接口说明
└── README.md # 仓库占位
```
---
## 10. 相关文档
- [HTTP API 参考](./api.md)
- 迅投 QMT Python 策略官方函数手册(`passorder``opType` / `prType` 等枚举以官方文档为准;本仓库只记录本封装实际传入的值)

1214
docs/api.md Normal file

File diff suppressed because it is too large Load Diff

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
}

20
scripts/run.bat Normal file
View File

@@ -0,0 +1,20 @@
@echo off
setlocal
cd /d "%~dp0.."
cd go-client
if not defined QMT_BASE_URL set "QMT_BASE_URL=http://127.0.0.1:10086"
if not defined QMT_TOKEN set "QMT_TOKEN=QMTbyYanweidong"
if not defined QMT_ACCOUNT set "QMT_ACCOUNT=stock"
echo QMT_BASE_URL=%QMT_BASE_URL%
echo QMT_ACCOUNT=%QMT_ACCOUNT%
echo.
go run ./apps/cmd %*
if errorlevel 1 (
echo.
echo 运行失败。
pause
exit /b 1
)