見出し画像

Regonn&Curry.fm Episode 110収録後記 Nim言語で仮想通貨取引所APIを利用する

このnoteは、Regonn&Curry.fm というポッドキャストの第110回の収録後記です。

110回では、JupyterLab 3.0やDALL·Eについて話していました。

最後の所で話していたのですが、現在BTCMEXという海外の仮想通貨取引所でBOT(手動での取引でなくAPIを利用した取引)限定のコンペティションが開催されていて参加しています。

高頻度取引等においては、処理のスピードも必要なので、最近はNim言語を利用してAPI等を叩いています(おそらく誤差の範囲ですが)

Pythonの場合はccxtというライブラリを利用すれば、主要な取引所の注文が取引所毎の仕様の差分を無視して共通で利用できます。(しかし、BTCMEXはccxtが未対応だったのでPythonでやるにしても自分で書いていく必要がありそうです)

Nim言語で書くにあたり、取引所毎にAPIで渡す情報が違っているので、今回は今まで自分が書いてきた幾つかの取引所をNim言語で叩くためのサンプルコードを紹介します。(実際の利用についてはすべて自己責任でお願いします。また、この記事は投資助言ではありません。)

共通部分

まず、どの取引所でもAPIキーとSecretキーはそれぞれの取引所で必要なので準備しておいてください。

また、HMAC (Hash-based Message Authentication Code) という認証メッセージコードが必要になってくるので、hmac というライブラリをインストールしておきます。

Nim言語はバージョン 1.4.2 を利用しています。

$ nimble install hmac -y

共通で利用するコードを書いておきます。

import httpclient, strformat, times, hmac, json, strutils, os, times, json, math, tables, sequtils

const api_key = "YOUR_API_KEY"
const secret_key = "YOUR_SECRET_KEY"

let client = newHttpClient(timeout = 10000)


BTCMEX

公式ドキュメントとサンプルが非常に少なく苦労しました。

他とは違ってタイムスタンプではなく expire を指定する必要があります。

また、Publicな情報が全然取得できないと悩んでいましたが、"/api/v1/trade" で "filter": {"open": true} を渡すとPublicな取引情報が取得できるっぽいですが、そんなことはドキュメントに全然書いていない。。。

GET系(口座情報を取得)

const end_point: string = "https://www.btcmex.com"

const margin_path = "/api/v1/user/margin"

proc get_balance(): float =
   let api_path = margin_path
   let request_method = "GET"
   let expires = int(toUnixFloat(getTime() + initDuration(seconds = 5)))
   let body = "";
   let signature_payload = fmt"{request_method}{api_path}{expires}{body}"
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "api-expires": $expires,
       "api-key": $api_key,
       "api-signature": signature
   })
   let response = request(client= client, url=fmt"{end_point}{api_path}", httpMethod=request_method, headers=headers)
   let jsonNode = parseJson(response.body)
   return jsonNode["walletBalance"].getFloat() / 100000000.0 # BTC単位に直す

POST系(指値でPostOnlyでの注文)

const order_path = "/api/v1/order"

proc post_order(symbol: symbol, side: string, price: float, qty: int) =
   let api_path = order_path
   let request_method = "POST"
   let expires = int(toUnixFloat(getTime() + initDuration(seconds = 5)))
   # "execInst": "ParticipateDoNotInitiate" で指値にならない場合はキャンセル   let body = %* {"symbol": symbol, "side": side, "orderQty": $qty, "price": $price, "ordType": "Limit", "execInst": "ParticipateDoNotInitiate"};
   let signature_payload = fmt"{request_method}{api_path}{$expires}{body}"
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "api-expires": $expires,
       "api-key": $api_key,
       "api-signature": signature
   })
   let response = request(client= client, url=fmt"{end_point}{api_path}", httpMethod=request_method, headers=headers, body= $body)

bybit

GET系はURLパラムで渡し、POST系はjsonを使って渡します。

GET系(指定した通貨の口座情報を取得)

const end_point: string = "https://api.bybit.com"

const wallet_balance_path = "/v2/private/wallet/balance"

proc get_balance(coin: string): float =
   let api_path = wallet_balance_path
   let request_method = "GET"
   let timestamp = int(toUnixFloat(getTime())) * 1000
   var body_params = {
       "api_key": api_key,
       "coin": coin,
       "timestamp": $timestamp,
   }.toOrderedTable() # Keyがabc順に並ぶように
   
   # TODO: map 処理で 'key=value' の形にして join したい   var signature_payload = ""
   for key, value in body_params:
       signature_payload &= key & "=" & value & "&"
   signature_payload.removeSuffix("&")
   
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "Content-Type": "application/json"
   })
   let response = request(client= client, url=fmt"{end_point}{api_path}?{signature_payload}&sign={signature}", httpMethod=request_method, headers=headers)
   let jsonNode = parseJson(response.body)
   return jsonNode["result"][coin]["wallet_balance"].getFloat()

POST系(指値でPostOnlyでの注文)

const end_point: string = "https://api.bybit.com"

const order_path = "/v2/private/order/create"

proc post_order(symbol: string, side: string, price: float, qty: int) =
   let api_path = order_path
   let request_method = "POST"
   let timestamp = int(toUnixFloat(getTime())) * 1000
   var body_params = {
       "api_key": api_key,
       "order_type": "Limit",
       "price": $price,
       "qty": $qty,
       "side": side,
       "symbol": symbol,
       "time_in_force": "PostOnly",
       "timestamp": $timestamp,
   }.toOrderedTable() # Keyがabc順に並ぶように
   
   # TODO: map 処理で 'key=value' の形にして join したい
   var signature_payload = ""
   for key, value in body_params:
       signature_payload &= key & "=" & value & "&"
   signature_payload.removeSuffix("&")
   
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "Content-Type": "application/json"
   })
   body_params["sign"] = signature
   let body = %* body_params
   let response = request(client= client, url=fmt"{end_point}{api_path}", httpMethod=request_method, headers=headers, body= $body)

GMOコイン

日本のサービスでサンプルコードも色々な言語に対応してありましたが、Nim言語はなかったです。。。

エンドポイントがPublicとPrivateに分かれています。

GET系(アカウントの口座情報を取得)

const public_end_point: string = "https://api.coin.z.com/public"
const private_end_point: string = "https://api.coin.z.com/private"

const assets_path = "/v1/account/assets"

proc get_assets(): seq[JsonNode] =
   let api_path = assets_path
   let request_method = "GET"
   let ts = toUnix(getTime()) * 1000;
   let signature_payload = fmt"{ts}{request_method}{api_path}"
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "API-KEY": api_key,
       "API-SIGN": signature,
       "API-TIMESTAMP": $ts,
   })
   let response = request(client= client, url=fmt"{private_end_point}{api_path}", httpMethod=request_method, headers=headers)
   let jsonNode = parseJson(response.body)
   let result = getElems(jsonNode["data"])
   return result

POST系(指値でPostOnlyでの注文)

const public_end_point: string = "https://api.coin.z.com/public"
const private_end_point: string = "https://api.coin.z.com/private"

const order_path = "/v1/order"

proc post_order(side: string, symbol: string, price: int, size: float) =
   let api_path = order_path
   let request_method = "POST"
   let ts = toUnix(getTime()) * 1000;
   let body = %*{
       "symbol": symbol,
       "side": side,
       "executionType": "LIMIT",
       "timeInForce": "SOK", # 指値にならない場合はキャンセル
       "price": price,
       "size": size,
   }
   let signature_payload = fmt"{ts}{request_method}{api_path}{body}"
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "API-KEY": api_key,
       "API-TIMESTAMP": $ts,
       "API-SIGN": signature
   })
   let response = request(client= client, url=fmt"{private_end_point}{api_path}", httpMethod=request_method, headers=headers, body= $body)

FTX

FTXではPublicとPrivateの概念はなく、全てAPIキーを利用してデータのやり取りをします。また、サブアカウントという複数のアカウントを切り分けて使えることができ、今回もサブアカウントを指定して利用します。

GET系(サブアカウントの口座情報を取得)

const end_point: string = "https://ftx.com"

const wallet_path = "/api/wallet/balances"
const sub_account = "YOUR SUB ACCOUNT NAME"

proc get_balances(): seq[JsonNode] =
   let api_path = wallet_path
   let request_method = "GET"
   let ts = toUnix(getTime()) * 1000;
   let signature_payload = fmt"{ts}{request_method}{api_path}"
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "FTX-KEY": api_key,
       "FTX-SIGN": signature,
       "FTX-TS": $ts,
       "FTX-SUBACCOUNT": sub_account
   })
   let response = request(client= client, url=fmt"{end_point}{api_path}", httpMethod=request_method, headers=headers)
   let jsonNode = parseJson(response.body)
   let result = getElems(jsonNode["result"])
   return result

POST系(指値でPostOnlyでの注文)

const end_point: string = "https://ftx.com"

const orders_path = "/api/orders"
const sub_account = "YOUR SUB ACCOUNT NAME"

proc post_order(side: string, coin: string, price: float, size: float) =
   let api_path = orders_path
   let request_method = "POST"
   let ts = toUnix(getTime()) * 1000;
   let body = %*{
       "market": fmt"{coin}/USD",
       "side": side,
       "price": price,
       "type": "limit",
       "size": size,
       "postOnly": true,
       "clientId": fmt"{sub_account}-{toUnix(getTime())}"
   }
   let signature_payload = fmt"{ts}{request_method}{api_path}{body}"
   let signature = toHex(hmac_sha256(key = secret_key, data = signature_payload))
   let headers = newHttpHeaders({
       "FTX-KEY": api_key,
       "FTX-SIGN": signature,
       "FTX-TS": $ts,
       "FTX-SUBACCOUNT": sub_account,
       "Content-Type": "application/json"
   })
   let response = request(client= client, url=fmt"{end_point}{api_path}", httpMethod=request_method, headers=headers, body= $body)

実際に書いてみて

やっぱり ccxt みたいな複数の取引所をまとめて操作できるライブラリは便利だなと改めて思いました。みなさんも良かったらNim言語を使ってみてください。


この記事が気に入ったらサポートをしてみませんか?