输入/搜索内容
入门
欢迎使用发明者量化交易平台
快速开始
密钥安全性
平台基础
账号与计费
实盘计费与充值
子账号
交易所
通用协议
本地凭据文件
交易所特殊说明
证券与期货
加密货币
托管者
策略库
实盘
编写策略
开发工具
回测系统
进阶专题
数据与研究
对外接口

调用扩展API接口时有两种验证方式:

  • 签名验证:用SecretKey对请求参数签名,SecretKey本身不在网络上传输。程序调用应使用这种方式。
  • 直接验证:把SecretKey直接放进请求URL,主要用于TradingView等只能填写一个URL的Webhook场景。

请求格式

向https://www.fmz.com/api/v1发送POST请求,参数以表单(application/x-www-form-urlencoded)提交。服务端也接受把同样的参数放在URL查询串中的GET请求,但参数会留在各处的访问日志里,推荐使用POST。

参数说明
version版本号,固定为1.0。
access_keyAPI KEY的AccessKey。
method调用的方法名,例如GetNodeList。
args方法参数组成的JSON字符串:按顺序排列的数组(如[]、[123, "ok"]),或按参数名传值的对象(如{"robotId": 123}),见扩展API接口详解。不传时按[]处理。
nonce毫秒时间戳。与服务器时间相差不能超过1小时,并且必须大于这把API KEY上一次请求使用的nonce。
sign签名,计算方法见下文。

请求中不包含SecretKey。

签名方式

按下面的格式拼接字符串,其中args是实际提交的JSON字符串原文:

plaintext
version + "|" + method + "|" + args + "|" + nonce + "|" + secretKey

对拼接结果计算MD5,转换为32位小写十六进制字符串,作为sign的值。

Python示例

python
import hashlib import json import time import urllib.parse import urllib.request ACCESS_KEY = '' # API KEY 的 AccessKey SECRET_KEY = '' # API KEY 的 SecretKey def api(method, *args, **kwargs): d = { 'version': '1.0', 'access_key': ACCESS_KEY, 'method': method, # 位置参数传数组,关键字参数传对象(按参数名传值) 'args': json.dumps(kwargs if kwargs else list(args)), 'nonce': int(time.time() * 1000), } s = '%s|%s|%s|%d|%s' % (d['version'], d['method'], d['args'], d['nonce'], SECRET_KEY) d['sign'] = hashlib.md5(s.encode('utf-8')).hexdigest() body = urllib.parse.urlencode(d).encode('utf-8') with urllib.request.urlopen('https://www.fmz.com/api/v1', body, timeout=10) as resp: return json.loads(resp.read().decode('utf-8')) print(api('GetNodeList')) # 托管者列表 print(api('GetRobotList', appId='member2')) # 按参数名传值:标签为 member2 的实盘 print(api('CommandRobot', 123, 'ok')) # 向实盘 123 发送交互命令 print(api('GetRobotDetail', 123)) # 实盘 123 的详细信息

Go示例

mylang
package main import ( "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "time" ) const ( accessKey = "" // API KEY 的 AccessKey secretKey = "" // API KEY 的 SecretKey baseAPI = "https://www.fmz.com/api/v1" ) var client = &http.Client{Timeout: 10 * time.Second} func api(method string, args ...interface{}) (string, error) { if args == nil { args = []interface{}{} } b, err := json.Marshal(args) if err != nil { return "", err } nonce := strconv.FormatInt(time.Now().UnixMilli(), 10) sum := md5.Sum([]byte("1.0|" + method + "|" + string(b) + "|" + nonce + "|" + secretKey)) form := url.Values{ "version": {"1.0"}, "access_key": {accessKey}, "method": {method}, "args": {string(b)}, "nonce": {nonce}, "sign": {hex.EncodeToString(sum[:])}, } resp, err := client.PostForm(baseAPI, form) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) return string(body), err } func main() { ret, err := api("GetNodeList") fmt.Println(ret, err) // 用新的配置重启实盘 123,settings 字段见「扩展API接口详解」中的实盘配置说明 settings := map[string]interface{}{ "name": "hedge test", "strategy": 456, "period": 60, "node": 789, "exchanges": []interface{}{ map[string]interface{}{"pid": 1001, "pair": "BTC_USDT"}, }, } ret, err = api("RestartRobot", 123, settings) fmt.Println(ret, err) }

直接验证不计算签名,而是把secret_key直接放在请求参数中,因此可以生成一个固定的URL,填到TradingView等只能设置一个URL的Webhook回调里。

**安全提示**:secret_key写在URL中,会留在浏览器历史、代理和服务器的访问日志、Webhook服务方的配置里,任何拿到这个URL的人都能以这把API KEY的权限调用接口。建议只在CommandRobot的Webhook中使用直接验证,并为它单独创建一把只授权CommandRobot的API KEY(见创建ApiKey);一旦泄露,立即删除这把API KEY。

请求参数为access_key、secret_key、method、args(JSON数组,需要URL编码),不需要version、nonce、sign。CommandRobot不做nonce校验;其他方法仍会校验:不传nonce时服务器以当前时间(精确到秒)代替,同一秒内的第二次调用会返回Nonce错误(code为3)。

例如API KEY的AccessKey为xxx、SecretKey为yyy,访问下面的URL即可向Id为186515的实盘发送交互命令ok12345:

plaintext
https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyy&method=CommandRobot&args=%5B186515%2C%22ok12345%22%5D

接收Webhook请求体

CommandRobot的命令参数为空字符串、请求为POST时,服务器把请求体(Body)作为交互命令发给实盘。例如在TradingView的Webhook URL中设置:

plaintext
https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyy&method=CommandRobot&args=%5B186515%2C+%22%22%5D

其中args的值%5B186515%2C+%22%22%5D解码后为[186515, ""](+是URL编码中的空格),186515是实盘Id,命令为空字符串。

模拟TradingView发送Webhook警报:

javascript
function main() { var options = { method: "POST", body: `{"test": 123}`, headers: {"Content-Type": "application/json"} } // Webhook 警报会自动发送 POST 请求,并带上需要的 headers return HttpQuery("https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyy&method=CommandRobot&args=%5B186515%2C+%22%22%5D", options) }

TradingView警报消息框中的内容就是请求体:

  • JSON格式:

    plaintext
    {"close": {{close}}, "name": "aaa"}

    Id为186515的实盘收到交互命令:{"close": 39773.75, "name": "aaa"}。

  • 文本格式:

    plaintext
    BTCUSDTPERP 穿过(Crossing) 39700.00 close: {{close}}

    Id为186515的实盘收到交互命令:BTCUSDTPERP 穿过(Crossing) 39700.00 close: 39739.4。

Python、Go示例

python
import json import urllib.parse import urllib.request ACCESS_KEY = '' # 只授权了 CommandRobot 的 API KEY 的 AccessKey SECRET_KEY = '' # SecretKey def api(method, *args): query = urllib.parse.urlencode({ 'access_key': ACCESS_KEY, 'secret_key': SECRET_KEY, 'method': method, 'args': json.dumps(list(args)), }) with urllib.request.urlopen('https://www.fmz.com/api/v1?' + query, timeout=10) as resp: return json.loads(resp.read().decode('utf-8')) # API KEY 没有该方法的权限时返回 {'code': 4, 'data': None} print(api('CommandRobot', 186515, 'ok12345'))
mylang
package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "time" ) const ( accessKey = "" // 只授权了 CommandRobot 的 API KEY 的 AccessKey secretKey = "" // SecretKey baseAPI = "https://www.fmz.com/api/v1" ) var client = &http.Client{Timeout: 10 * time.Second} func api(method string, args ...interface{}) (string, error) { if args == nil { args = []interface{}{} } b, err := json.Marshal(args) if err != nil { return "", err } q := url.Values{ "access_key": {accessKey}, "secret_key": {secretKey}, "method": {method}, "args": {string(b)}, } resp, err := client.Get(baseAPI + "?" + q.Encode()) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) return string(body), err } func main() { ret, err := api("CommandRobot", 186515, "ok12345") fmt.Println(ret, err) }

参考: