You can use this general protocol to access any exchange that provides API trading, the specific API protocol is not limited, whether it is rest, websocket, fix… All can be accessed to use. Python Custom protocol example: https://www.fmz.com/strategy/101399
The setting for the listening address and port of the “Custom Protocol Plugin” can be as follows:
For example: http://127.0.0.1:6666/DigitalAsset
or http://127.0.0.1:6666/exchange
.
Why do we need to set these IP and paths?
This is because when adding exchange page in the FMZ Quant Platform Dashboard, selecting the “Custom Protocol” option displays a “Service Address” in addition to the API-KEY
. This service address informs the docker where to access the IP and port (the docker and the Custom Protocol plugin program may not be running on the same device). For example, the service address can be filled in as http://127.0.0.1:6666/DigitalAsset
. DigitalAsset
is just an example and can be replaced with a name of your choice.
On the FMZ Quant Platform’s “Add Exchange” page, usually, the exchange configuration only requires access key
and secret key
. However, some exchanges’ API interfaces require the trading password to be passed (e.g., the order placement interface of certain exchanges). In such cases, since the Custom Protocol page does not have additional controls to enter this information, we can include the extra required configuration information in the secret key
(or access key
if the information is not sensitive). Then, in the Custom Protocol plugin program, we can perform a string split
operation to separate this data, as shown in the example image.
And then in the plugin, process it to obtain XXX_PassWord
.
For example, in the complete example at the end of this post, in the newBitgo function:
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
}
Parsing the parameters in the request data again, the docker sends the request data as:
"secret_key" : "XXX",
The plugin receives the incoming data containing this kind of information and separates the XXX_PassWord from it based on the comma separator, so that it gets the additional passed data.
Example of an overall custom protocol plugin main
function:
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) }
### 2. Response function
The "Custom Protocol Plugin" program continuously listens on a specified port for incoming ```request```. Once a request is received, it invokes the response function to execute and then parses the parameters from the request data. The request data sent by the docker is:
/* 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.
}
*/
So, based on the ```Method``` field in the``` request``` structure obtained by JSON deserializing the request Body data received in the Universal Protocol Plugin program, we can use a ```switch``` statement to categorize and handle different FMZ Quant APIs being called on the docker (i.e., identify which FMZ Quant ```API``` the strategy running on the docker is invoking):
Example in ```Go``` language:
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: …
After executing these branches, the data returned should be written into the structure that the Custom Protocol Plugin program will use to respond to the docker's request.
Example in Go language:
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 }()
### 3. Types of API calls
Roughly divided into two categories:
1. public interfaces that do not require a signature, e.g.:
```GetTicker()```
```GetDepth()```
```GetTrades()```
```GetRecords(period)```
...
2. user interfaces that need to sign, such as:
```Buy```, ```Sell```
```GetOrder(id)```
```GetOrders()```
```GetAccount()```
```CancelOrder(id)```
...
Signature methods may vary from exchange to exchange, and need to be written specifically according to the needs.
### 4. The data format for the interaction between the Custom Protocol Plugin and the docker when calling various FMZ Quant API interfaces:
> Some FMZ Quant API interfaces, such as ```GetName()```, ```GetLabel()```, etc., do not send requests to the **Custom Protocol Plugin** when called.
> When calling ```exchange.GetName()```, the exchange configured in the universal plugin will return "Exchange".
- 1. GetTicker: Used to get the current ticker data.
The ```method``` in the ```request``` sent by the **docker** to the listening response function is ```ticker```.
The docker sends the parameter: ```request.Params.symbol```, which is sent by the docker based on the currency set on the robot's page.
**The data format (JSON) carried in the request body when the docker requests the Custom Protocol Plugin**
{ “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 }
**Structure of the final return value sent to the docker: (i.e., the format in which the data returned to the docker after the exchange interface is requested by the common protocol plug-in)**
JSON structure
{ “data”: { “time”: 1500793319499, // Millisecond timestamp, integer “buy”: 1000, // floating-point type as follows “sell”: 1001, “last”: 1005, “high”: 1100, “low”: 980, “vol”: 523, } }
- 2. GetRecords: Used to retrieve the K-line data provided by the exchange. (Based on the parameters requested by the docker)
The ```method``` in the ```request``` sent by the docker to the listening response function is ```records```.
The docker sends the parameters: ```request.Params.period```, which is associated with the first parameter of the ```exchange.GetRecords``` function. The actual ```request.Params.period``` represents the period in minutes. For example, the daily period is ```60*24```, which is ```1440```. ```request.Params.symbol``` is sent by the docker based on the set currency.
**The data format carried in the request body when the docker requests the Custom Protocol Plugin.**
{ “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 }
**Structure of the final return value sent to the docker:**
JSON structure
{ “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], … ] }
Go language test data:
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} }
FMZ Quant Platform ```Log``` displays ```records``` data:
[ {“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} ]
Note: 1. The first element in the second-dimensional array is of type ```int``` and represents a timestamp. 2. The docker will automatically multiply the timestamp by 1000, as mentioned above.
- 3. GetDepth: Retrieves the depth information (order book, ask1, ask2... bid1, bid2...) from the exchange.
The ```method``` in the ```request``` sent by the docker to the listening response function is ```depth```.
The docker sends the parameter: ```request.Params.symbol```, which is sent by the docker based on the currency set in the strategy.
**The data format carried in the request body when the docker requests the Custom Protocol Plugin**
{ “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 }
**Structure of the final return value sent to the docker:**
JSON structure
{ “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: Get the trading records of the entire exchange within a certain period of time (excluding one's own trades)
The ```method``` in the ```request``` sent by the docker to the listening response function is: ```trades```.
Parameters sent by the docker: The value of ```request.Params.symbol``` is the trading currency, for example: ```btc```, which is sent by the docker based on the strategy settings.
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{ “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 }
**Structure of the final return value sent to the docker:**
JSON structure
{ “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: Get account asset information.
The ```method``` in the ```request``` sent by the docker to the listening response function is: ```accounts```.
Parameters sent by the docker: (Note: Generally, it is to get all the assets of the account! Please refer to the exchange interface to see if it is to get individual assets or the total asset information)
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{
“access_key” : “access_key”,
“secret_key” : “secret_key”,
“nonce” : “1500793319499”, // millisecond timestamp
“method” : “accounts”,
“params” : {},
}
**Structure of the final return value sent to the docker:**
JSON structure
{ “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 }
- 6. Buy, Sell: Place an order for trading (market order or limit order).
The ```method``` in the ```request``` sent by the docker to the listening response function is: ```trade```.
Parameters sent by the docker: ```request.Params.type```: sent by the docker based on whether it is calling ```exchange.Buy``` or ```exchange.Sell```, ```request.Params.price```: the first parameter of the ```API``` function called in the strategy, ```request.Params.amount```: the second parameter of the ```API``` function called in the strategy, ```request.Params.symbol```: sent by the docker based on the set trading currency.
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{ “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 }
**Structure of the final return value sent to the docker:**
JSON structure
{ “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 } }
- 7. GetOrder: Get information of a specific order by order ID
The ```method``` in the ```request``` sent by the docker to the listening response function is: ```order```.
Parameters sent by the docker: ```request.Params.id```, ```request.Params.symbol```.
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{ “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. }
**Structure of the final return value sent to the docker:**
JSON structure
{ “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 } }
- 8. GetOrders: Get information for all unfilled orders
The ```method``` in the ```request``` sent by the docker to the listening response function is ```orders```.
Parameters sent by the docker: ```request.Params.symbol```
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{ “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 }
**Structure of the final return value sent to the docker:**
JSON structure
{ “data”: [{ “id”: 542156, “amount”: 0.25, “price”: 1005, “deal_amount”: 0, “type”: “buy”, // “buy”、”sell” “status”: “open”, // “open” },{ … }] }
- 9. CancelOrder: Cancel an order with the specified order ID
The ```method``` in the ```request``` sent by the docker to the listening response function is ```cancel```.
Parameters sent by the docker: ```request.Params.id``` (string type, the first parameter of the API function called by the strategy), ```request.Params.symbol``` (e.g., btc, sent by the docker based on the currency set by the strategy)
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{ “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 }
**Structure of the final return value sent to the docker:**
JSON structure
{ “data”: true, // true or false }
- 10. IO: Call the exchange.IO function of the FMZ Quant Platform
The ```method``` in the ```request``` sent by the docker to the listening response function starts with ```_api_```.
**The data format carried by the docker when requesting the custom protocol plugin is as follows**
{ “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 }
**Structure of the final return value sent to the docker:**
{ “data”: {…} // The return value of a specific interface call }
As an example, the strategy call:
var io_str = exchange.IO(“api”, “POST”, “cancel_borrow”, “symbol=cny&borrow_id=123”)
Test code in the plugin (go language):
fmt.Println(“request.Method:”, request.Method, “request.Params:”, request.Params)
Plugin command line :
2017/08/31 10:19:59 Running http://127.0.0.1:6666/DigitalAsset ...
**Plugin command line printout of: request.Method, request.Params**
In the request body sent by the docker, the parsed data in the request is as follows:
```request.Method``` is ```__api_cancel_borrow```
```request.Params``` is ```{"borrow_id" : "123", "symbol" : "cny"}```
You can customize the handling of these ```exchange.IO``` calls that directly access the exchange ```API```.
// 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”)) … }
- Support for exchange.GetRawJSON:
The underlying system automatically handles the calls to ```exchange.GetRawJSON```, so there is no need to implement it in the plugin.
- Support for exchange.Go:
The underlying system automatically handles the calls to ```exchange.Go```, so there is no need to handle it in the plugin.
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)
![FMZ Quant Trading Platform Custom Protocol Access to Customized Exchanges](/upload/asset/28da159f41de4f36d2d74.png)
![FMZ Quant Trading Platform Custom Protocol Access to Customized Exchanges](/upload/asset/28e19ce4b53c84f49b877.png)
- Support for futures functions:
You need to implement specific handling in the plugin program for futures functions. For example, setting leverage, contract code, and order direction. You can set a local variable to record this information. To retrieve positions, you will need to access the exchange API to get raw data and process it into the position structure defined in the FMZ platform, and then return it.
When the following functions are called in the strategy, the format of the ```Rpc``` request received by the plugin program is slightly different from other interfaces. You need to pay attention to the format of the ```RpcRequest``` in the custom protocol plugin program. The main difference is that the value of params is a compound structure.
> SetContractType
Set the contract code.
{“access_key”:“123”,“method”:“io”,“nonce”:1623307269528738000,“params”:{“args”:[“quarter”],“code”:2},“secret_key”:“123”}
> SetDirection
Sets the direction for placing futures orders.
{“access_key”:“123”,“method”:“io”,“nonce”:1623308734966484000,“params”:{“args”:[“closesell”],“code”:1},“secret_key”:“123”}
> SetMarginLevel
Sets the futures leverage.
{“access_key”:“123”,“method”:“io”,“nonce”:1623308734966939000,“params”:{“args”:[12],“code”:0},“secret_key”:“123”}
> GetPosition
Get the futures position.
When ```exchange.GetPosition()``` is called:
{“access_key”:“123”,“method”:“io”,“nonce”:1623308734967442000,“params”:{“args”:[],“code”:3},“secret_key”:“123”}
When ```exchange.GetPosition("swap")``` is called:
{“access_key”:“123”,“method”:“io”,“nonce”:1623308734967442000,“params”:{“args”:[“swap”],“code”:3},“secret_key”:“123”}
- Complete Go Language Example of Custom Protocol Plugin (Access to Bitgo Exchange)
/* 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©.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©.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) } “`