Các nhà phát minh định lượng giao dịch nền tảng giao dịch thông thường truy cập tùy chỉnh giao dịch

Tác giả:Giấc mơ nhỏ, Tạo: 2017-08-24 16:29:56, Cập nhật: 2021-06-10 15:33:02

Tài liệu sử dụng GPL

Bạn có thể truy cập bất kỳ dịch vụ nào với giao thức này.APICác sàn giao dịch, không giới hạn các giao thức API cụ thể, cho dùrestwebsocketfix... có thể truy cập và sử dụng. Một ví dụ về giao thức Python:https://www.fmz.com/strategy/101399

  • 1, chạy plugin giao thức chung, thiết lập cổng

    Địa chỉ và thiết lập cổng của plugin giao thức chung của plugin được viết tốt. Ví dụ:http://127.0.0.1:6666/DigitalAssetCó thể là:http://127.0.0.1:6666/exchange

    Tại sao lại thiết lập những thứ này?IPĐường điCó gì không? Bởi vìNhà phát minh định lượngTrangTrung tâm điều khiểnNhảyThêm sàn giao dịchTrang, chọn hộp giao thức chung, hiển thị ngoại trừAPI-KEYCó một thanh địa chỉ dịch vụ, là địa chỉ dịch vụ cho người quản lý biết khi nào là lúc truy cập vào IP và cổng (những người quản lý và plugin giao thức chung có thể không chạy trên cùng một thiết bị).http://127.0.0.1:6666/DigitalAssetDigitalAssetĐây chỉ là một ví dụ.

    Trong trang của Inventor Quantitative Add Exchange, bạn có thể thấy: Thông thường, thông tin cấu hình tài khoản chỉ cần cấu hìnhaccess keysecret keyTuy nhiên, một số giao dịch API yêu cầu truyền mật khẩu giao dịch (ví dụ như giao dịch giao dịch dưới đây của một số giao dịch), vì trang giao thức chung không có điều khiển dư thừa để viết thông tin này, khi gặp phải API của các giao dịch như vậy, chúng ta có thể viết thông tin cấu hình cần truyền dư thừa vàosecret keyNếu thông tin không nhạy cảm, bạn có thể viết ởaccess key), và sau đó làm một chuỗi trong plugin giao thức chungsplitTrong khi đó, các ứng dụng khác cũng có thể được sử dụng để phân tách dữ liệu, ví dụ như hình trên.

    img

    Sau đó, bạn có thể xử lý nó trong plugin và lấy nó.XXX_PassWord‖ Ví dụ, trong ví dụ cuối cùng của bài viết này, newBitgo:

    func newBitgo(accessKey, secretKey string) *iBitgo {
        s := new(iBitgo)
        s.accessKey = accessKey
        s.secretKey = secretKey
        // 在此可以分离secretKey中的额外配置信息,可以写成如下注释中的内容
        /*
        arr := strings.SplitN(secretKey, ",", 2)
        if len(arr) != 2 {
            panic("配置错误!")
        }
        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
    }
    

    img

    Plugin giao thức chungmainVí dụ về hàm:GoNhững người khác cũng có thể làm điều đó.

    func main(){
        var addr = flag.String("b", "127.0.0.1:6666", "bing addr")   // 设置命令行参数,默认值描述,端口设置6666
        flag.Parse()                                                 // 解析命令行
        if *addr == "" {
            flag.Usage()                                             // 显示命令行描述
            return 
        }
        basePath := "/DigitalAsset"
        log.Println("Running ", fmt.Sprintf("http://%s%s", *addr, basePath), "...")   // 打印监听端口信息
        http.HandleFunc(basePath, OnPost)
        http.ListenAndServe(*addr, nil)
    }
    
  • 2, hàm đáp ứng

    requestMột khi có một yêu cầu, một chức năng trả lời sẽ được gọi để thực hiện phản hồi và phân tích các tham số trong dữ liệu yêu cầu, dữ liệu yêu cầu được gửi bởi người quản lý là:

    /* request的JSON结构,发明者量化调用GetTicker,托管者发送给通用协议插件情况举例(各个API调用时,params的值可能不一样,此处method为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",
                       ...
                   },                       // 各个请求参数略有区别。即在策略中调用不同的 发明者量化 API会有不同的参数, 在下文各个API 有介绍说明。
    }
    */
    

    Vì vậy, dựa trên các yêu cầu nhận được trong các chương trình plugin giao thức chung cơ thể dữ liệu được nhận được sau khi tự sắp xếp ngược JSONrequestTrongMethodChúng ta có thể sử dụngswitchĐể phân loại xử lý các API định lượng của các nhà phát minh khác nhau (có nghĩa là xác định các cuộc gọi chính sách đang chạy trên chủ nhà là nhà phát minh nào định lượng)API(Tạm dịch:

    GoVí dụ ngôn ngữ:

    switch request.Method {    // 此处request.Method的M为大写,通用协议程序接收到的请求主体中为JSON数据,在Go语言中反JSON序列化(Unmarshal)为结构体,字段首字母必须大写
      case "accounts" :        // 当托管者上的机器人策略中调用了exchange.GetAccount()函数,托管者会发送来请求,其中Body携带的数据中method属性值为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:
          ...
    

    Các nhánh này viết dữ liệu được trả về sau khi thực hiện vào cấu trúc mà chương trình plugin giao thức chung sẽ trả lời, đáp ứng yêu cầu của người quản lý.

    Ví dụ về ngôn ngữ Go:

    defer func(){                                // 处理收尾工作 
          if e := recover(); e != nil {          // recover()函数用于捕获panic,e != nil即有错误发生
              if ee, ok := e.(error); ok {       // 类型推导,推导成功把ee.Error()赋值给e
                  e = ee.Error()                 // 调用Error方法获取返回的错误字符串
              }
              ret = map[string]string{"error": fmt.Sprintf("%v", e)}
          }
          b, _ := json.Marshal(ret)              // 把本次调用获取的结果ret编码,赋值给b,写入响应指针
          w.Write(b)
          //fmt.Println("after w.Write the b is", string(b))     // 测试
      }()
    
  • 3, Các loại gọi API

    Có hai loại: Một trong những điều quan trọng nhất là bạn có thể sử dụng các giao diện công cộng mà không cần phải ký tên như:

    GetTicker()

    GetDepth()

    GetTrades()

    GetRecords(period)

    2, Các giao diện người dùng cần phải ký tên, ví dụ:

    BuySell

    GetOrder(id)

    GetOrders()

    GetAccount()

    CancelOrder(id)... Các sàn giao dịch khác nhau có thể sử dụng các hình thức ký hiệu khác nhau và cần phải viết cụ thể theo nhu cầu.

  • 4, Khi các nhà phát minh định lượng các cuộc gọi APIPlugin giao thức chungNgười quản lýCác định dạng dữ liệu tương tác:

    Một số nhà phát minh đã định lượng các API như:GetName()GetLabel()Chức năng tương đương, không được gọi theoPlugin giao thức chungGửi yêu cầu.exchange.GetName()Các giao dịch có cấu hình plugin chung sẽ trả về "Exchange" khi gọi.

    • 1, GetTicker:Các công ty này được sử dụng để có được dữ liệu hiện tại.

      Người quản lýVà chúng tôi sẽ gửi cho các chức năng phản hồi nghe lénrequestTrongmethodĐể:ticker

      Trình quản lý gửi tham số:request.Params.symbol: Được gửi bởi người quản lý theo tùy chọn của trang robot.

      Định dạng dữ liệu mà chủ thể mang theo (JSON) khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "ticker",
          "params" :     {"symbol" : "ETH_BTC"},     // 以ETH_BTC交易对举例
      }
      

      Kết quả cuối cùng được gửi đến người quản lý: (tức là định dạng dữ liệu được trả lại cho người quản lý sau khi plugin giao thức chung yêu cầu giao dịch giao diện)

      Cấu trúc JSON

      {
          "data": {
              "time": 1500793319499,  // 毫秒时间戳,整型
              "buy": 1000,            // 以下浮点型
              "sell": 1001,
              "last": 1005,
              "high": 1100,
              "low": 980,
              "vol": 523,
          }
      }
      
    • 2、GetRecords:Sử dụng để lấy dữ liệu K-line được cung cấp bởi sàn giao dịch.

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:records

      Trình quản lý gửi tham số:request.Params.periodGiá trị liên kết:exchange.GetRecordsCác tham số đầu tiên của hàm, thực tếrequest.Params.periodCác chu kỳ được biểu thị bằng số phút, ví dụ như chu kỳ ngày60*24Có nghĩa là1440request.Params.symbolNgười quản lý gửi theo loại tiền tệ đã thiết lập.

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "records",
          "params" :     {"symbol" : "ETH_BTC", "period" : "1440"},     // 以ETH_BTC交易对,K线周期为日线举例
      }
      

      Sau đó, bạn sẽ được gửi đến người quản lý và trả về các cấu trúc giá trị:

      Cấu trúc 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],
                  ...
          ]
      }
      

      Dữ liệu kiểm tra ngôn ngữ Go:

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

      Nền tảng định lượng của nhà phát minhLogHiển thịrecordsDữ liệu:

      [
          {"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}
      ]
      

      Lưu ý: phần tử đầu tiên trong một mảng hai chiều làintLoại, đại diện cho thời gian 2, quản trị viên sẽ tự động cho thời gian nhân 1000, hoặc nhiều hơn.

    • 3、GetDepth:Nhận thông tin sâu hơn về sàn giao dịch (đơn đặt hàng thấp, bán một, bán hai... mua một, mua hai...)

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:depth

      Trình quản lý gửi tham số:request.Params.symbol: Được gửi bởi người quản lý theo tiền tệ được thiết lập theo chính sách

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "depth",
          "params" :     {"symbol" : "ETH_BTC"},     // 以ETH_BTC交易对举例
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      Cấu trúc JSON

      {
          "data" : {
              "time" : 1500793319499,
              "asks" : [ [1000, 0.5], [1001, 0.23], [1004, 2.1], ... ],
              "bids" : [ [999, 0.25], [998, 0.8], [995, 1.4], ... ],
          }
      }
      
    • 4、GetTrades:Nhận hồ sơ giao dịch trong một khoảng thời gian nhất định cho toàn bộ sàn giao dịch (không phải là hồ sơ giao dịch của riêng bạn)

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:trades

      Trình quản lý gửi tham số:request.Params.symbolGiá trị được giao dịch bằng tiền tệ như:btcTrong khi đó, một số trang web khác trên mạng xã hội của bạn có thể sử dụng các tùy chọn này.

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "trades",
          "params" :     {"symbol" : "ETH_BTC"},     // 以ETH_BTC交易对举例
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      Cấu trúc 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",
              },{
                  ...
              }
          ]
      }
      
    • 5、GetAccount:Truy cập tài sản tài khoản

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:accounts

      Người quản lý gửi tham số: ((lưu ý! nói chung là truy cập tất cả tài sản tài khoản!, cụ thể xem giao diện sàn giao dịch, truy cập riêng lẻ hoặc truy cập thông tin tổng tài sản)

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "accounts",
          "params" :     {},                         
      }
      

      Sau đó, bạn sẽ được gửi đến người quản lý và trả về các cấu trúc giá trị:

      Cấu trúc 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" : {...}             // 可以写入插件访问交易所时,交易所返回的原始信息(response)
      }
      
    • 6、Buy、Sell:Gửi đơn đặt hàng, đặt hàng giao dịch.

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:trade

      Trình quản lý gửi tham số:request.Params.typeNgười quản lý: theo yêu cầuexchange.BuyHoặcexchange.SellNhững người bị ảnh hưởng là:request.Params.priceĐịa chỉ:APICác tham số đầu tiên của hàmrequest.Params.amountĐịa chỉ:APICác tham số thứ hai của hàmrequest.Params.symbolNgười quản lý gửi theo loại tiền tệ đã thiết lập.

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "trade",
          "params" :     {
                             "symbol" : "ETH_BTC", 
                             "type" : "buy", 
                             "price" : "1000", 
                             "amount" : "1"
                         },                          // 以ETH_BTC交易对,"type":"buy"买请求,价格1000,数量1举例
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      Cấu trúc JSON

      {
          "data": {
              "id": 125456,      // 下单后返回的订单id
                                 // 如果订单id是"asdf346sfasf532"这样的字符串形式
                                 // 此处id也可以是字符串类型
          }
      }
      
    • 7、GetOrder:Nhận thông tin đặt hàng theo số đơn đặt hàng

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:order

      Trình quản lý gửi tham số:request.Params.idrequest.Params.symbol

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "order",
          "params" :     {"symbol" : "ETH_BTC", "id" : "XXX"},     // 以ETH_BTC交易对,订单id为XXX举例,注意有些交易所的订单号是数字形式的订单ID,如123456,有些交易所的订单号是字符串形式的ID,如poimd55sdfheqxv,具体看交易所的订单ID格式
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      Cấu trúc JSON

      { 
          "data": {
              "id": 2565244,
              "amount": 0.15,
              "price": 1002,
              "status": "open",    // "open":挂起状态、"closed":完成关闭状态、"cancelled":已取消
              "deal_amount": 0,
              "type": "buy",       // "buy"、"sell"
              "avg_price": 0,      // 如果交易所没有提供,在处理时可以赋值为0
          }
      }
      
    • 8、GetOrders:Nhận thông tin về tất cả các đơn đặt hàng chưa hoàn thành

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrong đó, method là:orders

      Trình quản lý gửi tham số:request.Params.symbol

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "orders",
          "params" :     {"symbol" : "ETH_BTC"},     // 以ETH_BTC交易对举例
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      Cấu trúc JSON

      {
          "data": [{
              "id": 542156,
              "amount": 0.25,
              "price": 1005,
              "deal_amount": 0,
              "type": "buy",      // "buy"、"sell"
              "status": "open",   // "open"
          },{
              ...
          }]
      }
      
    • 9, Hoãn đơn đặt hàng:Hoại bỏ đơn đặt hàng với số đơn đặt hàng

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể:cancel

      Trình quản lý gửi tham số:request.Params.id: loại chuỗi, tham số đầu tiên của hàm API được gọi theo chính sách,request.Params.symbol:btc ((ví dụ) được gửi bởi người quản lý tùy theo tiền tệ được thiết lập theo chính sách

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "cancel",
          "params" :     {"symbol" : "ETH_BTC", "id" : "XXX"},     // 以ETH_BTC交易对,id为"XXX"(同GetOrder函数的参数id一样),举例
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      Cấu trúc JSON

      {
          "data": true,        // true or false
      }
      
    • 10 IO:调用发明者量化平台的exchange.IO函数

      Các chức năng được gửi bởi người quản lý đến các chức năng phản hồi ngherequestTrongmethodĐể__api_Tên của phương thức bắt đầu với.

      Định dạng dữ liệu mà chủ thể mang theo khi người quản lý yêu cầu plugin giao thức chung

      {
          "access_key" : "access_key",
          "secret_key" : "secret_key",
          "nonce" :      "1500793319499",            // 毫秒时间戳
          "method" :     "__api_XXX",                // XXX为具体交易所的API接口(不包含基地址)
          "params" :     {"borrow_id" : "123", "symbol" : "cny"},      // 具体是传入IO函数的参数
      }
      

      Sau đó, bạn sẽ được gửi một cấu trúc giá trị trả về cho người quản lý:

      {
          "data": {...}       // 具体的接口调用的返回值
      }
      

      Ví dụ, các cuộc gọi chiến lược:

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

      Mã thử nghiệm trong plugin (tiếng go):

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

      Dòng lệnh plugin: 2017/08/31 10:19:59 Runninghttp://127.0.0.1:6666/DigitalAsset

      Request.Method,request.Params được in trên dòng lệnh pluginTrong các yêu cầu được gửi bởi người quản lý sau khi phân tích dữ liệu trong Body:request.MethodĐể:__api_cancel_borrowTrong các yêu cầu được gửi bởi người quản lý sau khi phân tích dữ liệu trong Body:request.ParamsĐể:{"borrow_id" : "123", "symbol" : "cny"}

      Có thể tùy chỉnh xử lý các giao dịch truy cập trực tiếp nàyAPIcủaexchange.IOĐịa chỉ:

      # 注意:
      # 在调用exchange.IO("api", "POST", "/api/v1/getAccount", "symbol=BTC_USDT")时,
      # 如果第二个参数不是POST,而是:exchange.IO("api", "GET", "/api/v1/getAccount", "symbol=BTC_USDT")
      # 是GET方法,这时在通用协议插件接受到的http请求中头部Http-Method中储存的才是GET,
      # 所以在通用协议处理IO函数实现时,需要参考以下范例代码:
      // tapiCall函数定义
      func (p *iStocksExchange) tapiCall(method string, params map[string]string, httpType string) (js *Json, err error) {
          ...
      }
      
      // 在OnPost函数中
      if strings.HasPrefix(request.Method, "__api_") {
          var js *Json
          js, err = e.tapiCall(request.Method[6:], request.Params, r.Header.Get("Http-Method"))
          ...
      }
      
    • 对于exchange.GetRawJSON的支持

      Xử lý tự động dướiexchange.GetRawJSONCác ứng dụng khác cũng có thể được sử dụng trong các ứng dụng khác.

    • 对于exchange.Go的支持

      Xử lý tự động dướiexchange.GoCác ứng dụng khác cũng được sử dụng trong ứng dụng này.

      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

      # 注意:使用exchange.Go在wait的时候如果指定了超时时间, 
      #      一定要确保获取到最终的数据,这样申请的并发线程才能回收。
      
    • Hỗ trợ cho các hàm tương lai

      Cần thực hiện các xử lý cụ thể trong plugin, chẳng hạn như thiết lập đòn bẩy, mã hợp đồng, hướng đặt hàng, có thể thiết lập một bản ghi biến số cục bộ, truy cập giữ cần truy cập vào giao diện giao dịch để lấy dữ liệu nguyên liệu và xử lý cấu trúc giữ được xác định trên nền tảng FMZ và trở lại. Khi bạn gọi các hàm sau trong chính sách, plugin sẽ nhận đượcRpcĐịnh dạng yêu cầu hơi khác so với các giao diện khác, cần lưu ý trong plugin giao thức chungRpcRequestCác định dạng, chủ yếu được phân biệt là các giá trị của param là một cấu trúc phức tạp.

      • SetContractType
        Đặt mã hợp đồng.

        {"access_key":"123","method":"io","nonce":1623307269528738000,"params":{"args":["quarter"],"code":2},"secret_key":"123"}
        
      • SetDirection Đặt các hợp đồng tương lai theo hướng đơn.

        {"access_key":"123","method":"io","nonce":1623308734966484000,"params":{"args":["closesell"],"code":1},"secret_key":"123"}
        
      • SetMarginLevel Đặt đòn bẩy tương lai.

        {"access_key":"123","method":"io","nonce":1623308734966939000,"params":{"args":[12],"code":0},"secret_key":"123"}
        
      • GetPosition Có được cổ phiếu tương lai. Khiexchange.GetPosition()Khi gọi:

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

        Khiexchange.GetPosition("swap")Khi gọi:

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

  • Mô hình ngôn ngữ Go đầy đủ của plugin giao thức chung (đưa vào sàn giao dịch Bitcoin)

/*
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 // 按值排序
    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 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 {        // 结构体里的字段首字母必须大写,否则无法正常解析,结构体有导出和未导出,大写字母开头为导出。
                                // 在Unmarshal的时候会  根据 json 匹配查找该结构体的tag, 所以此处需要修饰符
    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)
}


Thêm nữa

Cỏ nhỏVí dụ về giao dịch tùy chỉnh theo giao thức Python Access. https://www.fmz.com/strategy/101399

Cỏ nhỏVí dụ về sàn giao dịch tùy chỉnh theo giao thức Python FMZ http://www.fmz.com/strategy/101399

liuwei9090Các sàn giao dịch bitmex đã truy cập nền tảng, nhưng không cần phải sử dụng _C ((exchange.GetTicker).Buy hoặc _C ((exchange.GetDepth).

Mỗi ngày một mô hình mỏngHaha, viết một plugin giao thức chung có thể tự kết nối hoàn toàn với API giao dịch, hoặc không nên sử dụng chương trình của riêng bạn trực tiếp, có phải là một chút hơn nữa.

zhchy751Có những ví dụ sẵn có về truy cập giao thức chung, robot hoàn chỉnh / chiến lược / cấu hình sàn giao dịch, v.v.?

dhyhuCó phải mã trên là plugin giao thức chung không? Plugin này không giống như mô hình chính sách mà tôi có thể gọi trực tiếp? Tôi sẽ sử dụng py post hoặc get để gửi và nhận dữ liệu và phân tích.

chalieNếu bạn có Python, bạn sẽ làm được.

sơnCó phải việc sử dụng plugin phổ biến có nghĩa là bạn phải tự viết nhiều chức năng, ví dụ như GetAccout (?).

alanyaobotVS không phải là nhiều ví dụ về js? Tại sao không có js? Tại sao lại cho GO?

nxtplayer6 lên máy bay, nhưng không làm gì cả.

Cô dâu cũng vậy.Độc ác

Giấc mơ nhỏBITMEX là một sàn giao dịch tương lai. Bạn cần phải cài đặt trước các hợp đồng mà bạn muốn vận hành hoặc truy cập, xem tài liệu API của exchange.SetContractType.

Mỗi ngày một mô hình mỏngVà cũng đúng.

Giấc mơ nhỏNhưng nếu chiến lược là đa nền tảng thì sao? Bạn không thể từ bỏ một khu rừng vì một cái cây!

Giấc mơ nhỏNó có thể được biên dịch thành một tập tin thực thi và chạy cùng với một trình quản lý. Không có vấn đề gì về danh mục nào. Một ví dụ về python hiện tại chưa có, tôi đã viết một ví dụ.

dhyhuPlugin làm thế nào để nó hoạt động, ví dụ như viết theo định dạng py, đặt trực tiếp trong thư mục exe của người quản lý?

Giấc mơ nhỏKhông cần xử lý đặc biệt trong chính sách, đối với giao diện API BotVS, các đối tượng trao đổi này đều giống nhau (dù BotVS đã được truy cập hoặc bạn đã viết bản plugin GPL của riêng bạn), chính sách không cần phải thực hiện bất kỳ điều chỉnh nào. Xem trên tài liệu: 1, chạy plugin giao thức chung, thiết lập cổng Các bạn có thể xem nội dung của bài viết này.

dhyhuSau khi tự viết, có giống như mô hình chính sách, để liên kết plugin tương ứng với sàn giao dịch sẽ gọi hàm ở đây, với các sàn giao dịch khác sẽ sử dụng botvs API hiện có?

Giấc mơ nhỏ- Có phải mã của plugin giao thức chung này thực hiện post, get và phân tích, đồng thời liên kết với API của botvs? Có, có. - http://127.0.0.1:6666/DigitalAsset Địa chỉ này đại diện cho đối tượng giao dịch, mọi yêu cầu của người quản lý sẽ được gửi đến địa chỉ này. Các phần mềm dịch vụ plugin sẽ nghe lén yêu cầu địa chỉ này và phản hồi, thay cho người quản lý truy cập vào sàn giao dịch và trả lại dữ liệu mà người quản lý yêu cầu.

Giấc mơ nhỏMột phần của nó được thực hiện bằng Python, và nó cũng giống như vậy, với các giao diện và định dạng tương tự. Chỉ có ngôn ngữ khác nhau.

Giấc mơ nhỏĐây là một ví dụ về các plugin bên ngoài người quản lý, nhằm mục đích viết một số mã tương tác trao đổi, để truy cập nền tảng BotVS, tương tự như tự nối sàn giao dịch với BotVS. Bạn có thể sử dụng ngôn ngữ PHP, node.js, python, Go, v.v.

Giấc mơ nhỏVâng, để viết thực hiện BotVS API GetAccount và các ứng dụng giao diện cụ thể như vậy, tương đương với việc tự viết mã của sàn giao dịch liên kết.

Giấc mơ nhỏTheo ví dụ, bạn có thể sử dụng python, node.js, PHP, Golang.