验证方式
调用扩展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_key | API 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"}。 -
文本格式:
plaintextBTCUSDTPERP 穿过(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)
}
参考: