Pencipta Platform Perdagangan Kuantitatif Protokol Umum Akses Bursa Persendirian

Penulis:Mimpi kecil, Dicipta: 2017-08-24 16:29:56, Dikemas kini: 2021-06-10 15:33:02

Dokumen Penggunaan Protokol Am

Pengguna boleh menggunakan protokol ini untuk mengakses mana-mana perkhidmatanAPIPertukaran yang diperdagangkan, protokol API tertentu tidak terhad, sama adarestwebsocketfix... boleh diakses dan digunakan. Contoh protokol umum Python:https://www.fmz.com/strategy/101399

  • 1. Jalankan pemalam protokol umum, tetapan port

    Tuliskan alamat dan tetapan pelabuhan untuk tetapan pemantauan plugin protokol umum. Contoh:http://127.0.0.1:6666/DigitalAssetAtau:http://127.0.0.1:6666/exchange

    Mengapa anda perlu menetapkan semua ini?IPLaluanAdakah itu? KeranaPencipta KuantitiHalamanPusat KawalanMelompatTambah bursaPada halaman ini, pilih untuk mengklik bar protokol umum, yang menunjukkan kecualiAPI-KEYTerdapat juga tetingkap alamat perkhidmatan, yang memberitahu pentadbir IP dan port mana yang harus diakses (pentadbir dan pemalam protokol umum tidak boleh berjalan di peranti yang sama).http://127.0.0.1:6666/DigitalAssetDigitalAssetIni adalah nama yang ditakrifkan sendiri, dan ini hanya contoh.

    Pada halaman pencipta kuantiti tambah bursa, ia kelihatan seperti ini: Biasanya, maklumat akaun pertukaran hanya perlu disesuaikanaccess keydansecret keyWalau bagaimanapun, antara muka API beberapa bursa memerlukan penghantaran kata laluan urus niaga (misalnya antara muka bawah beberapa bursa), di mana kita boleh menulis maklumat konfigurasi yang perlu dihantar apabila kita menemui API pertukaran ini kerana halaman protokol umum tidak mempunyai kawalan yang berlebihan untuk menulis maklumat ini.secret keyJika maklumat tidak sensitif boleh ditulis diaccess keyKemudian anda akan membuat strings dalam plugin protokol umum.splitOperasi, memisahkan data ini, seperti yang ditunjukkan dalam contoh di atas.

    img

    Kemudian, anda boleh mengeditnya di dalam plugin, dan anda boleh mendapatkannya.XXX_PassWordSaya tidak tahu. Sebagai contoh, dalam contoh lengkap di akhir catatan ini, fungsi 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 protokol umum secara keseluruhanmainContoh fungsi:GoPerkataan bahasa:

    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, fungsi tindak balas

    Pemanas protokol umum plugin menyiasat pada port yang ditetapkan, adakah ia dihantar untuk memintarequest◦ Apabila terdapat permintaan, fungsi tindak balas dipanggil untuk melaksanakan tindak balas, kemudian menganalisis parameter dalam data permintaan, data permintaan yang dihantar oleh hos ialah:

    /* 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 有介绍说明。
    }
    */
    

    Oleh itu, struktur yang diperoleh selepas penulisan semula data JSON berdasarkan permintaan yang diterima oleh program pemalam protokol umum.requestDalamMethodKita boleh gunakanswitchPergi ke API untuk mengkaji kuantiti pencipta yang berbeza (iaitu mengenal pasti panggilan dasar yang dijalankan pada hos adalah pencipta yang dikurangkan)APIPermintaan:

    GoContoh bahasa:

    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:
          ...
    

    Buah-buahan ini menulis data yang dikembalikan selepas pelaksanaan dalam struktur yang akan dijawab oleh program pemalam protokol am, dan menjawab permintaan pemegang am.

    Contoh bahasa 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. Jenis panggilan API

    Terdapat dua kategori: 1, Antara antara muka awam yang tidak memerlukan tandatangan, seperti:

    GetTicker()

    GetDepth()

    GetTrades()

    GetRecords(period)

    2. Antara muka pengguna yang memerlukan tandatangan, seperti:

    BuySell

    GetOrder(id)

    GetOrders()

    GetAccount()

    CancelOrder(id)... Cara penandatanganan mungkin berbeza di setiap bursa dan perlu disusun mengikut keperluan.

  • 4. Pencipta mengutip setiap panggilan APIPlugin Protokol UmumdanPengurusFormat data yang berinteraksi:

    Beberapa pencipta mempunyai API kuantitatif sepertiGetName()GetLabel()Fungsi sama, tidak akan dipanggilPlugin Protokol UmumMenghantar permintaan.exchange.GetName()Pertukaran yang dikonfigurasi dengan plugin umum akan dikembalikan "Exchange" apabila dipanggil.

    • 1,GetTicker:Ia digunakan untuk mendapatkan data terkini mengenai pasaran.

      PengurusPerisian yang digunakan untuk menghantar mesej kepada fungsi respon pendengaranrequestDalammethodUntuk:ticker

      Pengurus menghantar parameter:request.Params.symbol: Dikirim oleh pentadbir mengikut mata wang yang ditetapkan pada halaman bot.

      Format data yang dibawa oleh subjek (JSON) apabila pelayan meminta plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus: (iaitu format data yang dikembalikan kepada pengurus selepas plugin protokol umum meminta antara muka pertukaran)

      Struktur JSON

      {
          "data": {
              "time": 1500793319499,  // 毫秒时间戳,整型
              "buy": 1000,            // 以下浮点型
              "sell": 1001,
              "last": 1005,
              "high": 1100,
              "low": 980,
              "vol": 523,
          }
      }
      
    • 2、GetRecords:Digunakan untuk mendapatkan data K-line yang disediakan oleh bursa.

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:records

      Pengurus menghantar parameter:request.Params.periodPerhubungan Nilai:exchange.GetRecordsIni adalah parameter pertama fungsi, sebenarnya.request.Params.periodSiklus yang dinyatakan sebagai bilangan minit, seperti kitaran hari adalah60*24Maksudnya,1440request.Params.symbol: Pengurus menghantar mengikut mata wang yang ditetapkan.

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur 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],
                  ...
          ]
      }
      

      Data ujian bahasa 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}
      }
      

      Platform Pencipta KuantitiLogTunjukkanrecordsMaklumat:

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

      Perhatikan: 1. Elemen pertama dalam matriks dimensi kedua ialahintJenis, yang mewakili timeout.2. Pengurus akan secara automatik memberikan timeout kali 1000, atau lebih.

    • 3, Dapatkan kedalaman:Untuk mendapatkan maklumat lebih mendalam mengenai bursa (Order Minus, Sell One, Sell Two...Buy One, Buy Two...)

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:depth

      Pengurus menghantar parameter:request.Params.symbol: dihantar oleh pengurus mengikut mata wang yang ditetapkan oleh dasar

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur 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:Memperoleh rekod transaksi sepanjang tempoh tertentu (bukan rekod urus niaga sendiri) yang disediakan oleh bursa

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:trades

      Pengurus menghantar parameter:request.Params.symbolNilai untuk mata wang dagangan seperti:btc, yang dihantar oleh pengurus mengikut mata wang yang ditetapkan oleh dasar tersebut.

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur 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:Dapatkan maklumat aset akaun

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:accounts

      Pengurus menghantar parameter: ((perhatian! biasanya mendapatkan semua aset akaun!, khusus melihat antara muka bursa, memperoleh secara berasingan atau mendapatkan maklumat keseluruhan aset)

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur 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、Beli、Jual:Menghantar tempahan, membuat pesanan.

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:trade

      Pengurus menghantar parameter:request.Params.typePengurus: berdasarkan panggilanexchange.Buyatauexchange.SellDi sini terdapat beberapa contoh.request.Params.pricePermintaan dalam strategi:APIIni adalah parameter pertama fungsi.request.Params.amountPermintaan dalam strategi:APIIni adalah parameter kedua untuk fungsi.request.Params.symbol: Pengurus menghantar mengikut mata wang yang ditetapkan.

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

      {
          "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举例
      }
      

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur JSON

      {
          "data": {
              "id": 125456,      // 下单后返回的订单id
                                 // 如果订单id是"asdf346sfasf532"这样的字符串形式
                                 // 此处id也可以是字符串类型
          }
      }
      
    • 7 、GetOrder:Dapatkan maklumat pesanan dengan nombor pesanan tertentu

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:order

      Pengurus menghantar parameter:request.Params.idrequest.Params.symbol

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

      {
          "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格式
      }
      

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur 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, Dapatkan Perintah:Dapatkan semua pesanan yang belum selesai

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestMetod ialah:orders

      Pengurus menghantar parameter:request.Params.symbol

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur JSON

      {
          "data": [{
              "id": 542156,
              "amount": 0.25,
              "price": 1005,
              "deal_amount": 0,
              "type": "buy",      // "buy"、"sell"
              "status": "open",   // "open"
          },{
              ...
          }]
      }
      
    • 9, Batalkan pesanan:Batalkan tempahan pesanan dengan nombor pesanan tertentu

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk:cancel

      Pengurus menghantar parameter:request.Params.id: jenis tali, parameter pertama fungsi API yang dipanggil oleh dasar,request.Params.symbol:btc ((contoh) dihantar oleh pengurus mengikut mata wang yang ditetapkan oleh dasar

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

      Struktur JSON

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

      Perisian yang digunakan untuk menyiarkan mesej yang dihantar oleh pentadbir kepada fungsi tindak balas pendengaranrequestDalammethodUntuk__api_Nama kaedah yang bermula dengan.

      Format data yang dibawa oleh subjek yang diminta oleh pentadbir kepada plugin protokol umum

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

      Struktur nilai pulangan yang akhirnya dihantar kepada pengurus:

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

      Contohnya, panggilan strategi:

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

      Kod percubaan dalam plugin ((go language):

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

      Baris arahan plugin: 2017/08/31 10:19:59 Runninghttp://127.0.0.1:6666/DigitalAsset

      Permintaan.Method,request.Params yang dicetak di barisan arahan pemalamPermintaan yang dihantar oleh pentadbir dalam permintaan selepas data di Body diselesaikan:request.MethodUntuk:__api_cancel_borrowPermintaan yang dihantar oleh pentadbir dalam permintaan selepas data di Body diselesaikan:request.ParamsUntuk:{"borrow_id" : "123", "symbol" : "cny"}

      Perkhidmatan yang boleh disesuaikan untuk mendapatkan akses langsung ke bursaAPIPerbezaanexchange.IOPergi ke laman web ini.

      # 注意:
      # 在调用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的支持

      Pemprosesan automatik di bawahexchange.GetRawJSONPerkhidmatan ini boleh digunakan untuk membuat panggilan, tetapi tidak perlu dilaksanakan di dalam plugin.

    • 对于exchange.Go的支持

      Pemprosesan automatik di bawahexchange.GoPermohonan yang dibuat oleh pengguna tidak perlu diuruskan di dalam plugin.

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

      img

      img

      # 注意:使用exchange.Go在wait的时候如果指定了超时时间, 
      #      一定要确保获取到最终的数据,这样申请的并发线程才能回收。
      
    • Sokongan untuk fungsi hadapan

      Pemprosesan khusus yang perlu dilaksanakan dalam pemalam, seperti menetapkan leverage, kod kontrak, arah pesanan bawah, boleh menetapkan rekod pembolehubah tempatan, mendapatkan pegangan perlu mengakses antara muka bursa untuk mendapatkan data mentah dan memproses struktur pegangan yang ditakrifkan di platform FMZ dan kembali. Apabila anda memanggil fungsi berikut dalam dasar, plugin akan menerimaRpcFormat permintaan sedikit berbeza dengan antara muka lain, perlu diperhatikan dalam pemalam protokol umumRpcRequestFormat, yang membezakan nilai utama param adalah struktur komposit.

      • SetContractType
        Setup kod kontrak.

        {"access_key":"123","method":"io","nonce":1623307269528738000,"params":{"args":["quarter"],"code":2},"secret_key":"123"}
        
      • SetDirection Tetapkan satu arah di bawah niaga hadapan.

        {"access_key":"123","method":"io","nonce":1623308734966484000,"params":{"args":["closesell"],"code":1},"secret_key":"123"}
        
      • Set MarginLevel Setup Leverage Futures.

        {"access_key":"123","method":"io","nonce":1623308734966939000,"params":{"args":[12],"code":0},"secret_key":"123"}
        
      • GetPosition Mengambil saham niaga hadapan. Apabilaexchange.GetPosition()Apabila dipanggil:

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

        Apabilaexchange.GetPosition("swap")Apabila dipanggil:

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

  • Perisian tambahan protokol umum contoh bahasa Go lengkap (mengakses pertukaran 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)
}


Lebih lanjut

RumputPython Access Protocol contoh pertukaran tersuai https://www.fmz.com/strategy/101399

RumputPython Access Protocol Contoh Bursa Persendirian https://www.fmz.com/strategy/101399

liuwei9090Pertukaran bitmex telah mengakses platform, tetapi sama ada menggunakan _C ((exchange.GetTicker).Buy atau _C ((exchange.GetDepth).Bids[0] tidak dapat mendapatkan harga beli, dan juga akan membuat kesalahan. Tidak tahu mengapa.

Hari demi hari.Hahaha, menulis plugin protokol umum boleh menyambungkan API pertukaran sepenuhnya sendiri, atau lebih baik menggunakan program anda sendiri, bukankah ia sedikit lebih baik?

zhchy751Adakah terdapat contoh akses protokol umum siap pakai, bot lengkap / strategi / persediaan bursa, dan sebagainya?

DyahhuKod di atas adalah plugin protokol umum, bukan? Adakah plugin ini serupa dengan template dasar yang boleh saya panggil secara langsung? Saya akan menggunakan py post atau get untuk menghantar dan mendapatkan data, dan menguraikan. Adakah kod plugin protokol umum ini adalah post yang dilaksanakan, get diselesaikan, dan juga alamat pemantauan dan port untuk botvs yang diterima?

chalieSaya akan cuba untuk mendapatkannya dengan Python.

JadingAdakah anda masih belum memahami sepenuhnya... adakah menggunakan plugin program umum bermakna anda perlu menulis banyak fungsi sendiri, seperti GetAccout (?).

AlanyaobotVS bukan semua contoh jS? Kenapa tidak ada jS?

nxtplayer6 untuk terbang, namun tidak berjaya.

Perempuan juga.Keganasan

Mimpi kecilBITMEX adalah bursa niaga hadapan. Anda perlu menetapkan kontrak yang anda mahu beroperasi atau dapatkan, lihat dokumen API untuk fungsi exchange.SetContractType.

Hari demi hari.Saya juga.

Mimpi kecilTetapi bagaimana jika strategi ini adalah berbilang platform?

Mimpi kecilIa boleh disusun menjadi satu fail yang boleh dijalankan dan boleh dijalankan bersama-sama dengan hos. Tidak kira katalog mana. Contoh python belum ada buat masa ini, lepas masa saya tulis satu.

DyahhuBagaimana plugin membuat ia berjalan, misalnya ditulis dalam format py, dan diletakkan terus di direktori exe hoster? atau menambah laluan rujukan ke direktori fail py plugin dalam program py tempatan?

Mimpi kecilTidak memerlukan pengendalian khusus dalam dasar, untuk antara muka BotVS api, objek bursa ini adalah sama (sama ada BotVS yang telah diakses atau anda sendiri menulis protokol umum plugin yang disokong), dasar tidak perlu melakukan apa-apa penyesuaian. Plugin ini berjalan bersama-sama dengan hoster. Lihat di atas Dokumen: 1, Jalankan Plugin Protokol Am, Tetapan Pelabuhan Di sini, kita akan melihat apa yang berlaku.

DyahhuAdakah ini sama seperti template strategi, apabila anda ingin menyambungkan plugin yang sesuai dengan bursa, anda akan memanggil fungsi di sini, dengan bursa lain menggunakan API botvs yang ada?

Mimpi kecil- Adakah kod untuk plugin protokol umum ini adalah post, get yang dilaksanakan dan diselesaikan, dan API botvs yang disambungkan? Ya, saya faham. - http://127.0.0.1:6666/DigitalAsset Alamat ini mewakili objek bursa, semua permintaan pengurus akan dihantar ke alamat ini. Program perkhidmatan pemalam akan mendengar permintaan alamat ini, dan bertindak balas, menggantikan pengurus untuk mengakses bursa, dan mengembalikan data yang diminta oleh pengurus.

Mimpi kecilIni juga sama dengan Python, antara muka dan formatnya sama. Hanya bahasa yang digunakan sahaja yang berbeza.

Mimpi kecilIni adalah contoh bentuk plugin luar hoster yang bertujuan untuk menulis beberapa kod interaksi pertukaran untuk mengakses platform BotVS, yang sama dengan menghubungkan pertukaran ke BotVS sendiri. Anda boleh menggunakan bahasa seperti PHP, node.js, python, Go, dan lain-lain untuk menulis jenis plugin ini, selagi format data sesuai dengan format penerangan dokumen di atas, data yang diperoleh oleh plugin boleh disambungkan ke antara muka BotVS API yang sesuai.

Mimpi kecilYa, untuk menulis implementasi fungsi antara muka seperti BotVS API GetAccount, adalah sama seperti menulis kod sendiri untuk pertukaran yang disambungkan.

Mimpi kecilSebagai contoh, anda boleh menggunakan Python, Node.js, PHP, Golang.