Mac
FX
Swift
システムトレード
仮想通貨

Swiftで始める仮想通貨システムトレード入門

この投稿は、Abema Advent Calendar 2017の19日目の投稿です。

こんにちは、AbemaTV iOSチーム所属のrinovこと、石川です。

TL;TR;

[追記]2018/2/1
こちらをご利用いただくことで、売買ルールを組むだけですぐに自動取引を開始できますので、是非ご利用ください :rocket:
https://github.com/rinov/SwiftFlyer

きっかけ

前々から競技プログラミングや将棋のプログラムを書いたりと、アルゴリズムが結構好きなこともあり最近システムトレードというのに興味を持ったのがきっかけです。

なので今日は普段使ってるSwiftを使って、業務以外のことをネタにして書いてみたいと思います。
週末に一日書けて作ったのですが7000行ほどあるので、細かなロジックなどまではここでは紹介しきれないためざっくりとした流れで説明していきたいと思いますが、よろしくお願いします。
では早速初めていきましょう。

システムトレードとは

システムトレードとは、投資を行う際に裁量を排し一定売買ルールに従って売買を行う方法を指す和製英語。通常コンピュータに行わせる非裁量トレードの事を言う。略して「シストレ」。システムトレードをする人のことを『システムトレーダー』と称する。 (Wikipediaより)

開発環境

  • MacOS
  • Swift4
  • Xcode9.1

API

取引をするためにはシステムトレードから呼び出すためのAPIが必要になります。今回はビットコイン取引所であるbitFlyer LightningのAPIを使用します。
ドキュメントはこちらから参照できます。
https://lightning.bitflyer.jp/docs#http-api

APIの実装

ドキュメントのAPI一覧に引数とレスポンスが載っているので、それに沿って実装します。
Swift4からはデフォルトのDecodableURLSessionのみで簡単に実装することができます。
bitFlyer Lightning APIではPublic APIとPrivate APIの2種類があります。(今回はリアルタイムAPIは時間の都合上省略します)
Public APIはその名の通り、そのままリクエストをすることで取得することができ、Private APIは認証情報としてAPI Keyと秘密鍵で署名した情報をリクエストヘッダーに付加することで取得することができるようなります。

必要なライブラリ

PrivateAPIなどで認証が必要な場合に署名を行うため下記のライブラリをPod等でインストールします。

target 'SystemTradeSample' do
  use_frameworks!
  pod 'CryptoSwift', '~> 0.8.0'
end

APIの実装例

例としてPrivate APIの「現在の資産」を取得するプログラムを書いてみましょう。

モデルの定義

APIドキュメントのレスポンスでは以下のように定義されています。

[
  {
    "currency_code": "JPY",
    "amount": 1024078,
    "available": 508000
  },
  {
    "currency_code": "BTC",
    "amount": 10.24,
    "available": 4.12
  },
  {
    "currency_code": "ETH",
    "amount": 20.48,
    "available": 16.38
  }
]

これはDecodableでこのように定義することができます。

// 仮想通貨の種類
enum CurrencyCode: String, Decodable {
    case jpy = "JPY"
    case btc = "BTC"
    case eth = "ETH"
    case etc = "ETC"
    case bch = "BCH"
    case ltc = "LTC"
    case mona = "MONA"
}

// 資産モデル
struct Balance: Decodable {
    let amount: Double
    let available: Double
    let currencyCode: CurrencyCode

    enum CodingKeys: String, CodingKey {
        case amount = "amount"
        case available = "available"
        case currencyCode = "currency_code"
    }
}

これでモデルの定義は以上となりレスポンスの形式は[Balance]と表すことができるようになりました。
続いて、取引をリクエストするための実装を行います。

リクエストの実装

// 結果を表す列挙体
enum Result<T, E> {
    case success(T)
    case failed(E)
}

// エラーを定義(簡易)
enum ResponseError: Error {
    case unexpectedResponse
}

// リクエストするためのプロトコル
protocol Requestable {

    associatedtype Response: Decodable

    var baseURL: URL { get }

    var path: String { get }

    var headerField: [String: String] { get }

    var needsAuthorization: Bool { get }

    var httpMethod: String { get }

    var httpBody: Data? { get }

    var queryParameters: [String: Any] { get }
}

// リクエストを担うクラス
final class Session {

    static let shared = Session()

    private init() {}

    func send<T: Requestable>(_ request: T, closure: @escaping (Result<T.Response, ResponseError>) -> Void) {

        // `buildURLRequest`の実装は後述を参照
        let urlRequest = request.buildURLRequest()

        let task = URLSession.shared.dataTask(with: urlRequest) { (data, rawResponse, error) in

            // If an error is occurred.
            if error != nil {
                closure(.failed(.unexpectedResponse))
                return
            }

            // If the data is empty.
            guard let data = data else {
                closure(.failed(.unexpectedResponse))
                return
            }

            // Decode the value.
            do {
                let decoder = JSONDecoder()
                let result = try decoder.decode(T.Response.self, from: data)
                closure(.success(result))
            } catch {
                // Catch DecodingError
            }
        }
        task.resume()
    }
}

次に認証が必要な場合にヘッダーに署名したフィールドを付与しなければならないためRequestableへのextensionを定義します。

    func buildURLRequest() -> URLRequest {
        let url = baseURL.appendingPathComponent(path)

        var urlRequest = URLRequest(url: url)
        var header: [String: String] = headerField

        urlRequest.httpMethod = httpMethod

        // 認証が必要なリクエストの場合        
        if isAuthorizedRequest {
            header["ACCESS-KEY"] = // bitFlyer API Key
            let timeStamp = String(Date().timeIntervalSince1970)
            header["ACCESS-TIMESTAMP"] = timeStamp
            header["ACCESS-SIGN"] = makeAccessSignWith(accessKey: // bitFlyer API Key,
                                                       timeStamp: timeStamp,
                                                       method: self.httpMethod,
                                                       path: self.path,
                                                       queryParams: self.queryParameters,
                                                       body: self.httpBody)
            header["Content-Type"] = "application/json"
        }

        header.forEach { key, value in
            urlRequest.addValue(value, forHTTPHeaderField: key)
        }

        if let body = self.httpBody {
            urlRequest.httpBody = body
        }

        guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            return urlRequest
        }

        urlComponents.query = queryParameters
            .map { "\($0.key)=\($0.value)" }
            .joined(separator: "&")

        urlRequest.url = urlComponents.url

        return urlRequest
    }

    // HMAC sha256でリクエスト内容を署名した結果を返す
    private func makeAccessSignWith(accessKey: String, timeStamp: String, method: String, path: String, queryParams: [String: Any], body: Data?) -> String? {

        var bytes: [UInt8] = []

        bytes += timeStamp.bytes
        bytes += httpMethod.bytes
        bytes += path.bytes

        if !queryParams.isEmpty {
            let requestBody = "?" + queryParams
                .map { "\($0.key)=\($0.value)" }
                .joined(separator: "&")
            bytes += requestBody.bytes
        }

        if body?.isEmpty == false {
            if let bodyParameter = body,
                let bodyString = String(data: bodyParameter, encoding: .utf8) {
                bytes += bodyString.bytes
            }
        }

        let signedString = try? HMAC(key: "bitFlyer Secret API Key", variant: .sha256).authenticate(bytes)

        return signedString?.toHexString()
    }

続いて、リクエストも作成してみましょう。

final class GetBalanceRequest: Requestable {

    typealias Response = [Balance]

    var path: String {
        return "/v1/me/getbalance"
    }

    var httpMethod: String {
        return "GET"
    }

    // 認証が必要
    var isAuthorizedRequest: Bool {
        return true
    }

    init() {}
}

これであとはSessionクラスにリクエストを渡すだけでデコードされたモデルまたはエラーが取得されます。
ここまででAPIの準備は完了です。

例: 資産残高の取得

作成したAPIは下記のように使用することができます。

let request = GetBalanceRequest()

Session.shared.send(request) { result in
   switch result {
   case .success(let response):
     print(response)
   case .failed(let error):
     print(error)
   }
}

自動売買させるにあたって

ここまでの手順を参考にbitFlyer Lightning APIの注文関連のAPIも実装します。

新規注文を出す
注文をキャンセルする
新規の親注文を出す(特殊注文)
親注文をキャンセルする
すべての注文をキャンセルする
注文の一覧を取得
親注文の一覧を取得
親注文の詳細を取得
約定の一覧を取得
建玉の一覧を取得
証拠金の変動履歴を取得
取引手数料を取得

ここで、いよいよ自動トレードが可能になります。

ここまできてですが、入門(自動売買可能な状態)は以上となります!
あとは各自で試行錯誤の上ロジックについては頑張ってみてください。

これで終わりたいところですが、最後にいくつかシステムトレードをする上での考慮点と、より収益性を上げるために役立った情報を残してみますので少しでも参考になればと思います。

注意点

(1) 成行き注文は基本しない
(2) 取引所ステータスを考慮する
(3) メンテナンス時間を考慮する

(1)...成行注文は突発的な価格の上昇または下降によって約定した時点で思わぬ損失がでる場合があります。システムトレードのメリットは即応性のため、板情報から価格を取得してそれを参考に指値で注文することを基本としましょう。指値の新規注文時にtimeInForceというオプションがあります。(デフォルトではGTC)

TimeInForce 意味
GTC(Good Til Canceled) 注文が約定するかキャンセルされるまで有効であるという注文執行数量条件
IOC(Immediate or Cancel) 指定した価格かそれよりも有利な価格で即時に一部あるいは全部を約定させ、約定しなかった注文数量をキャンセルさせる注文執行数量条件
FOK(Fill or Kill) 発注の全数量が即座に約定しない場合当該注文をキャンセルする注文執行数量条件。

指値注文の時にデフォルトのGTCだと条件が満たされない場合に売買されないため、注文が残ってしまい相場が反転した時に執行されてしまう可能性があります。そのためIOCまたはFOKを使用することにより自分が指値で出した注文に対して即時な時間制約も付与できる + 注文が残らないというメリットがあるため、これを活用することによって注文時のリスクを最大限に減らすことができます。(機会損失とはトレードオフになりますが、コツコツといきたいか、それともリスクを負ってでも機会を逃したくないかは人それぞれだと思いますので自分にあったスタイルを選択すると良いです)

(2)...取引所のステータスとは、現在の取引所の混雑率を表しておりbitFlyerでは下記のように定義されています。

ステータス 状態
NORMAL 取引所は稼動しています。
BUSY 取引所に負荷がかかっている状態です。
VERY BUSY 取引所の負荷が大きい状態です。
SUPER BUSY 負荷が非常に大きい状態です。発注は失敗するか、遅れて処理される可能性があります。
NO ORDER 発注が受付できない状態です。
STOP 取引所は停止しています。発注は受付されません。

ここで重要なのがこれらは注文を受け付けるまでの遅延に直結しているということです。BUSY以上のステータスではこちらが送った注文が遅延して受理される、または、注文が失敗する可能性があります。遅延した状態での注文は短期トレードを行う上でかなり致命的になるため、負荷が大きい状態時用のロジックも考慮するか、取引自体を一時停止にさせたほうが良いです。

(3)...bitFlyerは朝4時から10分間のメンテナンス時間が毎日あるため、24時間運転する場合には注意しましょう。

収益を上げるために

(1) ボリンジャーバンドで予測する
(2) 特殊注文を活用する
(3) トレンドによって取引量を動的に変更する

(1)...ボリンジャーバンドの説明はこちらから。ボリンジャーバンドは現在の価格の推移がある一定区間に入る確率を表しています。つまり、損切の目安を資産のn%になったらではなく、ボリンジャーバンドの区間を目安に決めることが出来ます。上昇下降が激しい相場で決め打ちの損切りは突発的な上昇または下降によって約定してしまい、もったいないタイミングで損切りばかりするケースも考えられます。

(2)...bitFlyerでは成行き、指値以外にも特殊注文というものが用意されています。

注文種別 意味
IFD If Doneの略で、一度に2つの注文を出して最初の注文が約定したら2つめの注文が自動的に発注される注文パターンです。
OCO One-Cancels-the-Other orderの略で、2つの注文を同時に出して一方の注文が成立した際にもう一方の注文が自動的にキャンセルされる注文パターンです。
IFDOCO IFDとOCOの組み合わせで、IFD注文が約定した後に自動的にOCO注文が発注される注文パターンです。

IFDは注文が通ったら自動的に利益を確定させる注文もその時点で入れておくことが出来ます。
OCOはすでに注文が通っている状態で行うことで、利益確定と損切りの注文を同時に出すことが出来ます。また、どちらかが成立すると片方は自動でキャンセルされるため使い勝手が良いです。
IFDOCOはIFD+OCOなので、もし出した注文が成立したらその時点で利益確定損切りも自動で行いたい場合に便利です。

(3)...システムトレードでは自分で決めた取引量で売買することができます。そのため任意の時間足をみて、全体が上昇トレンドの場合には買い注文は順張りになるため取引量を上げ、逆張りの場合にはそのままなどトレンドの傾向によって条件を変えると良いです。