Recently, some users have been talking about the Exchange of AOFEX
, considering that there is no example on how to connect to the REST interface of the contract Exchange on FMZ. In this article, we will use AOFEX
as an example to explain how to access the contract Exchange.
There is a distinction between spot exchanges and futures exchanges on FMZ. For example, the three well-known exchanges have both spot trading and contract trading. These different market API interfaces are also different, and even some API systems are completely separate and independent. Therefore, when encapsulating these exchanges on FMZ, there is a distinction between spot and futures.
There are already many examples of using FMZ's general protocol to encapsulate the spot Exchange in the FMZ platform community and documentation, but there is no complete example of encapsulating a contract Exchange yet. However, the general protocol plug-in program that encapsulates the futures version is basically the same as the spot one, only with a few more interfaces.
FMZ general protocol document: https://www.fmz.com/bbs-topic/1052
A general protocol for spot exchanges is attached in the documentation to access DEMO
The interface that the spot exchange object and the futures exchange object need to encapsulate
The main interface of the spot exchange object
- exchange.GetTicker()
Obtain tick market data, both spot and futures. - exchange.GetDepth()
Obtain order book data, both spot and futures. - exchange.GetTrades()
Obtain order flow data (market transaction records), both spot and futures. - exchange.GetRecords()
Obtain K-line data, both spot and futures. - exchange.GetAccount()
Obtain account asset data, both spot and futures. - exchange.Buy()
Place a purchase order, both spot and futures. - exchange.Sell()
Place a sell order, both spot and futures. - exchange.GetOrder()
Obtain order data for the specified ID, both spot and futures. - exchange.GetOrders()
Obtain pending orders in the current operation, both spot and futures. - exchange.CancelOrder()
Cancel the order with the specified ID, both spot and futures.
The Futures Exchange object needs to encapsulate not only these interfaces of the spot Exchange object, but also additional interface functions used for futures.
- exchange.SetMarginLevel()
Set the leverage value of the current product. - exchange.SetDirection()
Set the trading direction of the current product, that is: open long position/open short position/close long position/close short position. - exchange.SetContractType()
Set the current contract code. Because futures have perpetual contracts (swap
), delivery contracts (quarter
), etc., each of which defined on FMZ has its own contract code, details can be found in the FMZ API documentation. These settings also need to be followed when encapsulating, otherwise the existing old strategies may not work properly. - exchange.GetPosition()
Obtain the position data of the current product. It can be seen here that there is no concept of holding positions in the spot, and the logical position can only be calculated by comparing the changes in the account. But futures have positions.
Body data in the request sent by the docker to the general protocol plug-in when the futures-specific interface is called in the strategy
Taking the AOFEX exchange as an example, if the exchange object of the general protocol is configured on FMZ, the API KEY filled in is:
- accessKey : 212f54a1-1c88-1bf5-54a1-f7bf52b3256c
- secretKey : 7RJPKpBJMBkUL87RJPKpkULJPSUpsaKpUL83ysDs
There are accessKey
and secretKey
input boxes on the general protocol configuration page on FMZ. After configuring the general protocol exchange object, the value of the accessKey
field of the JSON format data of the Body in the request received by the general protocol plug-in program is 212f54a1-1c88-1bf5-54a1-f7bf52b3256c
, and the value of the secret_key
field is 7RJPKpBJMBkUL87RJPKpkULJPSUpsaKpUL83ysDs
.
When the following interfaces are called, the docker will issue an RPC request to the general protocol plugin-in program as follows:
- When
exchange.SetMarginLevel(10)
is called in the strategy, the data in the requested Body is:
{ "access_key":"212f54a1-1c88-1bf5-54a1-f7bf52b3256c", "method":"io", "nonce":1631858961289247000, "params":{"args":[10],"code":0}, "secret_key":"7RJPKpBJMBkUL87RJPKpkULJPSUpsaKpUL83ysDs" }
Call exchange.SetMarginLevel(10), and the parameter is passed into 10.
- When
exchange.SetDirection("buy")
is called in the strategy, the data in the requested Body is:
{ "access_key":"212f54a1-1c88-1bf5-54a1-f7bf52b3256c", "method":"io", "nonce":1631860438946922000, "params":{"args":["buy"],"code":1}, "secret_key":"7RJPKpBJMBkUL87RJPKpkULJPSUpsaKpUL83ysDs" }
Call exchange.SetDirection("buy") , and the parameter is passed into "buy".
- When
exchange.SetContractType("swap")
is called in the strategy, the data in the requested Body is:
{ "access_key":"212f54a1-1c88-1bf5-54a1-f7bf52b3256c", "method":"io", "nonce":1631860847525039000, "params":{"args":["swap"],"code":2}, "secret_key":"7RJPKpBJMBkUL87RJPKpkULJPSUpsaKpUL83ysDs" }
Call exchange.SetContractType("swap") , and the parameter is passed into "swap".
- When
exchange.GetPosition()
is called in the strategy, the data in the requested Body is:
{ "access_key":"212f54a1-1c88-1bf5-54a1-f7bf52b3256c", "method":"io", "nonce":1631860996119505000, "params":{"args":[],"code":3}, "secret_key":"7RJPKpBJMBkUL87RJPKpkULJPSUpsaKpUL83ysDs" }
When calling exchange.GetPosition(), no parameters are passed.
Observing the JSON data in the Body of the above request, we can see that:
- When a function specific to the futures exchange object is called, the value of the
method
field in the requested Body isio
. - Distinguishing these functions needs to be judged from the
code
in theparams
field, that is: - If
code
is0
, it isSetMarginLevel
. - If
code
is1
, it isSetDirection
. - If
code
is2
, it isSetContractType
. - If
code
is3
, it isGetPosition
. - When these functions specific to the futures exchange object are called, the parameters passed in are all in the
args
of theparams
field in the request Body.
Compared with the stock version of the Go language plug-in program example, it is necessary to make some extensions in the OnPost
function:
See the following code where the comment "Function needed to extend the futures exchange object" is located.
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 := newZG(request.AccessKey, request.SecretKey) var symbol string if _, ok := request.Params["symbol"]; ok { symbol = strings.ToUpper(request.Params["symbol"].(string)) } var data interface{} switch request.Method { case "ticker": data, err = e.GetTicker(symbol, "GET") case "depth": data, err = e.GetDepth(symbol, "GET") case "trades": data, err = e.GetTrades(symbol, "GET") case "records": data, err = e.GetRecords(toInt64(request.Params["period"]), symbol, "GET") case "accounts": data, err = e.GetAccount(symbol, "GET") case "trade": side := request.Params["type"].(string) if side == "buy" { side = "BUY" } else { side = "SELL" } price := toFloat(request.Params["price"]) amount := toFloat(request.Params["amount"]) data, err = e.Trade(side, price, amount, symbol, "POST") case "orders": data, err = e.GetOrders(symbol, "POST") case "order": data, err = e.GetOrder(toString(request.Params["id"]), symbol, "POST") case "cancel": data, err = e.CancelOrder(toString(request.Params["id"]), symbol, "POST") default: if strings.HasPrefix(request.Method, "__api_") { params := map[string]interface{}{} for k, v := range request.Params { params[k] = toString(v) } data, err = e.tapiCall(request.Method[6:], params, "GET") } else if request.Method == "io" { // Functions needed to extend the futures exchange object code := toString(request.Params["code"]) if code == "0" { // Process SetMarginLevel // ... } else if code == "1" { // Process SetDirection // ... } else if code == "2" { // Process SetContractType // ... } else if code == "3" { // Process GetPosition // ... } else { panic(errors.New(request.Method + " not support")) } } else { panic(errors.New(request.Method + " not support")) } } if err != nil { panic(err) } ret = map[string]interface{}{ "data": data, } return }
SetMarginLevel
/SetDirection
/SetContractType
, the three functions are literally used to set the relevant configuration of the current trading species. Among them, SetDirection
/SetContractType
is designed to set a local variable to record the current order direction (that is, you need to read this setting when placing an order to know which direction to place an order, the reason is that there are two directions for futures buying: open long and short, so they need to be distinguished) and the current contract code (which contract is clearly queried when obtaining market quotations, orders, etc.).
SetMarginLevel
, it needs to be designed specifically according to the leverage mechanism of the exchange (1. Leverage parameters are passed as parameters in the order placement interface. 2. The exchange has a leverage interface).
GetPosition is a function to get the current position of the species. When the general protocol plug-in program obtains the data returned from the exchange position interface, it can directly construct the same data structure as [
position](https://www.fmz.com/api#position) on FMZ.
Example of the complete AOFEX futures general contract plug-in program:
Go language
/* CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build xxx.go */ package main import ( "bytes" "encoding/hex" "crypto/sha1" "encoding/json" "errors" "flag" "fmt" "io/ioutil" "log" "net/http" "net/url" "sort" "strconv" "strings" "time" // proxy "golang.org/x/net/proxy" "crypto/tls" "math/rand" ) var isUsedProxy bool = false var ct string = "" var direction string = "buy" var marginLevel float64 = 10 var currSymbol string = "" 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 } 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 } return ret } type headerTuple struct { name string value string } type Request struct { headers []headerTuple Proxy string Method string Uri string Body interface{} QueryString interface{} Timeout time.Duration ContentType string Accept string Host string UserAgent string } func (r *Request) AddHeader(name string, value string) { if r.headers == nil { r.headers = []headerTuple{} } r.headers = append(r.headers, headerTuple{name: name, value: value}) } type iAOFEX struct { accessKey string secretKey string clientID string currency string opCurrency string baseCurrency string quoteCurrency string apiBase string timeout time.Duration timeLocation *time.Location // ext contractTypes []string } 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].Key < ms[j].Key } func (ms MapSorter) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] } func encodeParams(params map[string]string, escape bool) string { ms := NewMapSorter(params) sort.Sort(ms) v := url.Values{} for _, item := range ms { v.Add(item.Key, item.Val) } if escape { return v.Encode() } var buf bytes.Buffer keys := make([]string, 0, len(v)) for k := range v { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { vs := v[k] prefix := k + "=" for _, v := range vs { if buf.Len() > 0 { buf.WriteByte('&') } buf.WriteString(prefix) buf.WriteString(v) } } return buf.String() } func newiAOFEX(accessKey, secretKey string) *iAOFEX { s := new(iAOFEX) s.accessKey = accessKey s.secretKey = secretKey s.apiBase = "https://openapi-contract.aofex.info" s.timeout = 20 * time.Second s.timeLocation = time.FixedZone("Asia/Shanghai", 8*60*60) s.contractTypes = []string{"swap"} return s } func (p *iAOFEX) isValidContractType(contractType string) bool { for _, t := range p.contractTypes { if contractType == t { return true } } return false } func (p *iAOFEX) calcContractTypeMap(currency string) (contractTypeMap map[string]string, err error) { var baseCurrency, quoteCurrency string contractTypeMap = map[string]string{} if arr := strings.SplitN(currency, "_", 2); len(arr) == 2 { baseCurrency = arr[0] quoteCurrency = arr[1] } else { err = errors.New("symbol error!") return } contractTypeMap["swap"] = fmt.Sprintf("%s-%s", strings.ToUpper(baseCurrency), strings.ToUpper(quoteCurrency)) return } func (p *iAOFEX) apiCall(method string) (*Json, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", p.apiBase, method), nil) if err != nil { return nil, err } fmt.Printf("\n %c[1;44;32m%s%c[0m\n", 0x1B, "apiCall GET create req:" + fmt.Sprintf("%s%s", p.apiBase, method), 0x1B) fmt.Println("req:", req) req.Header.Set("Content-Type", "application/json;utf-8") // proxy strProxy := "" client := http.DefaultClient if isUsedProxy { var auth *proxy.Auth proxyAddr := strings.Split(strProxy, "//")[1] if strings.Contains(proxyAddr, "@") { arr := strings.SplitN(proxyAddr, "@", 2) arrAuth := strings.SplitN(arr[0], ":", 2) proxyAddr = arr[1] auth = &proxy.Auth{} auth.User = arrAuth[0] if len(arrAuth) == 2 { auth.Password = arrAuth[1] } } var dialer proxy.Dialer if dialer, err = proxy.SOCKS5("tcp", proxyAddr, auth, proxy.Direct); err == nil { client = &http.Client{ Transport: &http.Transport{ Dial: dialer.Dial, MaxIdleConnsPerHost: 5, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, ResponseHeaderTimeout: 20 * time.Second, }, Timeout: 20 * time.Second, } } else { return nil, err } } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var js *Json js, err = NewJson(b) if err != nil { return nil, err } // fault tolerant if _, ok := js.data.(map[string]interface{}); ok { if code, ok := js.MustMap()["code"]; ok { if toString(code) != "0" { err = errors.New(fmt.Sprintf("%v", js.data)) } } } return js, err } func (p *iAOFEX) GetTicker(symbol string) (ticker interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } var js *Json js, err = p.apiCall(fmt.Sprintf("/openApi/contract/market?symbol=%s", realCt)) if err != nil { return } depth, errDepth := p.GetDepth(symbol) if errDepth != nil { err = errDepth return } ask1 := depth.(map[string]interface{})["asks"].([][2]float64)[0][0] bid1 := depth.(map[string]interface{})["bids"].([][2]float64)[0][0] mp := js.Get("result").MustMap() ticker = map[string]interface{}{ "time": time.Now().UnixNano() / 1e6, "buy": toFloat(bid1), "sell": toFloat(ask1), "last": toFloat(mp["close"]), "high": toFloat(mp["high"]), "low": toFloat(mp["low"]), "vol": toFloat(mp["vol"]), } return } func (p *iAOFEX) GetDepth(symbol string) (depth interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } var js *Json js, err = p.apiCall(fmt.Sprintf("/openApi/contract/depth?symbol=%s", realCt)) if err != nil { return } asks := [][2]float64{} bids := [][2]float64{} for _, pair := range js.Get("result").Get("asks").MustArray() { arr := pair.([]interface{}) asks = append(asks, [2]float64{toFloat(arr[0]), toFloat(arr[1])}) } for _, pair := range js.Get("result").Get("bids").MustArray() { arr := pair.([]interface{}) bids = append(bids, [2]float64{toFloat(arr[0]), toFloat(arr[1])}) } depth = map[string]interface{}{ "time": js.Get("result").Get("ts").MustInt64(), "asks": asks, "bids": bids, } return } func (p *iAOFEX) GetTrades(symbol string) (trades interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } var js *Json js, err = p.apiCall(fmt.Sprintf("/openApi/contract/trade?symbol=%s", realCt)) if err != nil { return } items := []map[string]interface{}{} for _, pair := range js.Get("result").Get("data").MustArray() { item := map[string]interface{}{} mp := pair.(map[string]interface{}) item["id"] = toString(mp["id"]) item["price"] = toFloat(mp["price"]) item["amount"] = toFloat(mp["amount"]) item["time"] = toInt64(mp["ts"]) if toString(mp["direction"]) == "buy" { item["type"] = "buy" } else { item["type"] = "sell" } items = append(items, item) } for i := 0; i < len(items); i++ { for j := 0; j < len(items)-i-1; j++ { if toInt64(items[j]["time"]) > toInt64(items[j+1]["time"]) { items[j], items[j+1] = items[j+1], items[j] } } } trades = items return } func (p *iAOFEX) GetRecords(step int64, symbol string) (records interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } var periodDict map[int64]string = map[int64]string{ 1 : "1min", 5 : "5min", 15 : "15min", 30 : "30min", 60 : "1hour", 1440 : "1day", } period, okPeriod := periodDict[step] if !okPeriod { err = errors.New("period not support") return } var js *Json js, err = p.apiCall(fmt.Sprintf("/openApi/contract/kline?symbol=%s&period=%s&size=500", realCt, period)) if err != nil { return } items := []interface{}{} recordsData := js.Get("result").Get("data").MustArray() for i := len(recordsData) - 1 ; i >= 0 ; i-- { mp := recordsData[i].(map[string]interface{}) item := [6]interface{}{} item[0] = toInt64(mp["id"]) // time item[1] = toFloat(mp["open"]) // open item[2] = toFloat(mp["high"]) // high item[3] = toFloat(mp["low"]) // low item[4] = toFloat(mp["close"]) // close item[5] = toFloat(mp["vol"]) // vol items = append(items, item) } records = items return } func JSON_Encode(d interface{}) string { buffer := &bytes.Buffer{} encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(false) encoder.Encode(d) return buffer.String() } func (p *iAOFEX) tapiCall(httpMethod string, method string, params map[string]string) (js *Json, err error) { if params == nil { params = map[string]string{} } nonce := toString(time.Now().UnixNano() / 1e9) strLetter := "124567890abcdefghijklmnopqrstuvwxyz" for i := 0 ; i < 5 ; i++ { rand.Seed(time.Now().UnixNano()) nonce += string(strLetter[rand.Intn(len(strLetter))]) } arrParams := []string{} arrParams = append(arrParams, p.accessKey) arrParams = append(arrParams, p.secretKey) arrParams = append(arrParams, nonce) for k, v := range params { arrParams = append(arrParams, fmt.Sprintf("%s=%s", k, v)) } sort.Strings(arrParams) strSign := "" for _, ele := range arrParams { strSign += toString(ele) } h := sha1.New() h.Write([]byte(strSign)) signature := hex.EncodeToString(h.Sum(nil)) strUrl := fmt.Sprintf("%s%s", p.apiBase, method) if len(params) > 0 { strUrl = fmt.Sprintf("%s%s?%s", p.apiBase, method, encodeParams(params, false)) } req, err := http.NewRequest(httpMethod, strUrl, nil) if err != nil { return nil, err } req.Header.Add("Nonce", nonce) req.Header.Add("Token", p.accessKey) req.Header.Add("Signature", signature) strProxy := "" client := http.DefaultClient if isUsedProxy { var auth *proxy.Auth proxyAddr := strings.Split(strProxy, "//")[1] if strings.Contains(proxyAddr, "@") { arr := strings.SplitN(proxyAddr, "@", 2) arrAuth := strings.SplitN(arr[0], ":", 2) proxyAddr = arr[1] auth = &proxy.Auth{} auth.User = arrAuth[0] if len(arrAuth) == 2 { auth.Password = arrAuth[1] } } var dialer proxy.Dialer if dialer, err = proxy.SOCKS5("tcp", proxyAddr, auth, proxy.Direct); err == nil { client = &http.Client{ Transport: &http.Transport{ Dial: dialer.Dial, MaxIdleConnsPerHost: 5, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, ResponseHeaderTimeout: 20 * time.Second, }, Timeout: 20 * time.Second, } } else { return nil, err } } fmt.Printf("\n %c[1;44;32m%s%c[0m\n", 0x1B, "apiCall GET create req:" + fmt.Sprintf("%s%s", p.apiBase, method), 0x1B) fmt.Println("tapiCall req:", req) resp, err := client.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 err != nil { return nil, err } // fault tolerant if mp, ok := js.data.(map[string]interface{}); ok { if errno, ok := mp["errno"]; !ok || toString(errno) != "0" { err = errors.New(fmt.Sprintf("%v", js.data)) } } else { err = errors.New(fmt.Sprintf("%v", js.data)) } return js, err } func (p *iAOFEX) GetAccount(symbol string) (account interface{}, err error) { symbol = currSymbol mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } var js *Json js, err = p.tapiCall("GET", "/openApi/contract/walletList", nil) if err != nil { return } assets := map[string]map[string]interface{}{} for _, ele := range js.Get("result").MustArray() { dic := ele.(map[string]interface{}) if realCt != toString(dic["symbol"]) { continue } arr := strings.SplitN(toString(dic["symbol"]), "-", 2) if arr[1] == "USDT" { if _, ok := assets[arr[1]]; !ok { assets[arr[1]] = map[string]interface{}{} } assets[arr[1]]["currency"] = arr[1] assets[arr[1]]["Info"] = dic assets[arr[1]]["free"] = toFloat(dic["avail"]) assets[arr[1]]["frozen"] = toFloat(dic["frozen"]) } } accounts := []map[string]interface{}{} for _, pair := range assets { accounts = append(accounts, pair) } account = accounts return } func (p *iAOFEX) Trade(side string, price, amount float64, symbol string) (orderId interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } params := map[string]string{} if direction == "buy" { params["contract_type"] = "open" } else if direction == "sell" { params["contract_type"] = "open" } else if direction == "closebuy" { params["contract_type"] = "close" } else if direction == "closesell" { params["contract_type"] = "close" } else { err = errors.New("invalid direction!") return } if (side == "buy-limit" || side == "buy-market") && (direction == "sell" || direction == "closebuy") { err = errors.New("invalid direction!") return } else if (side == "sell-limit" || side == "sell-market") && (direction == "buy" || direction == "closesell") { err = errors.New("invalid direction!") return } params["type"] = side params["lever_rate"] = toString(marginLevel) params["amount"] = toString(amount) params["symbol"] = realCt if price > 0 { params["price"] = toString(price) } var js *Json js, err = p.tapiCall("POST", "/openApi/contract/add", params) if err != nil { return } orderId = map[string]string{"id": toString(js.MustMap()["result"])} return } func (p *iAOFEX) GetOrders(symbol string) (orders interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } from := "" limit := 100 items := []map[string]interface{}{} for { params := map[string]string{ "symbol" : realCt, "limit" : toString(limit), } if from != "" { params["from"] = toString(from) } var js *Json js, err = p.tapiCall("GET", "/openApi/contract/currentList", params) if err != nil { return } arr := js.Get("result").MustArray() for _, ele := range arr { mp := ele.(map[string]interface{}) item := map[string]interface{}{} item["id"] = toString(mp["order_id"]) item["amount"] = toFloat(mp["amount"]) item["price"] = toFloat(mp["price"]) item["deal_amount"] = toFloat(mp["deal_amount"]) item["avg_price"] = toFloat(mp["price_avg"]) if toString(mp["type"]) == "buy-limit" || toString(mp["type"]) == "buy-market" || toString(mp["type"]) == "buy-tactics" || toString(mp["type"]) == "buy-market-tactic" || toString(mp["type"]) == "buy-plan" || toString(mp["type"]) == "buy-market-plan" { item["type"] = "buy" } else { item["type"] = "sell" } item["status"] = "open" item["contract_type"] = ct items = append(items, item) from = toString(mp["order_id"]) } if len(arr) < limit { break } } return items, nil } func (p *iAOFEX) GetOrder(orderId string, symbol string) (order interface{}, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } var js *Json js, err = p.tapiCall("GET", "/openApi/contract/historyList", map[string]string{ "from" : toString(orderId), "symbol" : realCt, "limit" : "1", }) if err != nil { return } item := map[string]interface{}{} for _, ele := range js.Get("result").MustArray() { mp := ele.(map[string]interface{}) if realCt != toString(mp["symbol"]) || toString(mp["order_id"]) != toString(orderId) { continue } item["id"] = toString(mp["order_id"]) item["amount"] = toFloat(mp["amount"]) item["price"] = toFloat(mp["price"]) item["deal_amount"] = toFloat(mp["deal_amount"]) item["avg_price"] = toFloat(mp["price_avg"]) item["contract_type"] = ct if toString(mp["type"]) == "buy-limit" || toString(mp["type"]) == "buy-market" || toString(mp["type"]) == "buy-tactics" || toString(mp["type"]) == "buy-market-tactic" || toString(mp["type"]) == "buy-plan" || toString(mp["type"]) == "buy-market-plan" { item["type"] = "buy" } else { item["type"] = "sell" } switch toString(mp["status"]) { case "1", "2": item["status"] = "open" case "3": item["status"] = "closed" case "4", "5", "6": item["status"] = "cancelled" } return item, nil } err = errors.New("order not found") return } func (p *iAOFEX) CancelOrder(orderId string, symbol string) (ret bool, err error) { mpCt, mpCtErr := p.calcContractTypeMap(symbol) if mpCtErr != nil { err = mpCtErr return } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") return } _, err = p.tapiCall("POST", "/openApi/contract/cancel", map[string]string{ "order_ids" : toString(orderId), "symbol" : realCt, }) if err != nil { return } ret = true return } type RpcRequest struct { AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` Nonce int64 `json:"nonce"` Method string `json:"method"` Params map[string]interface{} `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 := newiAOFEX(request.AccessKey, request.SecretKey) symbol := strings.ToUpper(toString(request.Params["symbol"])) if _, ok := request.Params["symbol"]; ok { currSymbol = symbol } 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 := toString(request.Params["type"]) if side == "buy" { side = "buy-limit" if toFloat(request.Params["price"]) <= 0 { side = "buy-market" } } else { side = "sell-limit" if toFloat(request.Params["price"]) <= 0 { side = "sell-market" } } 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(toString(request.Params["id"]), symbol) case "cancel": data, err = e.CancelOrder(toString(request.Params["id"]), symbol) default: if strings.HasPrefix(request.Method, "__api_") { params := map[string]string{} for k, v := range request.Params { params[k] = toString(v) } data, err = e.tapiCall("GET", request.Method[6:], params) } else if request.Method == "io" { code := toString(request.Params["code"]) if code == "0" { if args, ok := request.Params["args"].([]interface{}); ok && len(args) == 1 { marginLevel = toFloat(args[0]) } else { err = errors.New(fmt.Sprintf("%v", request.Params)) } } else if code == "1" { if args, ok := request.Params["args"].([]interface{}); ok && len(args) == 1 { orderDirection := toString(args[0]) if orderDirection == "buy" || orderDirection == "sell" || orderDirection == "closebuy" || orderDirection == "closesell" { direction = orderDirection data = orderDirection } else { err = errors.New(fmt.Sprintf("not support orderDirection: %s", orderDirection)) } } else { err = errors.New(fmt.Sprintf("%v", request.Params)) } } else if code == "2" { if args, ok := request.Params["args"].([]interface{}); ok && len(args) == 1 { contractType := toString(args[0]) if e.isValidContractType(contractType) { ct = contractType data = contractType } else { err = errors.New(fmt.Sprintf("not support contractType: %s", contractType)) } } else { err = errors.New(fmt.Sprintf("%v", request.Params)) } } else if code == "3" { symbol := currSymbol mpCt, mpCtErr := e.calcContractTypeMap(symbol) if mpCtErr != nil { panic(mpCtErr) } realCt, ok := mpCt[ct] if !ok { err = errors.New("invalid contractType!") panic(err) } var js *Json js, err = e.tapiCall("GET", "/openApi/contract/position", map[string]string{ "symbol" : realCt, }) if err != nil { panic(err) } items := []map[string]interface{}{} for _, ele := range js.Get("result").MustArray() { mp := ele.(map[string]interface{}) item := map[string]interface{}{} item["MarginLevel"] = toFloat(mp["lever_rate"]) item["Amount"] = toFloat(mp["amount"]) item["FrozenAmount"] = toFloat(mp["contract_frozen"]) item["Price"] = toFloat(mp["open_price_avg"]) item["Profit"] = toFloat(mp["un_profit"]) if toString(mp["type"]) == "1" { item["Type"] = 0 } else { item["Type"] = 1 } item["ContractType"] = ct item["Margin"] = toFloat(mp["bood"]) items = append(items, item) } data = items } else { panic(errors.New(request.Method + " not support")) } } 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:6617", "bind addr") flag.Parse() if *addr == "" { flag.Usage() return } basePath := "/AOFEX" log.Println("Running ", fmt.Sprintf("http://%s%s", *addr, basePath), "...") http.HandleFunc(basePath, OnPost) http.ListenAndServe(*addr, nil) }