Вы можете использовать этот общий протокол для доступа к любой бирже, которая предоставляет API торговлю, конкретный протокол API не ограничивается, будь то отдых, веб-сокет, исправление... все можно получить доступ к использованию. Пример протокола Python Custom:https://www.fmz.com/strategy/101399
Настройка адреса прослушивания и порта плагина http://127.0.0.1:6666/DigitalAsset
илиhttp://127.0.0.1:6666/exchange
.
Зачем нам устанавливать этиИПидорожки- Что?
Это потому, что когдадобавление обменаСтраница вФМЗ Квантовая платформа, выбирая опцию API-KEY
. Этот адрес обслуживания сообщает докеру, где получить доступ к IP и порту (докер и плагин Custom Protocol могут не работать на одном устройстве). Например, адрес обслуживания может быть заполнен какhttp://127.0.0.1:6666/DigitalAsset
. DigitalAsset
Это всего лишь пример и его можно заменить именем по вашему выбору.
На странице FMZ Quant Platformaccess key
иsecret key
. Однако некоторые обменыsecret key
(илиaccess key
Если информация не является конфиденциальной). Затем, в программе плагина Custom Protocol, мы можем выполнить строкуsplit
Операция для разделения этих данных, как показано на примере.
И затем в плагине, обработать его, чтобы получитьXXX_PassWord
- Да, конечно.
Например, в полном примере в конце этого сообщения, в функции newBitgo:
func newBitgo(accessKey, secretKey string) *iBitgo {
s := new(iBitgo)
s.accessKey = accessKey
s.secretKey = secretKey
// Additional configuration information in the secretKey can be separated here and can be written as in the following comment
/*
arr := strings.SplitN(secretKey, ",", 2)
if len(arr) != 2 {
panic("Configuration error!")
}
s.secretKey = arr[0] // secret key
s.passWord = arr[1] // XXX_PassWord
*/
s.apiBase = "https://www.bitgo.cn"
s.timeout = 20 * time.Second
s.timeLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
return s
}
Опять анализируя параметры в данных запроса, докер отправляет данные запроса как:
"secret_key" : "XXX",
Плагин получает входящие данные, содержащие такую информацию, и отделяет XXX_PassWord от него на основе сепаратора запятой, так что он получает дополнительные передаваемые данные.
Пример общего плагина пользовательского протоколаmain
Функция:Go
Описание языка:
func main(){
var addr = flag.String("b", "127.0.0.1:6666", "bing addr") // Set command line parameters, default value description, port setting 6666
flag.Parse() // Parsing the command line
if *addr == "" {
flag.Usage() // Display the command line description
return
}
basePath := "/DigitalAsset"
log.Println("Running ", fmt.Sprintf("http://%s%s", *addr, basePath), "...") // Print listening port information
http.HandleFunc(basePath, OnPost)
http.ListenAndServe(*addr, nil)
}
Программа request
После получения запроса он вызывает функцию ответа для выполнения, а затем анализирует параметры из данных запроса.
/* JSON structure of request, FMZ Quant call GetTicker, docker sent to the custom protocol plugin case example (the value of params may be different for each API call, here the method is 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",
...
}, // The parameters are slightly different for each request. That is, different FMZ Quant APIs are called in the strategy with different parameters, which are described in the following sections for each API.
}
*/
Итак, основываясь наMethod
Поле вrequest
Структура получена JSON deserializing запрос Тело данных, полученных в программе Universal Protocol Plugin, мы можем использоватьswitch
заявление для классификации и обработки различных FMZ Quant API, вызванных на докер (т.е. определить, какие FMZ QuantAPI
стратегия, выполняемая на докере, вызывает:
Пример вGo
язык:
switch request.Method { // M of request.Method is capitalized here, the body of the request received by the custom protocol program for the JSON data, in the Go language, the anti-JSON serialization (Unmarshal) is a structure, the first letter of the field must be capitalized
case "accounts" : // When the exchange.GetAccount() function is called in the bot strategy on the docker, the docker sends in a request where the Body carries data with a method attribute value of 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:
...
После выполнения этих ветвей возвращенные данные должны быть записаны в структуру, которую программа Custom Protocol Plugin будет использовать для ответа на запрос докера.
Пример на языке Go:
defer func(){ // Handling of closing work
if e := recover(); e != nil { // The recover() function is used to catch panic, e ! = nil, i.e. an error has occurred
if ee, ok := e.(error); ok { // Type derivation, successful derivation assigns ee.Error() to e
e = ee.Error() // Call the Error method to get the returned error string
}
ret = map[string]string{"error": fmt.Sprintf("%v", e)}
}
b, _ := json.Marshal(ret) // Encode the result obtained from this call, ret, assign it to b, and write it into the response pointer
w.Write(b)
//fmt.Println("after w.Write the b is", string(b)) // test
}()
В основном разделены на две категории:
GetTicker()
GetDepth()
GetTrades()
GetRecords(period)
…
Buy
, Sell
GetOrder(id)
GetOrders()
GetAccount()
CancelOrder(id)
- Да, конечно.
Способы подписи могут варьироваться от биржи к бирже и должны быть написаны специально в соответствии с потребностями.
Некоторые интерфейсы FMZ Quant API, такие как
GetName()
,GetLabel()
, и т.д., не отправляйте запросы вПротокольный плагинкогда тебя позовут. Когда звонишьexchange.GetName()
, обменник, настроенный в универсальном плагине, вернетExchange .
Вmethod
вrequest
ОтправленныйДокердля функции ответа слушания:ticker
.
Докер отправляет параметр:request.Params.symbol
, который отправляется докером на основе валюты, установленной на странице робота.
Формат данных (JSON) в теле запроса, когда докер запрашивает плагин пользовательского протокола
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "ticker",
"params" : {"symbol" : "ETH_BTC"}, // Take the ETH_BTC trading pair for example
}
Структура окончательного значения возврата, отправленного докеру: (т.е. формат, в котором данные возвращаются докеру после того, как интерфейс обмена запрашивается плагином общего протокола)
Структура JSON
{
"data": {
"time": 1500793319499, // Millisecond timestamp, integer
"buy": 1000, // floating-point type as follows
"sell": 1001,
"last": 1005,
"high": 1100,
"low": 980,
"vol": 523,
}
}
Вmethod
вrequest
отправленный докером в функцию ответа слушания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", // millisecond timestamp
"method" : "records",
"params" : {"symbol" : "ETH_BTC", "period" : "1440"}, // Example of an ETH_BTC pair with a daily K-period
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура JSON
{
"data": [
[1500793319, 1.1, 2.2, 3.3, 4.4, 5.5], // "Time":1500793319000,"Open":1.1,"High":2.2,"Low":3.3,"Close":4.4,"Volume":5.5
[1500793259, 1.01, 2.02, 3.03, 4.04, 5.05],
...
]
}
Данные языкового теста:
ret_records = []interface{}{
[6]interface{}{1500793319, 1.1, 2.2, 3.3, 4.4, 5.5},
[6]interface{}{1500793259, 1.01, 2.02, 3.03, 4.04, 5.05}
}
Квантовая платформа FMZLog
дисплеиrecords
данные:
[
{"Time":1500793319000,"Open":1.1,"High":2.2,"Low":3.3,"Close":4.4,"Volume":5.5},
{"Time":1500793259000,"Open":1.01,"High":2.02,"Low":3.03,"Close":4.04,"Volume":5.05}
]
Примечание: 1. Первый элемент в двухмерном массиве - типint
2. Докер автоматически умножит отметку времени на 1000, как упоминалось выше.
Вmethod
вrequest
отправленный докером в функцию ответа слушанияdepth
.
Докер отправляет параметр:request.Params.symbol
, который отправляется докером на основе валюты, установленной в стратегии.
Формат данных, содержащийся в теле запроса, когда докер запрашивает плагин пользовательского протокола
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "depth",
"params" : {"symbol" : "ETH_BTC"}, // Take the ETH_BTC trading pair for example
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура JSON
{
"data" : {
"time" : 1500793319499,
"asks" : [ [1000, 0.5], [1001, 0.23], [1004, 2.1], ... ],
"bids" : [ [999, 0.25], [998, 0.8], [995, 1.4], ... ],
}
}
Вmethod
вrequest
отправленный докером в функцию ответа на прослушивание:trades
.
Параметры, отправленные докером: значениеrequest.Params.symbol
является валютой торговли, например:btc
, который отправляется докером на основе настроек стратегии.
Формат данных, переносимый докером при запросе плагина пользовательского протокола, выглядит следующим образом
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "trades",
"params" : {"symbol" : "ETH_BTC"}, // Take the ETH_BTC trading pair for example
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура 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",
},{
...
}
]
}
Вmethod
вrequest
отправленный докером в функцию ответа на прослушивание:accounts
.
Параметры, отправленные докером: (Примечание: как правило, это для получения всех активов счета! Пожалуйста, обратитесь к интерфейсу обмена, чтобы узнать, является ли это для получения отдельных активов или общей информации об активах)
Формат данных, переносимый докером при запросе плагина пользовательского протокола, выглядит следующим образом
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"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" : {...} // It is possible to write the raw message (response) returned by the exchange when the plugin accesses the exchange
}
Вmethod
вrequest
отправленный докером в функцию ответа на прослушивание: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", // millisecond timestamp
"method" : "trade",
"params" : {
"symbol" : "ETH_BTC",
"type" : "buy",
"price" : "1000",
"amount" : "1"
}, // Example of an ETH_BTC trading pair, "type": "buy" buy request, price 1000, quantity 1
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура JSON
{
"data": {
"id": 125456, // Order id returned after placing an order
// If the order id is in the form of a string like "asdf346sfasf532"
// Here the id can also be a string type
}
}
Вmethod
вrequest
отправленный докером в функцию ответа на прослушивание:order
.
Параметры, отправленные докером:request.Params.id
, request.Params.symbol
.
Формат данных, переносимый докером при запросе плагина пользовательского протокола, выглядит следующим образом
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "order",
"params" : {"symbol" : "ETH_BTC", "id" : "XXX"}, // Take the ETH_BTC trading pair and order ID XXX as an example. Please note that some exchanges use numerical order IDs, such as 123456, while others use string order IDs, such as poimd55sdfheqxv. The specific format of the order ID depends on the exchange.
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура JSON
{
"data": {
"id": 2565244,
"amount": 0.15,
"price": 1002,
"status": "open", // "open": pending, "closed": closed, "canceled": canceled
"deal_amount": 0,
"type": "buy", // "buy"、"sell"
"avg_price": 0, // If not provided by the exchange, it can be assigned a value of 0 during processing
}
}
Вmethod
вrequest
отправленный докером в функцию ответа слушанияorders
.
Параметры, отправленные докером:request.Params.symbol
Формат данных, переносимый докером при запросе плагина пользовательского протокола, выглядит следующим образом
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "orders",
"params" : {"symbol" : "ETH_BTC"}, // Take the ETH_BTC trading pair for example
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура JSON
{
"data": [{
"id": 542156,
"amount": 0.25,
"price": 1005,
"deal_amount": 0,
"type": "buy", // "buy"、"sell"
"status": "open", // "open"
},{
...
}]
}
Вmethod
вrequest
отправленный докером в функцию ответа слушанияcancel
.
Параметры, отправленные докером:request.Params.id
(тип строки, первый параметр функции API, вызванной стратегией),request.Params.symbol
(например, btc, отправленный докером на основе валюты, установленной стратегией)
Формат данных, переносимый докером при запросе плагина пользовательского протокола, выглядит следующим образом
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "cancel",
"params" : {"symbol" : "ETH_BTC", "id" : "XXX"}, // Take an ETH_BTC trading pair with an id of "XXX" (same as the GetOrder function's parameter id) for example
}
Структура окончательного возвращаемого значения, отправленного докеру:
Структура JSON
{
"data": true, // true or false
}
Вmethod
вrequest
отправленный докером на функцию ответа слушания начинается с_api_
.
Формат данных, переносимый докером при запросе плагина пользовательского протокола, выглядит следующим образом
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "__api_XXX", // XXX is the API interface for the specific exchange (base address not included)
"params" : {"borrow_id" : "123", "symbol" : "cny"}, // Specifically, the parameters passed into the IO function
}
Структура окончательного возвращаемого значения, отправленного докеру:
{
"data": {...} // The return value of a specific interface call
}
В качестве примера призыв к реализации стратегии:
var io_str = exchange.IO("api", "POST", "cancel_borrow", "symbol=cny&borrow_id=123")
Тестный код в плагине (язык перехода):
fmt.Println("request.Method:", request.Method, "request.Params:", request.Params)
Вставка командной строки: 2017/08/31 10:19:59 Бегhttp://127.0.0.1:6666/DigitalAsset …
Принт из командной строки плагина: request.Method, request.ParamsВ тексте запроса, отправленном докером, анализируемые данные в запросе следующие:request.Method
это__api_cancel_borrow
request.Params
это{"borrow_id" : "123", "symbol" : "cny"}
Вы можете настроить обработку этихexchange.IO
вызовы, которые непосредственно получают доступ к обменуAPI
.
# Attention:
# When calling exchange.IO("api", "POST", "/api/v1/getAccount", "symbol=BTC_USDT"),
# If the second parameter is not POST but: exchange.IO("api", "GET", "/api/v1/getAccount", "symbol=BTC_USDT")
# is the GET method, which is then stored in the header Http-Method in the http request accepted by the custom protocol plugin.
# So you need to refer to the following sample code for the custom protocol handling IO function implementation:
// tapiCall function definition
func (p *iStocksExchange) tapiCall(method string, params map[string]string, httpType string) (js *Json, err error) {
...
}
// In the OnPost function
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.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)
# Note: If you specify a timeout when waiting using exchange.
# Always make sure to obtain the final data so that the concurrent threads of the application can be reclaimed.
Вам необходимо реализовать специальную обработку в плагине для фьючерсных функций. Например, настройка рычага, кода контракта и направления заказа. Вы можете установить локальную переменную для записи этой информации. Чтобы получить позиции, вам нужно будет получить доступ к API обмена, чтобы получить сырые данные и обработать их в структуру позиций, определенную в платформе FMZ, а затем вернуть ее.
При вызове следующих функций в стратегии форматRpc
запрос, полученный программой плагина немного отличается от других интерфейсов.RpcRequest
Главное отличие заключается в том, что значение парамов представляет собой сложную структуру.
SetContractType Установите код контракта.
{"access_key":"123","method":"io","nonce":1623307269528738000,"params":{"args":["quarter"],"code":2},"secret_key":"123"}
Установка направления Устанавливает направление размещения фьючерсных ордеров.
{"access_key":"123","method":"io","nonce":1623308734966484000,"params":{"args":["closesell"],"code":1},"secret_key":"123"}
Установка уровня границы Устанавливает фьючерсный рычаг.
{"access_key":"123","method":"io","nonce":1623308734966939000,"params":{"args":[12],"code":0},"secret_key":"123"}
ПолучитьПозицию Получи позицию фьючерса. Когда?
exchange.GetPosition()
называется:
{"access_key":"123","method":"io","nonce":1623308734967442000,"params":{"args":[],"code":3},"secret_key":"123"}
Когда?exchange.GetPosition("swap")
называется:
{"access_key":"123","method":"io","nonce":1623308734967442000,"params":{"args":["swap"],"code":3},"secret_key":"123"}
/*
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 // Sort by value
return ms[i].Key < ms[j].Key // Sort by 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 { // The fields in a struct must start with an uppercase letter, otherwise they cannot be parsed correctly. Structs can have exported and unexported fields, where fields starting with an uppercase letter are considered exported.
// During unmarshaling, the JSON tag of a struct is used to match and find the corresponding field. Therefore, modifiers are required in this case.
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)
}