Loading ...

发明者量化交易平台 通用协议 接入 自定义交易所

Author: 小小梦, Created: 2017-08-24 16:29:56, Updated: 2019-04-22 15:50:00

通用协议 使用文档

可使用该通用协议接入任何一家 提供 API 交易的 交易所, 具体API 协议不限, 无论是 rest 、websocket 、fix … 都可以接入使用。 Python通用协议例子:https://www.fmz.com/strategy/101399

  • 图解,GetAccount 举例:

    如果完全 兼容 托管者 通信协议标准,也可以不用通过插件中转数据,可直接由托管者 和 交易所 按照以下文档的API标准 交互。

    img

  • 1、 通用协议插件运行、端口设置

    编写好的 “通用协议插件” 的监听地址和端口设置。

    举例 : http://127.0.0.1:6666/DigitalAsset 或者 : http://127.0.0.1:6666/exchange

    为什么要设置这些 IP、路径 呢? 因为在 BotVS 页面 控制中心 跳转 添加交易所 页面,选择 “通用协议”,显示除了API-KEY ,还有一项 “服务地址” ,这个服务地址 就是告诉 托管者 到时候去访问什么IP 和端口。服务地址举例 填写 “ http://127.0.0.1:6666/DigitalAsset ” DigitalAsset 这个是自己定义的名字,在此只是举例。

    在BotVS 添加交易所页面是这样的:

    img

    然后插件中做处理,拿到 XXX_PassWord 。

    img

    整体通用协议插件 main 函数 范例: Go 语言描述

    func main(){
      var addr = flag.String("b", "127.0.0.1:6666", "bing addr")   // 设置命令行参数,默认值描述,端口设置 6666 ,游览器 默认 拒绝访问 认为 unsafe
      flag.Parse()         // 解析命令行
      if *addr == "" {
          flag.Usage()     // 显示 命令行 描述
          return 
      }
      basePath := "/DigitalAsset"
      log.Println("Running ", fmt.Sprintf("http://%s%s", *addr, basePath), "...")   // 打印监听端口信息
      http.HandleFunc(basePath, OnPost)
      http.ListenAndServe(*addr, nil)
    }
    
  • 2、响应函数

    “通用协议插件”程序在不停的监听 指定的端口,有没有发送来请求 request 。 一旦有请求 就会调用 响应函数 响应执行。

    再解析请求数据中的参数,托管者发送的 请求数据为:

    /* request 的JSON结构 ,BotVS 调用 GetTicker ,托管者发送给 通用协议插件情况举例(各个API 调用时,Params 的值可能不一样,此处 Method 为 ticker):
    {
        "access_key" : "XXX",               // `json:"access_key"`
        "secret_key" : "XXX",               // `json:"secret_key"`
        "nonce" : "1502345965295519602",    // `json:"nonce"`
        "method" : "ticker",                // `json:"method"`
        "params" : {                        // `json:"params"`
                       "symbol" : "btc",
                       ...
                   },                     // 各个请求参数略有区别。即在策略中调用不同的BotVS API会有不同的参数, 在下文各个API 有介绍说明。
    }
    */
    

    所以根据 request 中的 Method ,我们可以 使用 switch 去分类处理不同的 BotVS 上的API(即识别出 托管者上运行的策略调用的是哪个BotVS API) 调用:

    Go语言举例:

    switch request.Method {
      case "accounts" :
          data, err = e.GetAccount(symbol)
      case "ticker" :
          data, err = e.GetTicker(symbol)
      case "depth" :
          data, err = e.GetDepth(symbol)
      case "trades" :
          data, err = e.GetTrades(symbol)
      case "trade" :
      ...
      default:
          ...
    

    这些分支执行完 返回的数据写入 应答结构。

    Go语言举例:

    defer func(){                                // 处理收尾工作 
          if e := recover(); e != nil {          // recover() 函数 用于捕获panic,e != nil 即有错误发生
              if ee, ok := e.(error); ok {       // 类型推导 ,推导成功 把ee.Error() 赋值给 e
                  e = ee.Error()                 // 调用 Error 方法 获取返回的错误字符串
              }
              ret = map[string]string{"error": fmt.Sprintf("%v", e)}
          }
          b, _ := json.Marshal(ret)              // 把 本次调用 获取的结果ret 编码,赋值给b ,写入 响应指针
          w.Write(b)
          //fmt.Println("after w.Write the b is", string(b))     // 测试
      }()
    
  • 3、 API 调用的种类

    大致分为两类: 1、 不需要签名的 公共接口,如:

    GetTicker()

    GetDepth()

    GetTrades()

    GetRecords() …

    2、 需要签名的 用户接口, 如:

    Buy、Sell

    GetOrder(id)

    GetOrders()

    GetAccount()

    CancelOrder(id) … 各个交易所 的签名方式可能各不相同,需要根据需求具体编写。

  • 4、各个BotVS API ,通用协议插件 和 托管者 交互的数据格式

    一些 BotVS 的 API 接口 如 GetName , GetLabel 等函数,调用时,不会向 通用协议 插件 发送请求。 exchange.GetName() 通用插件 配置的交易所 调用时会返回 “Exchange” 。

    • 1、 GetTicker: 用于获取当前行情数据。

      托管者 发送给监听响应函数的 request 中的 Method为: ticker

      托管者发送参数: request.Params.symbol :由托管者根据策略设置的币种发送

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "ticker",
          "params" :     {"symbol" : "ETH_BTC"},     // 以  ETH_BTC  交易对举例。
      }
      

      最终发送给托管者的 返回值结构:(即 通用协议 插件 请求交易所接口后的数据 返回给托管者的 格式)

      JSON结构

      {
          "data": {
              "time": 1500793319499,  // 毫秒时间戳, 整型
              "buy": 1000,            // 以下浮点型
              "sell": 1001,
              "last": 1005,
              "high": 1100,
              "low": 980,
              "vol": 523,
          }
      }
      
    • 2、GetRecords: 用于获取交易所提供的K线数据。(根据托管者请求的参数获取)

      托管者 发送给监听响应函数的 request 中的 Method为: records

      托管者发送参数:request.Params.period : 值关联 exchange.GetRecords 函数的第一个参数,实际 request.Params.period 为 分钟数 表示的周期,例如 日周期 为 60*24 即 1440 、request.Params.symbol : 托管者根据设置的币种发送。

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "records",
          "params" :     {"symbol" : "ETH_BTC", "period" : "1440"},     // 以  ETH_BTC  交易对,K线周期为日线 举例。
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      {
          "data": [
                  [1500793319499, 1.1, 2.2, 3.3, 4.4, 5.5],         // "Time":1500793319499000,"Open":1.1,"High":2.2,"Low":3.3,"Close":4.4,"Volume":5.5
                  [1500793259499, 1.01, 2.02, 3.03, 4.04, 5.05],
                  ...
          ]
      }
      

      // Go 语言测试 : ret_records = []interface{}{[6]interface{}{100, 1.1,2.2,3.3,4.4,5.5}, [6]interface{}{100, 1.01,2.02,3.03,4.04,5.05}} // BotVS 平台Log显示 : records [{“Time”:1500793319499000,“Open”:1.1,“High”:2.2,“Low”:3.3,“Close”:4.4,“Volume”:5.5}, // {“Time”:1500793259499000,“Open”:1.01,“High”:2.02,“Low”:3.03,“Close”:4.04,“Volume”:5.05}]

      注意:1、第二维数组中的第一个元素 是 int 类型,代表时间戳。 2、托管者 会自动 给时间戳 乘 1000 ,以上可以见。

    • 3、GetDepth: 获取交易所的深度信息(订单薄 卖一 卖二… 买一 买二 …)

      托管者 发送给监听响应函数的 request 中的 Method为: depth

      托管者发送参数:request.Params.symbol :由托管者根据策略设置的币种发送

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "depth",
          "params" :     {"symbol" : "ETH_BTC"},     // 以  ETH_BTC  交易对 举例。
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      {
          "data" : {
              "time" : 1500793319499,
              "asks" : [ [1000, 0.5], [1001, 0.23], [1004, 2.1], ... ],
              "bids" : [ [999, 0.25], [998, 0.8], [995, 1.4], ... ],
          }
      }
      
    • 4、GetTrades: 获取交易所提供的 整个交易所一定时间内的交易记录(非自己的交易记录)

      托管者 发送给监听响应函数的 request 中的 Method为: trades

      托管者发送参数: request.Params.symbol 值为 交易币种 例如: btc ,由托管者根据策略设置的币种发送

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "trades",
          "params" :     {"symbol" : "ETH_BTC"},     // 以  ETH_BTC  交易对 举例。
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      { 
          "data": [
              {
                  "id": 12232153,
                  "time" : 1529919412968,
                  "price": 1000,
                  "amount": 0.5,
                  "type": "buy",             // "buy" 、"sell"    
              },{
                  "id": 12545664,
                  "time" : 1529919412900,
                  "price": 1001,
                  "amount": 1,
                  "type": "sell",
              },{
                  ...
              }
          ]
      }
      
    • 5、GetAccount: 获取账户资产信息

      托管者 发送给监听响应函数的 request 中的 Method为: accounts

      托管者发送参数:(需注意!一般是获取账户的所有资产!,具体看交易所接口,是单独获取还是获取总资产信息。)

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "accounts",
          "params" :     {},                         
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      {
          "data": [
              {"currency": "btc", "free": 1.2, "frozen": 0.1},
              {"currency": "ltc", "free": 25, "frozen": 2.1},
              {"currency": "ltc", "free": 25, "frozen": 2.1},
              ...
          ],
          "raw" : {...}             # 可以写入 插件访问 交易所时 , 交易所返回的 原始信息 (response)
      }
      
    • 6、Buy、Sell: 发送委托单,下单交易。(市价单、限价单)

      托管者 发送给监听响应函数的 request 中的 Method为: trade

      托管者发送参数: request.Params.type : 托管者根据调用exchange.Buy 还是 exchange.Sell 发送 、request.Params.price : 策略中调用的API 函数的第一个参数 、request.Params.amount : 策略中调用的API函数的第二个参数 、request.Params.symbol : 托管者根据设置的币种发送。

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "trade",
          "params" :     {
                             "symbol" : "ETH_BTC", 
                             "type" : "buy", 
                             "price" : "1000", 
                             "amount" : "1"
                         },                          // 以  ETH_BTC  交易对,"type" : "buy" 买请求,价格 1000, 数量 1  举例。
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      {
          "data": {
              "id": 125456,      // 下单后 返回的订单id
                                 // 如果 订单id是   "asdf346sfasf532" 这样的 字符串形式,
                                 // 此处 id 也可以 是 字符串类型。
          }
      }
      
    • 7、GetOrder: 获取指定订单号的订单信息

      托管者 发送给监听响应函数的 request 中的 Method为: order

      托管者发送参数: request.Params.id 、request.Params.symbol

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "order",
          "params" :     {"symbol" : "ETH_BTC", "id" : "XXX"},     // 以  ETH_BTC  交易对, 订单 id 为 XXX 举例,注意 有些交易所的订单号是 数字形式的订单ID,如 123456 ,有些交易所的订单号是 字符串形式的ID,如 poimd55sdfheqxv,具体看交易所的订单 ID 格式。
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      { 
          "data": {
              "id": 2565244,
              "amount": 0.15,
              "price": 1002,
              "status": "open",    // "open" : 挂起状态 、"closed" : 完成关闭状态 、 "cancelled" : 已取消
              "deal_amount": 0,
              "type": "buy",       // "buy"、"sell"
              "avg_price": 0,      // 如果交易所没有提供, 在处理时 可以赋值为  0 。
          }
      }
      
    • 8、GetOrders: 获取所有未完成的订单信息

      托管者 发送给监听响应函数的 request 中的 Method为: orders

      托管者发送参数: request.Params.symbol

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "orders",
          "params" :     {"symbol" : "ETH_BTC"},     // 以  ETH_BTC  交易对 举例。
      }
      

      最终发送给托管者的 返回值结构:

      JSON结构

      {
          "data": [{
              "id": 542156,
              "amount": 0.25,
              "price": 1005,
              "deal_amount": 0,
              "type": "buy",   // "buy" 、 "sell"
              "status": "open", // "open"
          },{
              ...
          }]
      }
      
    • 9、CancelOrder: 取消指定订单号的订单委托

      托管者 发送给监听响应函数的 request 中的 Method为: cancel

      托管者发送参数: request.Params.id : 字符串 类型,策略调用的API函数的第一个参数、 request.Params.symbol :btc (举例)由托管者根据策略设置的币种发送

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "cancel",
          "params" :     {"symbol" : "ETH_BTC", "id" : "XXX"},     // 以  ETH_BTC  交易对 ,id 为 "XXX"(同GetOrder 函数的参数 id 一样), 举例。
      }
      

      最终发送给托管者的 返回值结构:

      布尔值

      {
          "data": true,        // true or false
      }
      
    • 10、 IO: 调用BotVS平台的 exchange.IO 函数

      托管者 发送给监听响应函数的 request 中的 Method为 以 “ _api ” 开头的的方法名。

      托管者 向通用协议 插件 请求的数据格式

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "__api_XXX",                // XXX 为具体 交易所的 API 接口(不包含 基地址)。
          "params" :     {"borrow_id" : "123", "symbol" : "cny"},      // 具体是传入 IO 函数的参数。
      }
      

      最终发送给托管者的 返回值结构:

      {
          "data": {...}       // 具体的接口调用的返回值。
      }
      

      举例:

      策略调用:

      var io_str = exchange.IO("api", "cancel_borrow", "symbol=cny&borrow_id=123")
      

      插件中的测试代码:(Golang)

      fmt.Println("request.Method:", request.Method, "request.Params:", request.Params)
      

      插件命令行 : 2017/08/31 10:19:59 Running http://127.0.0.1:6666/DigitalAsset

      插件命令行中打印出的 request.Method, request.Params 托管者发送的请求中 request.Method 为: “ __api_cancel_borrow ” 托管者发送的请求中 request.Params 为: {“borrow_id” : “123”, “symbol” : “cny”}

      可自定义处理这些 直接访问交易所 API 的 exchange.IO 调用。

      # 注意:
      # 举例
      # 在调用 exchange.IO("api", "POST", "/api/v1/getAccount", "symbol=BTC_USDT") 时 ,
      # 如果第二个参数 不是 POST , 而是 : exchange.IO("api", "GET", "/api/v1/getAccount", "symbol=BTC_USDT")
      # 是GET方法,这时在通用协议插件接受到的 http 请求中 头部 Http-Method 中储存的才是GET,
      # 所以在 通用协议处理IO函数实现时,需要参考以下范例代码:
      // tapiCall 函数定义
      func (p *iStocksExchange) tapiCall(method string, params map[string]string, httpType string) (js *Json, err error) {
          ...
      }
      
      // 在 OnPost 函数中
      if strings.HasPrefix(request.Method, "__api_") {
          var js *Json
          js, err = e.tapiCall(request.Method[6:], request.Params, r.Header.Get("Http-Method"))
          ...
      }
      
    • 对于 exchange.GetRawJSON 的支持

      底层自动处理 exchange.GetRawJSON 的调用,不用在插件中实现。

    • 对于 exchange.Go 的支持

      底层自动处理 exchange.Go 的调用,不用在插件中处理。

      var beginTime = new Date().getTime()
      var ret = exchange.Go("GetDepth")
      var endTime = new Date().getTime()
      Log(endTime - beginTime, "#FF0000")
      
      // Sleep(2000)
      beginTime = new Date().getTime()
      Log(exchange.GetTicker())
      endTime = new Date().getTime()
      Log(endTime - beginTime, "#FF0000")
      var depth = ret.wait()
      Log("depth:", depth)
      

      img

      img

      # 注意: 使用 exchange.Go 在wait 的时候 如果指定了 超时时间, 
      #       一定要确保 获取到最终的数据,这样 申请的并发线程才能回收。
      

  • 通用协议插件 完整的 Go 语言范例 (接入 比特购交易所)

/*
GOOS=linux GOARCH=amd64 go build -ldflags '-s -w -extldflags -static' rest_bitgo.go
*/
package main

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"
)

func toFloat(s interface{}) float64 {
	var ret float64
	switch v := s.(type) {
	case float64:
		ret = v
	case float32:
		ret = float64(v)
	case int64:
		ret = float64(v)
	case int:
		ret = float64(v)
	case int32:
		ret = float64(v)
	case string:
		ret, _ = strconv.ParseFloat(strings.TrimSpace(v), 64)
	}
	return ret
}

func float2str(i float64) string {
	return strconv.FormatFloat(i, 'f', -1, 64)
}

func toInt64(s interface{}) int64 {
	var ret int64
	switch v := s.(type) {
	case int:
		ret = int64(v)
	case float64:
		ret = int64(v)
	case bool:
		if v {
			ret = 1
		} else {
			ret = 0
		}
	case int64:
		ret = v
	case string:
		ret, _ = strconv.ParseInt(strings.TrimSpace(v), 10, 64)
	}
	return ret
}

func toString(s interface{}) string {
	var ret string
	switch v := s.(type) {
	case string:
		ret = v
	case int64:
		ret = strconv.FormatInt(v, 10)
	case float64:
		ret = strconv.FormatFloat(v, 'f', -1, 64)
	case bool:
		ret = strconv.FormatBool(v)
	default:
		ret = fmt.Sprintf("%v", s)
	}
	return ret
}

type Json struct {
	data interface{}
}

func NewJson(body []byte) (*Json, error) {
	j := new(Json)
	err := j.UnmarshalJSON(body)
	if err != nil {
		return nil, err
	}
	return j, nil
}

func (j *Json) UnmarshalJSON(p []byte) error {
	return json.Unmarshal(p, &j.data)
}

func (j *Json) Get(key string) *Json {
	m, err := j.Map()
	if err == nil {
		if val, ok := m[key]; ok {
			return &Json{val}
		}
	}
	return &Json{nil}
}

func (j *Json) CheckGet(key string) (*Json, bool) {
	m, err := j.Map()
	if err == nil {
		if val, ok := m[key]; ok {
			return &Json{val}, true
		}
	}
	return nil, false
}

func (j *Json) Map() (map[string]interface{}, error) {
	if m, ok := (j.data).(map[string]interface{}); ok {
		return m, nil
	}
	return nil, errors.New("type assertion to map[string]interface{} failed")
}

func (j *Json) Array() ([]interface{}, error) {
	if a, ok := (j.data).([]interface{}); ok {
		return a, nil
	}
	return nil, errors.New("type assertion to []interface{} failed")
}

func (j *Json) Bool() (bool, error) {
	if s, ok := (j.data).(bool); ok {
		return s, nil
	}
	return false, errors.New("type assertion to bool failed")
}

func (j *Json) String() (string, error) {
	if s, ok := (j.data).(string); ok {
		return s, nil
	}
	return "", errors.New("type assertion to string failed")
}

func (j *Json) Bytes() ([]byte, error) {
	if s, ok := (j.data).(string); ok {
		return []byte(s), nil
	}
	return nil, errors.New("type assertion to []byte failed")
}

func (j *Json) Int() (int, error) {
	if f, ok := (j.data).(float64); ok {
		return int(f), nil
	}

	return -1, errors.New("type assertion to float64 failed")
}

func (j *Json) MustArray(args ...[]interface{}) []interface{} {
	var def []interface{}

	switch len(args) {
	case 0:
	case 1:
		def = args[0]
	default:
		log.Panicf("MustArray() received too many arguments %d", len(args))
	}

	a, err := j.Array()
	if err == nil {
		return a
	}

	return def
}

func (j *Json) MustMap(args ...map[string]interface{}) map[string]interface{} {
	var def map[string]interface{}

	switch len(args) {
	case 0:
	case 1:
		def = args[0]
	default:
		log.Panicf("MustMap() received too many arguments %d", len(args))
	}

	a, err := j.Map()
	if err == nil {
		return a
	}

	return def
}

func (j *Json) MustString(args ...string) string {
	var def string

	switch len(args) {
	case 0:
	case 1:
		def = args[0]
	default:
		log.Panicf("MustString() received too many arguments %d", len(args))
	}

	s, err := j.String()
	if err == nil {
		return s
	}

	return def
}

func (j *Json) MustInt64() int64 {
	var ret int64
	var err error
	switch v := j.data.(type) {
	case int:
		ret = int64(v)
	case int64:
		ret = v
	case float64:
		ret = int64(v)
	case string:
		if ret, err = strconv.ParseInt(v, 10, 64); err != nil {
			panic(err)
		}
	default:
		ret = 0
		//panic("type assertion to int64 failed")
	}
	return ret
}

func (j *Json) MustFloat64() float64 {
	var ret float64
	var err error
	switch v := j.data.(type) {
	case int:
		ret = float64(v)
	case int64:
		ret = float64(v)
	case float64:
		ret = v
	case string:
		v = strings.Replace(v, ",", "", -1)
		if ret, err = strconv.ParseFloat(v, 64); err != nil {
			panic(err)
		}
	default:
		ret = 0
		//panic("type assertion to float64 failed")
	}
	return ret
}

type iBitgo struct {
	accessKey     string
	secretKey     string
	currency      string
	opCurrency    string
	baseCurrency  string
	secret        string
	secretExpires int64
	apiBase       string
	step          int64
	newRate       float64
	timeout       time.Duration
	timeLocation  *time.Location
}

type MapSorter []Item

type Item struct {
	Key string
	Val string
}

func NewMapSorter(m map[string]string) MapSorter {
	ms := make(MapSorter, 0, len(m))

	for k, v := range m {
		if strings.HasPrefix(k, "!") {
			k = strings.Replace(k, "!", "", -1)
		}
		ms = append(ms, Item{k, v})
	}

	return ms
}

func (ms MapSorter) Len() int {
	return len(ms)
}

func (ms MapSorter) Less(i, j int) bool {
	//return ms[i].Val < ms[j].Val // 按值排序
	return ms[i].Key < ms[j].Key // 按键排序
}

func (ms MapSorter) Swap(i, j int) {
	ms[i], ms[j] = ms[j], ms[i]
}

func encodeParams(params map[string]string, escape bool) string {
	ms := NewMapSorter(params)
	sort.Sort(ms)

	v := url.Values{}
	for _, item := range ms {
		v.Add(item.Key, item.Val)
	}
	if escape {
		return v.Encode()
	}
	var buf bytes.Buffer
	keys := make([]string, 0, len(v))
	for k := range v {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		vs := v[k]
		prefix := k + "="
		for _, v := range vs {
			if buf.Len() > 0 {
				buf.WriteByte('&')
			}
			buf.WriteString(prefix)
			buf.WriteString(v)
		}
	}
	return buf.String()
}

func newBitgo(accessKey, secretKey string) *iBitgo {
	s := new(iBitgo)
	s.accessKey = accessKey
	s.secretKey = secretKey
	s.apiBase = "https://www.bitgo.cn"
	s.timeout = 20 * time.Second
	s.timeLocation = time.FixedZone("Asia/Shanghai", 8*60*60)

	return s
}

func (p *iBitgo) apiCall(method string) (*Json, error) {
	req, err := http.NewRequest("POST", fmt.Sprintf("%s/appApi.html?%s", p.apiBase, method), nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return NewJson(b)
}

func (p *iBitgo) GetTicker(symbol string) (ticker interface{}, err error) {
	var js *Json
	js, err = p.apiCall("action=market&symbol=" + symbol)
	if err != nil {
		return
	}
	dic := js.Get("data")
	ticker = map[string]interface{}{
		"time": js.Get("time").MustInt64(),
		"buy":  dic.Get("buy").MustFloat64(),
		"sell": dic.Get("sell").MustFloat64(),
		"last": dic.Get("last").MustFloat64(),
		"high": dic.Get("high").MustFloat64(),
		"low":  dic.Get("low").MustFloat64(),
		"vol":  dic.Get("vol").MustFloat64(),
	}
	return
}

func (p *iBitgo) GetDepth(symbol string) (depth interface{}, err error) {
	var js *Json
	js, err = p.apiCall("action=depth&symbol=" + symbol)
	if err != nil {
		return
	}
	dic := js.Get("data")

	asks := [][2]float64{}
	bids := [][2]float64{}

	for _, pair := range dic.Get("asks").MustArray() {
		arr := pair.([]interface{})
		asks = append(asks, [2]float64{toFloat(arr[0]), toFloat(arr[1])})
	}

	for _, pair := range dic.Get("bids").MustArray() {
		arr := pair.([]interface{})
		bids = append(bids, [2]float64{toFloat(arr[0]), toFloat(arr[1])})
	}
	depth = map[string]interface{}{
		"time": js.Get("time").MustInt64(),
		"asks": asks,
		"bids": bids,
	}
	return
}

func (p *iBitgo) GetTrades(symbol string) (trades interface{}, err error) {
	var js *Json
	js, err = p.apiCall("action=trades&symbol=" + symbol)
	if err != nil {
		return
	}
	dic := js.Get("data")
	items := []map[string]interface{}{}
	for _, pair := range dic.MustArray() {
		item := map[string]interface{}{}
		arr := pair.(map[string]interface{})
		item["id"] = toInt64(arr["id"])
		item["price"] = toFloat(arr["price"])
		item["amount"] = toFloat(arr["amount"])
		// trade.Time = toInt64(arr["time"]) * 1000
		if toString(arr["en_type"]) == "bid" {
			item["type"] = "buy"
		} else {
			item["type"] = "sell"
		}
		items = append(items, item)
	}
	trades = items
	return
}

func (p *iBitgo) GetRecords(step int64, symbol string) (records interface{}, err error) {
	var js *Json
	js, err = p.apiCall(fmt.Sprintf("action=kline&symbol=%s&step=%d", symbol, step*60))
	if err != nil {
		return
	}
	items := []interface{}{}
	for _, pair := range js.Get("data").MustArray() {
		arr := pair.([]interface{})
		if len(arr) < 6 {
			err = errors.New("response format error")
			return
		}
		item := [6]interface{}{}
		item[0] = toInt64(arr[0])
		item[1] = toFloat(arr[1])
		item[2] = toFloat(arr[2])
		item[3] = toFloat(arr[3])
		item[4] = toFloat(arr[4])
		item[5] = toFloat(arr[5])

		items = append(items, item)
	}
	records = items
	return
}

func (p *iBitgo) tapiCall(method string, params map[string]string) (js *Json, err error) {
	if params == nil {
		params = map[string]string{}
	}
	params["api_key"] = p.accessKey
	h := md5.New()
	h.Write([]byte(encodeParams(params, false) + "&secret_key=" + p.secretKey))
	params["sign"] = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
	params["action"] = method
	qs := encodeParams(params, false)

	req, err := http.NewRequest("POST", fmt.Sprintf("%s/appApi.html?%s", p.apiBase, qs), nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	js, err = NewJson(b)
	if js != nil {
		if code := js.Get("code").MustInt64(); code != 200 {
			s := js.Get("msg").MustString()
			if s == "" {
				s = fmt.Sprintf("%v", toString(js.data))
			}
			return nil, errors.New(s)
		}
	}
	return js, err
}

func (p *iBitgo) GetAccount(symbol string) (account interface{}, err error) {
	var js *Json
	js, err = p.tapiCall("userinfo", nil)
	if err != nil {
		return
	}
	mp := js.Get("data")
	assets := map[string]map[string]interface{}{}
	for k := range mp.MustMap() {
		dic := mp.Get(k)
		if k == "free" {
			for c := range dic.MustMap() {
				if _, ok := assets[c]; !ok {
					assets[c] = map[string]interface{}{}
				}
				assets[c]["currency"] = c
				assets[c]["free"] = dic.Get(c).MustFloat64()
			}
		} else if k == "frozen" {
			for c := range dic.MustMap() {
				if _, ok := assets[c]; !ok {
					assets[c] = map[string]interface{}{}
				}
				assets[c]["currency"] = c
				assets[c]["frozen"] = dic.Get(c).MustFloat64()
			}
		}
	}
	accounts := []map[string]interface{}{}
	for _, pair := range assets {
		accounts = append(accounts, pair)
	}

	account = accounts
	return
}

func (p *iBitgo) Trade(side string, price, amount float64, symbol string) (orderId interface{}, err error) {
	var js *Json
	js, err = p.tapiCall("trade", map[string]string{
		"symbol": symbol,
		"type":   side,
		"price":  float2str(price),
		"amount": float2str(amount),
	})
	if err != nil {
		return
	}
	orderId = map[string]int64{"id": js.Get("orderId").MustInt64()}
	return
}

func (p *iBitgo) GetOrders(symbol string) (orders interface{}, err error) {
	var js *Json
	js, err = p.tapiCall("entrust", map[string]string{"symbol": symbol})
	if err != nil {
		return
	}
	items := []map[string]interface{}{}
	for _, ele := range js.Get("data").MustArray() {
		mp := ele.(map[string]interface{})
		item := map[string]interface{}{}
		item["id"] = toInt64(mp["id"])
		item["amount"] = toFloat(mp["count"])
		if _, ok := mp["prize"]; ok {
			item["price"] = toFloat(mp["prize"])
		} else {
			item["price"] = toFloat(mp["price"])
		}
		item["deal_amount"] = toFloat(mp["success_count"])

		if toInt64(mp["type"]) == 0 {
			item["type"] = "buy"
		} else {
			item["type"] = "sell"
		}
		item["status"] = "open"
		items = append(items, item)
	}
	return items, nil
}

func (p *iBitgo) GetOrder(orderId int64, symbol string) (order interface{}, err error) {
	var js *Json
	js, err = p.tapiCall("order", map[string]string{"id": toString(orderId)})
	if err != nil {
		return
	}

	found := false
	item := map[string]interface{}{}
	for _, ele := range js.Get("data").MustArray() {
		mp := ele.(map[string]interface{})
		if toInt64(mp["id"]) != orderId {
			continue
		}
		item["id"] = toInt64(mp["id"])
		item["amount"] = toFloat(mp["count"])
		if _, ok := mp["prize"]; ok {
			item["price"] = toFloat(mp["prize"])
		} else {
			item["price"] = toFloat(mp["price"])
		}
		item["deal_amount"] = toFloat(mp["success_count"])

		if toInt64(mp["type"]) == 0 {
			item["type"] = "buy"
		} else {
			item["type"] = "sell"
		}
		switch toInt64(mp["status"]) {
		case 1, 2:
			item["status"] = "open"
		case 3:
			item["status"] = "closed"
		case 4:
			item["status"] = "cancelled"
		}
		found = true
		break
	}
	if !found {
		return nil, errors.New("order not found")
	}
	return item, nil
}

func (p *iBitgo) CancelOrder(orderId int64, symbol string) (ret bool, err error) {
	_, err = p.tapiCall("cancel_entrust", map[string]string{"id": strconv.FormatInt(orderId, 10)})
	if err != nil {
		return
	}
	ret = true
	return
}

type RpcRequest struct {        // 结构体里的字段首字母必须大写,否则无法正常解析,结构体有导出和未导出,大写字母开头为导出。
                                // 在Unmarshal的时候会  根据 json 匹配查找该结构体的tag, 所以此处需要修饰符
	AccessKey string            `json:"access_key"`
	SecretKey string            `json:"secret_key"`
	Nonce     int64             `json:"nonce"`
	Method    string            `json:"method"`
	Params    map[string]string `json:"params"`
}

func OnPost(w http.ResponseWriter, r *http.Request) {
	var ret interface{}
	defer func() {
		if e := recover(); e != nil {
			if ee, ok := e.(error); ok {
				e = ee.Error()
			}
			ret = map[string]string{"error": fmt.Sprintf("%v", e)}
		}
		b, _ := json.Marshal(ret)
		w.Write(b)
	}()

	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		panic(err)
	}
	var request RpcRequest
	err = json.Unmarshal(b, &request)
	if err != nil {
		panic(err)
	}
	e := newBitgo(request.AccessKey, request.SecretKey)
	symbol := request.Params["symbol"]
	if s := request.Params["access_key"]; len(s) > 0 {
		e.accessKey = s
	}
	if s := request.Params["secret_key"]; len(s) > 0 {
		e.secretKey = s
	}
	if symbolIdx, ok := map[string]int{
		"btc":  1,
		"ltc":  2,
		"etp":  3,
		"eth":  4,
		"etc":  5,
		"doge": 6,
		"bec":  7,
	}[strings.Replace(strings.ToLower(symbol), "_cny", "", -1)]; ok {
		symbol = toString(symbolIdx)
	}
	var data interface{}
	switch request.Method {
	case "ticker":
		data, err = e.GetTicker(symbol)
	case "depth":
		data, err = e.GetDepth(symbol)
	case "trades":
		data, err = e.GetTrades(symbol)
	case "records":
		data, err = e.GetRecords(toInt64(request.Params["period"]), symbol)
	case "accounts":
		data, err = e.GetAccount(symbol)
	case "trade":
		side := request.Params["type"]
		if side == "buy" {
			side = "0"
		} else {
			side = "1"
		}
		price := toFloat(request.Params["price"])
		amount := toFloat(request.Params["amount"])
		data, err = e.Trade(side, price, amount, symbol)
	case "orders":
		data, err = e.GetOrders(symbol)
	case "order":
		data, err = e.GetOrder(toInt64(request.Params["id"]), symbol)
	case "cancel":
		data, err = e.CancelOrder(toInt64(request.Params["id"]), symbol)
	default:
		if strings.HasPrefix(request.Method, "__api_") {
			data, err = e.tapiCall(request.Method[6:], request.Params)
		} else {
			panic(errors.New(request.Method + " not support"))
		}
	}
	if err != nil {
		panic(err)
	}
	ret = map[string]interface{}{
		"data": data,
	}

	return
}

func main() {
	var addr = flag.String("b", "127.0.0.1:6666", "bind addr")
	flag.Parse()
	if *addr == "" {
		flag.Usage()
		return
	}
	basePath := "/exchange"
	log.Println("Running ", fmt.Sprintf("http://%s%s", *addr, basePath), "...")
	http.HandleFunc(basePath, OnPost)
	http.ListenAndServe(*addr, nil)
}


More

botvsing Python接通用协议自定义交易所例子 https://www.fmz.com/strategy/101399

botvsing Python接通用协议自定义交易所例子https://www.fmz.com/strategy/101399

liuwei9090 bitmex交易所已经接入平台,但不管用 _C(exchange.GetTicker).Buy 还是 _C(exchange.GetDepth).Bids[0] 都无法取得买一价,而且还会报错。不知道是为什么

每日一嫩模 哈哈,写一个通用协议插件都可以自己完全对接交易所API了,还不如直接用自己的程序,是不是有点多此一举了。

zhchy751 有没有现成的通用协议接入的例子完整机器人/策略/交易所配置等?讲再多不如模仿,哈哈哈

dyhhu 这上面的代码,就是通用协议插件是吧?这个插件不是类似于策略模版那种我直接可以调用?我会用py post或者get 发送和获得数据,并解析。这段通用协议插件的代码,是不是就是实现的post、get和解析,同时对接到botvs的api?编写好的 “通用协议插件” 的监听地址和端口设置这个 http://127.0.0.1:6666/DigitalAsset,实际上是不是类似于 https://www.bitmex.com/ 这种地址,机器人像这个地址发信息?

chalie 有python的就好了,只会这个

jading 还是没有完全懂……使用通用程序插件是不是就表示要自己编写很多函数的实现,例如 GetAccout()?

alanyao botVS不都是js范例多吗?为何没js的?怎么又给GO的?

nxtplayer 6到飞起,然而并不会搞。。。

韩诺她 牛逼

小小梦 BITMEX 是期货交易所 需要先设置 你要操作 或者 获取 行情的 合约, 参看 API 文档 exchange.SetContractType 函数

每日一嫩模 也对。

小小梦 但是 如果 策略是个多平台的呢?不能为了一棵树放弃一片森林啊 ! ^^

小小梦 就是 编译成一个 可执行文件, 和托管者一起运行就可以了 哪个目录无所谓。 python 的 例子暂时 还没有,抽时间我写个。

dyhhu 插件是如何让它运行起来,例如编写成py格式的,直接放在托管者exe目录里吗?还在本地py程序里添加引用路径到插件的py文件目录?

小小梦 策略里面不用特殊的处理 ,对于 BotVS api 接口 , 这些交易所对象都是 一样的(不论是 BotVS 已经接入的,还是 您自己编写 通用协议 插件 支持的),策略不用做任何 调整。 这个 插件 和托管者 一起运行起来 。 就实现了 对这个 交易所的支持。 和 已经接入的 完全一样。 看下 上面 文档 : 1、 通用协议插件运行、端口设置 的内容 就明白了。

dyhhu 这个自己写好后,是不是类似于策略模版,要对接插件对应的交易所就调用这里面的函数,用其他的交易所就用现有的botvs api?

小小梦 - 这段通用协议插件的代码,是不是就是实现的post、get和解析,同时对接到botvs的api? 是的。 - http://127.0.0.1:6666/DigitalAsset 这个地址是 代表 交易所对象, 托管者 的请求都会 发送到这个地址。 插件服务程序会 监听这个地址请求, 并作出响应,代替托管者去访问交易所,并返回 托管者 要求 的数据(格式),实现 ,策略机器人 兼容某个 BotVS 还没有接入的交易所。

小小梦 用 python 实现 也是 一样的 , 接口 格式 都一样 仅仅是 用的语言不同而已。

小小梦 这个是 托管者 外部 插件形式的 范例,目的是 编写一些 交易所 交互 代码 ,对接入 BotVS 平台, 相当于 自己接交易所到BotVS 了。您可以使用 PHP 、node.js 、python 、 Go 等 语言 编写这类的 插件, 只要 数据格式符合以上文档说明格式,插件获取的数据都可以对接到 相应的 BotVS API 接口。

小小梦 是的 , 要编写 实现 BotVS API GetAccount 等 这些 接口 函数的具体实现, 相当于 把自己编写 对接的交易所的代码 衔接到 BotVS 方便 策略统一 调用。这样可以 没有限制的去 参与 任何提供 接口的 交易所交易。

小小梦 根据例子 , 使用 python 、node.js、PHP 、Golang 都可以接的。