カテゴリー: iOS

Swift ChartsライブラリとCryptowatchAPIでローソク足を描画してみる。

はじめに

こんにちは鈴木です。最近ローソク足の記事を書いているにあたり、テスト用のランダムデータではなく、実際のOHLCのデータを使いたい思っておりました。今回色々調べた結果CryptoWatchのAPIが簡単にJSONデータの取得ができそうだったため実装しました。

CryptoWatchについて

仮想通貨のチャートサイトであり、APIを提供してくれています。
APIの詳細な情報はこちらから確認ができます。
本記事ではCryptWatchのAPIは詳しく触れていませんので詳しく知りたい方は是非!

OHLC

OHLCとはローソク足の描画に使われる。下記の情報の頭文字です。

  • Open/始値
  • High/高値
  • Low/安値
  • Close/終値

上記の情報をCryptWatchAPIで取得します。

リクエスト方法

CryptoWatchのAPI

ローソク足で表示するため

  • OHLCのデータが複数取得できる
  • スパンは1日ごと

CryptWatchの下記のリンクからリクエストを作成します。
https://cryptowat.ch/docs/api#market-ohlc
下記が基本の形式です。ExchangeとPairが気になる方はそれぞれリストをJSON形式で取得することも可能です。
market/[Exchange]/[Pair]/ohlc
例として値を入力するとhttps://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc
アクセスするとJSON形式でデータが取得できます。

{"result":{
  "期間":[[終了時間、始値、高値、安値、終値、出来高],,,,,,],
  "期間":[[終了時間、始値、高値、安値、終値、出来高],,,,,,]
  }
}

今回は一日ごとのデータだけが欲しいため下記の様に指定を行います。
https://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc?periods=86400

Swiftでの実装方法

通信周りは外部ライブラリを使用しないで下記で実装しました。

  • Codable
  • JSONDecoder
  • URLSession

Codable

//一月ごとのデータ
struct MonthOHLCListResult: Codable{
    let result: OHLCList
}
struct OHLCList: Codable{
    //本来であれば別クラスを定義すべきかもしれませんが、処理的にもあまり多くしたくないため[[Double]]としてそのまま利用
    let list: [[Double]]
    private enum CodingKeys: String, CodingKey {
        case list = "86400"
    }
}

JSONDecoder・URLSession
関数として成功時にOHLCListをコールバックする様な実装です。エラー処理は特に考えていません。

    func fetchOHLCData(completionHandler: @escaping (OHLCList) -> Void ){
        guard let url = URL(string: "https://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc?periods=86400") else {
            return
        }
        URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
            guard let data = data else{
                return
            }
            //JSONDecoder
            let decoder = JSONDecoder()
            //JSONDecoderでレスポンス→MonthOHLCListResultクラスとして変換
            if let response = try? decoder.decode(MonthOHLCListResult.self, from: data){
                //メインスレッドで返却
                DispatchQueue.main.async{
                    completionHandler(response.result)
                }
            }
        }.resume()
    }

以前の実装の移動平均線の描画箇所をコールバックの値を使用するように変更しました。
細かな説明は不要かとは思いますので今回の変更を含めてまとめて記述します。

import UIKit
import Charts

class ViewController: UIViewController,ChartViewDelegate {
    @IBOutlet weak var combinedChartView: CombinedChartView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //データ取得できたら更新
        fetchOHLCData { ohlcList in
            let combinedData = CombinedChartData()
            combinedData.candleData = self.generateCandleData(ohlcList)
            combinedData.lineData = self.generateMovingAverageData(ohlcList)
            self.setupCombineChart(self.combinedChartView,data: combinedData)
        }
        
    }

    func generateCandleData(_ ohlcList: OHLCList) -> CandleChartData{
        let candleChartDataEntries:[CandleChartDataEntry] = ohlcList.list.enumerated().compactMap{
            return CandleChartDataEntry(x: Double($0.offset), shadowH: $0.element[2], shadowL: $0.element[3], open: $0.element[1], close: $0.element[4])
            
        }
        let set = CandleChartDataSet(values: candleChartDataEntries, label: "Candle DataSet")
        set.setColor(.lightText)
        set.decreasingColor = .blue
        set.decreasingFilled = true
        set.increasingColor = .red
        set.increasingFilled = true
        set.shadowColor = .darkGray
        set.valueFont = .systemFont(ofSize: 10)
        set.drawValuesEnabled = false
        return CandleChartData(dataSet: set)
    }

    func generateMovingAverageData(_ ohlcList: OHLCList) -> LineChartData{
        //幾つのデータの平均値を取得するか
        let movingDataCount = 5
        var movingLineChartData:[ChartDataEntry] = []
        ohlcList.list.enumerated().forEach { (i, data) in
            //最初の4件はデータを表示しない
            guard i >= movingDataCount - 1 else{
                return
            }
            //移動平均線用にデータを計算頭の4つはデータの取得が出来ていないのでこのような形
            let val = ohlcList.list[i - (movingDataCount - 1) ... i].reduce(0, { (result, ohlc) -> Double in
                result + ohlc[4]
            }) / Double(movingDataCount)
            movingLineChartData.append(ChartDataEntry(x: Double(i), y: val))
        }
        let set1 = LineChartDataSet(values: movingLineChartData, label: "MovingAverageData")
        set1.lineWidth = 1.75
        set1.setColor(.black)
        set1.highlightColor = .yellow
        set1.drawValuesEnabled = false
        set1.drawCirclesEnabled = false
        return LineChartData(dataSet: set1)
    }
    
    
    
    func fetchOHLCData(completionHandler: @escaping (OHLCList) -> Void ){
        guard let url = URL(string: "https://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc?periods=86400") else {
            return
        }
        URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
            guard let data = data else{
                return
            }
            //JSONDecoder
            let decoder = JSONDecoder()
            //JSONDecoderでレスポンス→MonthOHLCListResultクラスとして変換
            if let response = try? decoder.decode(MonthOHLCListResult.self, from: data){
                print(response.result)
                DispatchQueue.main.async{
                    completionHandler(response.result)
                }
            }
        }.resume()
    }
    
    //  複合チャートの描画
    func setupCombineChart(_ chart: CombinedChartView, data: CombinedChartData){
        chart.dragEnabled = true
        chart.setScaleEnabled(true)
        chart.pinchZoomEnabled = true
        //      legendの設定
        chart.legend.horizontalAlignment = .left
        chart.legend.verticalAlignment = .bottom
        chart.legend.orientation = .horizontal
        chart.legend.drawInside = false
        chart.legend.font = UIFont(name: "HelveticaNeue-Light", size: 10)!
        //      左の目盛りの設定
        chart.leftAxis.labelFont = UIFont(name: "HelveticaNeue-Light", size: 10)!
        chart.leftAxis.spaceTop = 0.3
        chart.leftAxis.spaceBottom = 0.3
//        chart.leftAxis.axisMinimum = 90
        //
        chart.rightAxis.enabled = false
        chart.xAxis.enabled = false
        chart.data = data
        
        chart.setVisibleYRange(minYRange: data.yMax - 2000, maxYRange: data.yMax, axis: .left)
        chart.setVisibleXRange(minXRange: 20, maxXRange: 50)
    }
}


//一月ごとのデータ取得
struct MonthOHLCListResult: Codable{
    let result: OHLCList
}


struct OHLCList: Codable{
    //本来であれば別クラスを定義すべきかもしれませんが、処理的にもあまり多くしたくないため[[Double]]としてそのまま利用
    let list: [[Double]]
    private enum CodingKeys: String, CodingKey {
        case list = "86400"
    }
}

さいごに

最後までありがとうございます。今回JSONDecoder+Codableを使いたかったため、通信周りはよそのライブラリを使用しないで実装しました。
APIにもよりますがライブラリ不要でシンプルな実装ができるのは嬉しいですね。

おすすめ書籍

    

— NORMAL —
suzuki

シェア
執筆者:
suzuki
タグ: Swift

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前