FMZ 양자 거래 플랫폼 사용자 지정 프로토콜 사용자 지정 거래소 접근

저자:리디아, 창작: 2023-07-12 15:29:54, 업데이트: 2024-01-03 21:01:07

img

FMZ 양자 거래 플랫폼 사용자 지정 프로토콜 사용자 지정 거래소 접근

사용자 지정 프로토콜 사용 문서

이 일반적인 프로토콜을 사용하여 API 트레이딩을 제공하는 모든 교환에 액세스 할 수 있습니다. 특정 API 프로토콜은 제한되지 않습니다. 휴식, 웹 소켓, 수정... 모두 사용할 수 있습니다. 파이썬 사용자 지정 프로토콜 예제:https://www.fmz.com/strategy/101399

1. 사용자 지정 프로토콜 플러그인 동작, 포트 설정

Custom Protocol Plugin의 듣기 주소와 포트의 설정은 다음과 같습니다. 예를 들어:http://127.0.0.1:6666/DigitalAsset또는http://127.0.0.1:6666/exchange.

왜 우리가 이IP그리고경로- 왜? 왜냐하면,교환을 추가페이지에FMZ 퀀트 플랫폼 대시보드, Custom Protocol 옵션을 선택하면API-KEY이 서비스 주소는 도커에 IP 및 포트를 액세스할 수 있는 위치를 알려줍니다. 예를 들어, 서비스 주소는http://127.0.0.1:6666/DigitalAsset. DigitalAsset그냥 예제일 뿐이고, 여러분이 원하는 이름으로 대체할 수 있습니다.

FMZ 퀀트 플랫폼의 Add Exchange 페이지에서, 일반적으로 교환 구성은access key그리고secret key. 그러나 일부 거래소 API 인터페이스는 거래 비밀번호가 전달되어야 합니다. (예를 들어, 특정 거래소의 주문 배치 인터페이스). 그런 경우, 사용자 지정 프로토콜 페이지에 이 정보를 입력하는 추가 컨트롤이 없기 때문에, 우리는 추가로 필요한 구성 정보를 포함 할 수 있습니다secret key(또는access key만약 정보가 민감하지 않다면) Custom Protocol 플러그인 프로그램에서 문자열을 실행할 수 있습니다.split이 데이터를 분리하는 작업, 예제 그림에서 보여진 것처럼.

img

그리고 플러그인에서, 그것을 처리하여XXX_PassWord- 그래요 예를 들어, 이 포스트의 끝에 있는 완전한 예제에서 newBitgo 함수에서:

func newBitgo(accessKey, secretKey string) *iBitgo {
    s := new(iBitgo)
    s.accessKey = accessKey
    s.secretKey = secretKey
    // Additional configuration information in the secretKey can be separated here and can be written as in the following comment
    /*
    arr := strings.SplitN(secretKey, ",", 2)
    if len(arr) != 2 {
        panic("Configuration error!")
    }
    s.secretKey = arr[0]   // secret key 
    s.passWord = arr[1]    // XXX_PassWord
    */
    s.apiBase = "https://www.bitgo.cn"
    s.timeout = 20 * time.Second
    s.timeLocation = time.FixedZone("Asia/Shanghai", 8*60*60)

    return s
}

요청 데이터의 매개 변수를 다시 분석하면 도커는 요청 데이터를 다음과 같이 전송합니다.

"secret_key" : "XXX",

이 플러그인은 이러한 종류의 정보를 포함하는 입력 데이터를 수신하고 XXX_PassWord을 코마 분리자에 따라 분리하여 추가로 전달된 데이터를 얻습니다.

전체 사용자 지정 프로토콜 플러그인의 예main기능:Go언어 설명:

func main(){
    var addr = flag.String("b", "127.0.0.1:6666", "bing addr")   // Set command line parameters, default value description, port setting 6666
    flag.Parse()                                                 // Parsing the command line
    if *addr == "" {
        flag.Usage()                                             // Display the command line description
        return 
    }
    basePath := "/DigitalAsset"
    log.Println("Running ", fmt.Sprintf("http://%s%s", *addr, basePath), "...")   // Print listening port information
    http.HandleFunc(basePath, OnPost)
    http.ListenAndServe(*addr, nil)
}

2. 반응 기능

Custom Protocol Plugin 프로그램은 입력된 포트를 지속적으로 듣습니다.request요청이 수신되면 응답 함수를 호출하여 실행하고 요청 데이터에서 매개 변수를 분석합니다. 도커가 전송하는 요청 데이터는:

/* JSON structure of request, FMZ Quant call GetTicker, docker sent to the custom protocol plugin case example (the value of params may be different for each API call, here the method is ticker):
{
    "access_key" : "XXX",               // `json:"access_key"`
    "secret_key" : "XXX",               // `json:"secret_key"`
    "nonce" : "1502345965295519602",    // `json:"nonce"`
    "method" : "ticker",                // `json:"method"`
    "params" : {                        // `json:"params"`
                   "symbol" : "btc",
                   ...
               },                       // The parameters are slightly different for each request. That is, different FMZ Quant APIs are called in the strategy with different parameters, which are described in the following sections for each API.
}
*/

그래서,Method현장에서requestJSON에 의해 얻은 구조 요청 deserializing 신체 데이터 유니버설 프로토콜 플러그인 프로그램에서 수신, 우리는 사용할 수 있습니다switch도커에서 호출되는 다른 FMZ Quant API를 분류하고 처리하기 위한 명령어 (즉, 어떤 FMZ Quant를 식별하는 것)API도커에서 실행되는 전략이 호출됩니다):

예를 들어Go언어:

switch request.Method {    // M of request.Method is capitalized here, the body of the request received by the custom protocol program for the JSON data, in the Go language, the anti-JSON serialization (Unmarshal) is a structure, the first letter of the field must be capitalized
  case "accounts" :        // When the exchange.GetAccount() function is called in the bot strategy on the docker, the docker sends in a request where the Body carries data with a method attribute value of accounts
      data, err = e.GetAccount(symbol)
  case "ticker" :
      data, err = e.GetTicker(symbol)
  case "depth" :
      data, err = e.GetDepth(symbol)
  case "trades" :
      data, err = e.GetTrades(symbol)
  case "trade" :
  ...
  default:
      ...

이러한 브랜치를 실행한 후, 반환된 데이터는 사용자 지정 프로토콜 플러그인 프로그램이 도커의 요청에 응답하기 위해 사용할 구조에 기록되어야 합니다.

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
  }()

3. API 호출의 종류

대략 두 가지로 나다:

  1. 서명을 필요로 하지 않는 공개 인터페이스, 예를 들어:

GetTicker()

GetDepth()

GetTrades()

GetRecords(period)

  1. 서명해야 하는 사용자 인터페이스, 예를 들어:

Buy, Sell

GetOrder(id)

GetOrders()

GetAccount()

CancelOrder(id)... 서명 방법은 교환에서 교환에 따라 다를 수 있으며 필요에 따라 구체적으로 작성되어야합니다.

4. 다양한 FMZ 퀀트 API 인터페이스를 호출 할 때 사용자 지정 프로토콜 플러그인과 도커 사이의 상호 작용에 대한 데이터 형식:

일부 FMZ Quant API 인터페이스, 예를 들어GetName(), GetLabel(), 등에 요청을 보내지 않습니다사용자 지정 프로토콜 플러그인전화가 오면 전화할 때exchange.GetName(), 유니버설 플러그인에서 구성된 교환은 Exchange을 반환합니다.

    1. GetTicker: 현재 틱어 데이터를 얻기 위해 사용됩니다.

methodrequest의 발송도커듣기 반응 기능은ticker.

도커가 파라미터를 전송합니다.request.Params.symbol, 로봇의 페이지에 설정된 화폐를 기반으로 도커에 의해 전송됩니다.

도커가 사용자 지정 프로토콜 플러그인을 요청할 때 요청 본체에서 전송되는 데이터 형식 (JSON)

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "ticker",
    "params" :     {"symbol" : "ETH_BTC"},     // Take the ETH_BTC trading pair for example
}

도커에 전송된 최종 반환 값의 구조: (즉, 교환 인터페이스가 공통 프로토콜 플러그인에서 요청된 후에 도커에 반환된 데이터의 형식)

JSON 구조

{
    "data": {
        "time": 1500793319499,  // Millisecond timestamp, integer
        "buy": 1000,            // floating-point type as follows
        "sell": 1001,
        "last": 1005,
        "high": 1100,
        "low": 980,
        "vol": 523,
    }
}
    1. GetRecords: 교환이 제공하는 K-라인 데이터를 검색하는 데 사용됩니다. (도커가 요청한 매개 변수에 기초)

methodrequest도커가 듣기 응답 기능에 보내는records.

도커가 매개 변수를 전송합니다.request.Params.period, 이 값은exchange.GetRecords실제 기능request.Params.period예를 들어, 일간 기간은60*24, 즉1440. request.Params.symbol정해진 화폐를 기반으로 도커에 의해 전송됩니다.

도커가 사용자 지정 프로토콜 플러그인을 요청할 때 요청 본체에서 전송되는 데이터 형식.

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "records",
    "params" :     {"symbol" : "ETH_BTC", "period" : "1440"},     // Example of an ETH_BTC pair with a daily K-period
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

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

언어 테스트 데이터:

ret_records = []interface{}{
    [6]interface{}{1500793319, 1.1, 2.2, 3.3, 4.4, 5.5}, 
    [6]interface{}{1500793259, 1.01, 2.02, 3.03, 4.04, 5.05}
}

FMZ 퀀트 플랫폼Log디스플레이records데이터:

[
    {"Time":1500793319000,"Open":1.1,"High":2.2,"Low":3.3,"Close":4.4,"Volume":5.5},
    {"Time":1500793259000,"Open":1.01,"High":2.02,"Low":3.03,"Close":4.04,"Volume":5.05}
]

참고: 1. 2차원 배열의 첫 번째 요소는 유형입니다int도커는 위에서 언급했듯이 자동으로 시간표를 1000으로 곱합니다.

    1. GetDepth: 심도 정보를 검색합니다. (오더북, ask1, ask2...bid1, bid2...)

methodrequest도커가 듣기 응답 기능에 보내는depth.

도커가 파라미터를 전송합니다.request.Params.symbol, 그것은 전략에 설정된 통화에 기초하여 도커에 의해 전송됩니다.

도커가 사용자 지정 프로토콜 플러그인을 요청할 때 요청 본체에 전송되는 데이터 형식

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "depth",
    "params" :     {"symbol" : "ETH_BTC"},     // Take the ETH_BTC trading pair for example
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{
    "data" : {
        "time" : 1500793319499,
        "asks" : [ [1000, 0.5], [1001, 0.23], [1004, 2.1], ... ],
        "bids" : [ [999, 0.25], [998, 0.8], [995, 1.4], ... ],
    }
}
    1. GetTrades: 특정 기간 내에 전체 거래소의 거래 기록을 얻습니다 (자기 거래 제외)

methodrequest도커가 듣기 응답 기능에 보내는 것은:trades.

도커에 의해 전송되는 매개 변수:request.Params.symbol예를 들어, 거래 통화입니다.btc, 전략 설정에 따라 도커에 의해 전송됩니다.

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "trades",
    "params" :     {"symbol" : "ETH_BTC"},     // Take the ETH_BTC trading pair for example
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{ 
    "data": [
        {
            "id": 12232153,
            "time" : 1529919412968,
            "price": 1000,
            "amount": 0.5,
            "type": "buy",             // "buy"、"sell"
        },{
            "id": 12545664,
            "time" : 1529919412900,
            "price": 1001,
            "amount": 1,
            "type": "sell",
        },{
            ...
        }
    ]
}
    1. GetAccount: 계정 자산 정보를 얻습니다.

methodrequest도커가 듣기 응답 기능에 보내는 것은:accounts.

도커에 의해 전송된 매개 변수: (고기: 일반적으로, 그것은 계좌의 모든 자산을 얻을 것입니다! 교환 인터페이스를 참조하여 개별 자산을 얻으려면 전체 자산 정보를 얻으려면)

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "accounts",
    "params" :     {},                         
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{
    "data": [
        {"currency": "btc", "free": 1.2, "frozen": 0.1},
        {"currency": "ltc", "free": 25, "frozen": 2.1},
        {"currency": "ltc", "free": 25, "frozen": 2.1},
        ...
    ],
    "raw" : {...}             // It is possible to write the raw message (response) returned by the exchange when the plugin accesses the exchange
}
    1. 구매, 판매: 거래를 위한 주문 (시장 주문 또는 제한 주문) 을 배치합니다.

methodrequest도커가 듣기 응답 기능에 보내는 것은:trade.

도커가 보낸 매개 변수:request.Params.type: 도커에 의해 전송exchange.Buy또는exchange.Sell, request.Params.price: 첫 번째 매개 변수API전략에서 호출된 함수,request.Params.amount: 두 번째 파라미터API전략에서 호출된 함수,request.Params.symbol: 정해진 거래 통화를 기반으로 도커에 의해 전송됩니다.

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "trade",
    "params" :     {
                       "symbol" : "ETH_BTC", 
                       "type" : "buy", 
                       "price" : "1000", 
                       "amount" : "1"
                   },                          // Example of an ETH_BTC trading pair, "type": "buy" buy request, price 1000, quantity 1
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{
    "data": {
        "id": 125456,      // Order id returned after placing an order
                           // If the order id is in the form of a string like "asdf346sfasf532"
                           // Here the id can also be a string type
    }
}
    1. GetOrder: 주문 ID에 따라 특정 주문에 대한 정보를 얻으십시오

methodrequest도커가 듣기 응답 기능에 보내는 것은:order.

도커가 보낸 매개 변수:request.Params.id, request.Params.symbol.

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "order",
    "params" :     {"symbol" : "ETH_BTC", "id" : "XXX"},     // Take the ETH_BTC trading pair and order ID XXX as an example. Please note that some exchanges use numerical order IDs, such as 123456, while others use string order IDs, such as poimd55sdfheqxv. The specific format of the order ID depends on the exchange.
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{ 
    "data": {
        "id": 2565244,
        "amount": 0.15,
        "price": 1002,
        "status": "open",    // "open": pending, "closed": closed, "canceled": canceled
        "deal_amount": 0,
        "type": "buy",       // "buy"、"sell"
        "avg_price": 0,      // If not provided by the exchange, it can be assigned a value of 0 during processing
    }
}
    1. GetOrders: 채우지 않은 모든 주문에 대한 정보를 얻으십시오

methodrequest도커가 듣기 응답 기능에 보내는orders.

도커가 보낸 매개 변수:request.Params.symbol

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "orders",
    "params" :     {"symbol" : "ETH_BTC"},     // Take the ETH_BTC trading pair for example
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{
    "data": [{
        "id": 542156,
        "amount": 0.25,
        "price": 1005,
        "deal_amount": 0,
        "type": "buy",      // "buy"、"sell"
        "status": "open",   // "open"
    },{
        ...
    }]
}
    1. CancelOrder: 지정된 주문 ID를 가진 주문을 취소

methodrequest도커가 듣기 응답 기능에 보내는cancel.

도커가 보낸 매개 변수:request.Params.id(string type, 전략에 의해 호출되는 API 함수의 첫 번째 매개 변수)request.Params.symbol(예를 들어, btc, 전략에 의해 설정된 통화에 기초한 도커에 의해 전송)

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "cancel",
    "params" :     {"symbol" : "ETH_BTC", "id" : "XXX"},     // Take an ETH_BTC trading pair with an id of "XXX" (same as the GetOrder function's parameter id) for example
}

도커에 전송된 최종 반환 값의 구조:

JSON 구조

{
    "data": true,        // true or false
}

methodrequest도커에서 듣기 응답 기능에 전송_api_.

사용자 지정 프로토콜 플러그인을 요청할 때 도커에 의해 운반되는 데이터 형식은 다음과 같습니다

{
    "access_key" : "access_key",
    "secret_key" : "secret_key",
    "nonce" :      "1500793319499",            // millisecond timestamp
    "method" :     "__api_XXX",                // XXX is the API interface for the specific exchange (base address not included)
    "params" :     {"borrow_id" : "123", "symbol" : "cny"},      // Specifically, the parameters passed into the IO function
}

도커에 전송된 최종 반환 값의 구조:

{
    "data": {...}       // The return value of a specific interface call
}

예를 들어, 전략 초청:

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

플러그인 테스트 코드 (go 언어):

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

플러그인 명령 줄: 2017/08/31 10:19:59 달리기http://127.0.0.1:6666/DigitalAsset

플러그인 명령 줄 인쇄: request.Method, request.Param도커가 보낸 요청 본체에서, 요청에 있는 분석된 데이터는 다음과 같습니다.request.Method__api_cancel_borrow request.Params{"borrow_id" : "123", "symbol" : "cny"}

이러한 처리 방식을 사용자 정의 할 수 있습니다exchange.IO교환에 직접 액세스 할 수 있는 호출API.

# Attention:
# When calling exchange.IO("api", "POST", "/api/v1/getAccount", "symbol=BTC_USDT"), 
# If the second parameter is not POST but: exchange.IO("api", "GET", "/api/v1/getAccount", "symbol=BTC_USDT")
# is the GET method, which is then stored in the header Http-Method in the http request accepted by the custom protocol plugin.
# So you need to refer to the following sample code for the custom protocol handling IO function implementation:
// tapiCall function definition
func (p *iStocksExchange) tapiCall(method string, params map[string]string, httpType string) (js *Json, err error) {
    ...
}

// In the OnPost function
if strings.HasPrefix(request.Method, "__api_") {
    var js *Json
    js, err = e.tapiCall(request.Method[6:], request.Params, r.Header.Get("Http-Method"))
    ...
}
  • 교환 지원.GetRawJSON:

기본 시스템은 자동으로 호출을 처리exchange.GetRawJSON, 그래서 플러그인에서 구현할 필요가 없습니다.

  • 교환을 지원합니다.

기본 시스템은 자동으로 호출을 처리exchange.Go, 그래서 플러그인에서 처리 할 필요가 없습니다.

var beginTime = new Date().getTime()
var ret = exchange.Go("GetDepth")
var endTime = new Date().getTime()
Log(endTime - beginTime, "#FF0000")

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

img

img

# 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.
  • 미래에 대한 기능 지원:

플러그인 프로그램에서 퓨처스 함수를 위해 특정 처리 기능을 구현해야합니다. 예를 들어 레버리지, 계약 코드 및 주문 방향을 설정합니다. 이러한 정보를 기록하기 위해 로컬 변수를 설정할 수 있습니다. 포지션을 검색하려면 원자 데이터를 얻고 FMZ 플랫폼에서 정의된 위치 구조로 처리하고 반환하기 위해 교환 API에 액세스해야합니다.

다음 함수들이 전략에서 호출될 때,Rpc플러그인 프로그램에서 수신된 요청은 다른 인터페이스에서 약간 다릅니다.RpcRequest사용자 지정 프로토콜 플러그인 프로그램에서. 주요 차이점은 파라암의 값이 복합 구조라는 것입니다.

SetContractType 계약 코드를 설정해

{"access_key":"123","method":"io","nonce":1623307269528738000,"params":{"args":["quarter"],"code":2},"secret_key":"123"}

방향 설정 선물 주문을 하는 방향을 설정합니다.

{"access_key":"123","method":"io","nonce":1623308734966484000,"params":{"args":["closesell"],"code":1},"secret_key":"123"}

MarginLevel 설정 미래에 대한 레버리지를 설정합니다.

{"access_key":"123","method":"io","nonce":1623308734966939000,"params":{"args":[12],"code":0},"secret_key":"123"}

포지션을 얻으십시오 선물 포지션을 잡아요 언제?exchange.GetPosition()그 이름은:

{"access_key":"123","method":"io","nonce":1623308734967442000,"params":{"args":[],"code":3},"secret_key":"123"}

언제?exchange.GetPosition("swap")그 이름은:

{"access_key":"123","method":"io","nonce":1623308734967442000,"params":{"args":["swap"],"code":3},"secret_key":"123"}
  • 전체 Go 언어 사용자 지정 프로토콜 플러그인 예 (Bitgo 교환에 액세스)
/*
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)
}

더 많은