dev 1
This commit is contained in:
1537
api/QMT_API.py
Normal file
1537
api/QMT_API.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
api/__pycache__/QMT_API.cpython-311.pyc
Normal file
BIN
api/__pycache__/QMT_API.cpython-311.pyc
Normal file
Binary file not shown.
257
docs/README.md
Normal file
257
docs/README.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# QMT HTTP API
|
||||
|
||||
将迅投 QMT(MiniQMT / 投研版)策略进程内的 `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)。
|
||||
- POST:Body 必须是 **合法 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
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
117
go-client/apps/cmd/main.go
Normal 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
70
go-client/apps/zt/boot.go
Normal 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
118
go-client/apps/zt/config.go
Normal 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
10
go-client/apps/zt/log.go
Normal 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
105
go-client/apps/zt/main.go
Normal 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
114
go-client/apps/zt/open.go
Normal 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
374
go-client/apps/zt/order.go
Normal 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)
|
||||
}
|
||||
298
go-client/apps/zt/positions.go
Normal file
298
go-client/apps/zt/positions.go
Normal 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
278
go-client/apps/zt/remote.go
Normal 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
179
go-client/apps/zt/state.go
Normal 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
104
go-client/apps/zt/stock.go
Normal 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
3
go-client/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module big-qmt/go-client
|
||||
|
||||
go 1.22
|
||||
193
go-client/sdk/account.go
Normal file
193
go-client/sdk/account.go
Normal 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
67
go-client/sdk/check.go
Normal 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
111
go-client/sdk/client.go
Normal 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
94
go-client/sdk/coerce.go
Normal 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
136
go-client/sdk/context.go
Normal 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
581
go-client/sdk/data.go
Normal 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
4
go-client/sdk/doc.go
Normal 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
38
go-client/sdk/error.go
Normal 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
47
go-client/sdk/ext.go
Normal 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
30
go-client/sdk/sys.go
Normal 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
302
go-client/sdk/trade.go
Normal 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
20
scripts/run.bat
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user