Vous pouvez utiliser ce protocole général pour accéder à n'importe quel échange qui fournit le commerce d'API, le protocole spécifique d'API n'est pas limité, qu'il s'agisse de repos, de websocket, de fixation... Tout peut être accédé à l'utilisation. Exemple de protocole personnalisé Python:https://www.fmz.com/strategy/101399
Le paramètre de l'adresse d'écoute et du port du plugin de protocole personnalisé peut être le suivant:
Par exemple:http://127.0.0.1:6666/DigitalAsset
ouhttp://127.0.0.1:6666/exchange
.
Pourquoi avons-nous besoin de mettre cesIntégration intellectuelleetles cheminsJe ne sais pas.
C'est parce que quandajout d'échangepage dans leTableau de bord de la plateforme quantique FMZ, en sélectionnant l'option API-KEY
. Cette adresse de service indique au docker où accéder à l'adresse IP et au port (le docker et le plugin Custom Protocol peuvent ne pas être exécutés sur le même appareil).http://127.0.0.1:6666/DigitalAsset
. DigitalAsset
est juste un exemple et peut être remplacé par un nom de votre choix.
Sur la page access key
etsecret key
Cependant, certaines interfaces API des échanges nécessitent le passage du mot de passe de négociation (par exemple, l'interface de placement des commandes de certains échanges). Dans de tels cas, puisque la page du protocole personnalisé ne dispose pas de commandes supplémentaires pour entrer ces informations, nous pouvons inclure les informations de configuration supplémentaires requises dans lesecret key
(ouaccess key
Si l'information n'est pas sensible). Ensuite, dans le programme de plugin Custom Protocol, nous pouvons exécuter une chaînesplit
opération de séparation de ces données, comme indiqué sur l'image.
Et puis dans le plugin, traiter pour obtenirXXX_PassWord
Je suis désolée.
Par exemple, dans l'exemple complet à la fin de cet article, dans la fonction 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
}
En analysant à nouveau les paramètres dans les données de demande, le docker envoie les données de demande comme:
"secret_key" : "XXX",
Le plugin reçoit les données entrantes contenant ce type d'informations et sépare le XXX_PassWord de celui-ci en fonction du séparateur de virgules, de sorte qu'il obtienne les données supplémentaires transmises.
Exemple d'un plugin de protocole personnalisé globalmain
fonction:Go
Description de la langue:
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)
}
Le programme request
Une fois qu'une requête est reçue, elle appelle la fonction de réponse à exécuter et analyse ensuite les paramètres à partir des données de la requête.
/* 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.
}
*/
Donc, basé sur leMethod
dans le domainerequest
structure obtenue par JSON désérialisation de la demande Les données du corps reçues dans le programme Universal Protocol Plugin, nous pouvons utiliser unswitch
déclaration pour classer et gérer les différentes API FMZ Quant appelées sur le docker (c'est-à-dire, identifier quelles sont les API FMZ QuantAPI
la stratégie en cours d'exécution sur le docker est appelée):
Exemple dansGo
langue:
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:
...
Après l'exécution de ces branches, les données renvoyées doivent être écrites dans la structure que le programme Custom Protocol Plugin utilisera pour répondre à la demande du docker.
Exemple en langage 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
}()
En gros divisé en deux catégories:
GetTicker()
GetDepth()
GetTrades()
GetRecords(period)
…
Buy
, Sell
GetOrder(id)
GetOrders()
GetAccount()
CancelOrder(id)
- Je ne sais pas.
Les méthodes de signature peuvent varier d'un échange à l'autre et doivent être écrites spécifiquement en fonction des besoins.
Certaines interfaces API FMZ Quant, telles que
GetName()
,GetLabel()
, etc., n'envoient pas de demandes à laPlugin de protocole personnaliséquand il est appelé. Lorsque vous appelezexchange.GetName()
, l'échange configuré dans le plugin universel renverraExchange .
Lemethod
dans lerequest
Envoyé par lele dockerà la fonction de réponse d'écouteticker
.
Le docker envoie le paramètre:request.Params.symbol
, qui est envoyé par le docker en fonction de la devise définie sur la page du robot.
Le format de données (JSON) transporté dans le corps de la demande lorsque le docker demande le plugin de protocole personnalisé
{
"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 de la valeur de retour finale envoyée au docker: (c'est-à-dire le format dans lequel les données sont renvoyées au docker après que l'interface d'échange a été demandée par le plug-in de protocole commun)
Structure du format JSON
{
"data": {
"time": 1500793319499, // Millisecond timestamp, integer
"buy": 1000, // floating-point type as follows
"sell": 1001,
"last": 1005,
"high": 1100,
"low": 980,
"vol": 523,
}
}
Lemethod
dans lerequest
envoyé par le docker à la fonction de réponse d'écoute estrecords
.
Le docker envoie les paramètres:request.Params.period
, qui est associé au premier paramètre duexchange.GetRecords
La fonction actuellerequest.Params.period
représente la période en minutes. Par exemple, la période quotidienne est60*24
, qui est1440
. request.Params.symbol
est envoyé par le docker en fonction de la devise définie.
Le format de données transporté dans le corps de la demande lorsque le docker demande le plugin de protocole personnalisé.
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format 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],
...
]
}
Données du test de langue:
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}
}
Plateforme quantique FMZLog
affichagerecords
les données:
[
{"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}
]
Remarque: 1. le premier élément du tableau en deux dimensions est de typeint
Le docker multipliera automatiquement l'horodatage par 1000, comme mentionné ci-dessus.
Lemethod
dans lerequest
envoyé par le docker à la fonction de réponse d'écoute estdepth
.
Le docker envoie le paramètre:request.Params.symbol
, qui est envoyé par le docker en fonction de la devise définie dans la stratégie.
Le format de données transporté dans le corps de la demande lorsque le docker demande le plugin de protocole personnalisé
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format JSON
{
"data" : {
"time" : 1500793319499,
"asks" : [ [1000, 0.5], [1001, 0.23], [1004, 2.1], ... ],
"bids" : [ [999, 0.25], [998, 0.8], [995, 1.4], ... ],
}
}
Lemethod
dans lerequest
envoyé par le docker à la fonction d'écoute est:trades
.
Paramètres envoyés par le docker: la valeur derequest.Params.symbol
est la monnaie de négociation, par exemple:btc
, qui est envoyé par le docker en fonction des paramètres de stratégie.
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format 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",
},{
...
}
]
}
Lemethod
dans lerequest
envoyé par le docker à la fonction d'écoute est:accounts
.
Paramètres envoyés par le docker: (Remarque: Généralement, il s'agit d'obtenir tous les actifs du compte! Veuillez vous référer à l'interface d'échange pour voir s'il s'agit d'obtenir les actifs individuels ou les informations sur les actifs totaux)
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"access_key" : "access_key",
"secret_key" : "secret_key",
"nonce" : "1500793319499", // millisecond timestamp
"method" : "accounts",
"params" : {},
}
Structure de la valeur de retour finale envoyée au docker:
Structure du format 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
}
Lemethod
dans lerequest
envoyé par le docker à la fonction d'écoute est:trade
.
Paramètres envoyés par le docker:request.Params.type
: envoyé par le docker en fonction de l'appelexchange.Buy
ouexchange.Sell
, request.Params.price
: le premier paramètre duAPI
fonction appelée dans la stratégie,request.Params.amount
: le deuxième paramètre duAPI
fonction appelée dans la stratégie,request.Params.symbol
: envoyé par le docker en fonction de la monnaie de négociation définie.
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format 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
}
}
Lemethod
dans lerequest
envoyé par le docker à la fonction d'écoute est:order
.
Paramètres envoyés par le docker:request.Params.id
, request.Params.symbol
.
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format 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
}
}
Lemethod
dans lerequest
envoyé par le docker à la fonction de réponse d'écoute estorders
.
Paramètres envoyés par le docker:request.Params.symbol
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format JSON
{
"data": [{
"id": 542156,
"amount": 0.25,
"price": 1005,
"deal_amount": 0,
"type": "buy", // "buy"、"sell"
"status": "open", // "open"
},{
...
}]
}
Lemethod
dans lerequest
envoyé par le docker à la fonction de réponse d'écoute estcancel
.
Paramètres envoyés par le docker:request.Params.id
(type de chaîne, premier paramètre de la fonction API appelée par la stratégie),request.Params.symbol
(par exemple, btc, envoyé par le docker en fonction de la devise définie par la stratégie)
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"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 de la valeur de retour finale envoyée au docker:
Structure du format JSON
{
"data": true, // true or false
}
Lemethod
dans lerequest
envoyé par le docker à la fonction de réponse d'écoute commence par_api_
.
Le format de données transporté par le docker lors de la demande du plugin de protocole personnalisé est le suivant
{
"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 de la valeur de retour finale envoyée au docker:
{
"data": {...} // The return value of a specific interface call
}
À titre d'exemple, l'appel à la stratégie:
var io_str = exchange.IO("api", "POST", "cancel_borrow", "symbol=cny&borrow_id=123")
Code de test dans le plug-in (langue d'entrée):
fmt.Println("request.Method:", request.Method, "request.Params:", request.Params)
La ligne de commande du plugin: 2017/08/31 10:19:59 Je courshttp://127.0.0.1:6666/DigitalAsset …
Impression de ligne de commande du plugin de: requête.Méthode, requête.ParamètresDans le corps de la demande envoyée par le docker, les données analysées dans la demande sont les suivantes:request.Method
est__api_cancel_borrow
request.Params
est{"borrow_id" : "123", "symbol" : "cny"}
Vous pouvez personnaliser la manipulation de cesexchange.IO
appels qui accèdent directement à l'échangeAPI
.
# 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"))
...
}
Le système sous-jacent gère automatiquement les appels versexchange.GetRawJSON
, donc il n'est pas nécessaire de le mettre en œuvre dans le plugin.
Le système sous-jacent gère automatiquement les appels versexchange.Go
, donc il n'y a pas besoin de le gérer dans le 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)
# 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.
Vous devez implémenter une manipulation spécifique dans le programme du plugin pour les fonctions futures. Par exemple, définir le levier, le code de contrat et la direction de l'ordre. Vous pouvez définir une variable locale pour enregistrer ces informations. Pour récupérer les positions, vous devrez accéder à l'API d'échange pour obtenir des données brutes et les traiter dans la structure de position définie dans la plate-forme FMZ, puis les retourner.
Lorsque les fonctions suivantes sont appelées dans la stratégie, le format duRpc
La demande reçue par le programme de plug-in est légèrement différente des autres interfaces.RpcRequest
La principale différence est que la valeur des paramètres est une structure composée.
Type de contrat défini Définissez le code du contrat.
{"access_key":"123","method":"io","nonce":1623307269528738000,"params":{"args":["quarter"],"code":2},"secret_key":"123"}
Définir la direction Définit la direction de la mise en place des ordres à terme.
{"access_key":"123","method":"io","nonce":1623308734966484000,"params":{"args":["closesell"],"code":1},"secret_key":"123"}
Définir le niveau de marge Définit le levier des contrats à terme.
{"access_key":"123","method":"io","nonce":1623308734966939000,"params":{"args":[12],"code":0},"secret_key":"123"}
Obtenez la position Prends la position des futures. Quand?
exchange.GetPosition()
est appelé:
{"access_key":"123","method":"io","nonce":1623308734967442000,"params":{"args":[],"code":3},"secret_key":"123"}
Quand?exchange.GetPosition("swap")
est appelé:
{"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)
}