カテゴリー: Android

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

はじめに

こんにちはsuzukiです。本日はSwiftの記事でも紹介したCryptoWatchのAPIを利用してMPAndroidChartのチャートを表示したいと思います。前回の記事ではOkHttp3の導入で詰まった際の備忘を書いたので、もし詰まってしまった方は参考にしていただければと思います。

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

Androidでの実装

通信周りはOkHttp3を利用します。
またOkHttpで取得したデータをMoshiというライブラリを利用してマッピングします。

実装前の準備

build.gradle(Module:app)に下記の内容を追加

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.macbook007.blogcharts"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    //下記を追加
    compileOptions {
        targetCompatibility = "8"
        sourceCompatibility = "8"
    }
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    //OkHttpの追加
    implementation("com.squareup.okhttp3:okhttp:3.14.1")
    //Moshiの追加
    implementation("com.squareup.moshi:moshi:1.8.0")

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.github.PhilJay:MPAndroidChart:v3.0.3'
}

AndroidManifestにパーミッションの追加

    

データクラスの作成

OkHttpのレスポンスをマッピングするためのデータクラスを作成します。

//JSONがresult:{86400:[[],[]]}}という形のためデータクラスを二つ作成する。
data class ResponseData(
    val result: OhlcList
)
data class OhlcList (
    @field:Json(name = "86400")
    var ohlcList:List<List<Float>>
)

リクエストの作成

通信のエラーのハンドリングなどは特に記述していません。

    //通信を結果のOhlcListをコールバックで返却を行う。
    fun fetchOHLCData( callback: (OhlcList) -> Unit){
        val client = OkHttpClient()
        val request = Request.Builder()
            .url("https://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc?periods=86400")
            .build()

        client.newCall(request).enqueue(object : okhttp3.Callback {

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {

                response.body()?.string()?.let {
                    val adapter = Moshi.Builder().build().adapter(ResponseData::class.java)
                    adapter.fromJson(it)?.result?.let {
                        callback(it)
                    }
                }
            }
            override fun onFailure(call: Call, arg1: IOException) {

            }
        })
    }

データ作成

行なった変更は今までランダム生成していたキャンドル用のデータから通信結果から取得したOhlcListを元に変更しております。

   //ローソク足のデータ作成
   private fun generateCandleData( ohlcList: OhlcList): CandleData{
        val values = mutableListOf<CandleEntry>()
        ohlcList.list.forEachIndexed { index, list ->
            values.add(
                CandleEntry(index.toFloat(),list[2],list[3],list[1],list[4])
            )
        }
        val set1 = CandleDataSet(values, "SampleChandleData").apply {

            axisDependency = YAxis.AxisDependency.LEFT
            shadowColor = Color.DKGRAY
            shadowWidth = 0.7f
            //アイコン描画
            setDrawIcons(false)
            //color for open < close
            decreasingColor = Color.RED
            decreasingPaintStyle = Paint.Style.FILL
            //color for open > close
            increasingColor = Color.rgb(122, 242, 84)
            increasingPaintStyle = Paint.Style.STROKE
            neutralColor = Color.BLUE
        }
        return CandleData(set1)
    }
   //移動平均線のデータ作成
    fun movingAverageData(ohlcList: OhlcList): LineData{
        //幾つのデータの平均値を取得するか
        val movingDataCount = 5
        var movingLineChartData = mutableListOf<Entry>()
        ohlcList.list.forEachIndexed { index, list ->
            if (index >= movingDataCount -1) {
                //多元配列をreduceでうまく扱えませんでした。一時的にsumという変数を作成しています。
                var sum = 0.0f
                ohlcList.list.slice(index - (movingDataCount - 1)..index).forEach {
                    sum += it[4]
                }
                movingLineChartData.add(Entry(index.toFloat() + 0.5f, sum / movingDataCount))
            }
        }
        val yVals = LineDataSet(movingLineChartData, "MovingAverageData").apply {
            lineWidth = 2f
            highLightColor = Color.YELLOW
            setDrawValues(false)
            axisDependency =  YAxis.AxisDependency.LEFT
            color = Color.BLACK
            setDrawCircles(false)
            setDrawCircleHole(false)
        }
        val data = LineData(yVals)
        data.setValueTextColor(Color.BLACK)
        data.setValueTextSize(9f)
        return  data
    }

こちらで今回行いたかった通信したデータからチャートの作成が完了です。
念のためコードの全量を記述いたします。

class MainActivity : AppCompatActivity(),OnChartValueSelectedListener {

    private var mTypeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        fetchOHLCData {
            val combineData = CombinedData()
            combineData.setData(generateCandleData(it))
            combineData.setData(movingAverageData(it))
            setCombinedChart(combineData)
        }
    }

    //データからしてチャートを更新
    private fun setCombinedChart(combinedData: CombinedData){
        combinedChart.apply {
            setOnChartValueSelectedListener(this@MainActivity)
            description.isEnabled = false
            setTouchEnabled(true)
            isDragEnabled = true
            isScaleXEnabled = true
            setPinchZoom(false)
            setDrawGridBackground(false)
            //データラベルの表示
            legend.apply {
                form = Legend.LegendForm.LINE
                typeface = mTypeface
                textSize = 11f
                textColor = Color.BLACK
                verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM
                horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT
                orientation = Legend.LegendOrientation.HORIZONTAL
                setDrawInside(false)
            }

            //y軸右側の表示
            axisRight.isEnabled = false

            //X軸表示
            xAxis.apply {
                typeface = mTypeface
                //表示領域の調整
                setDrawLabels(false)
                setDrawGridLines(true)
            }

            //y軸左側の表示
            axisLeft.apply {
                typeface = mTypeface
                textColor = Color.BLACK
                setDrawGridLines(true)
            }
            data = combinedData
            //チャートの初期表示を調整
            setVisibleYRange( data.yMax - 2000, data.yMax,YAxis.AxisDependency.LEFT)
            setVisibleXRange(20f, 50f)
            //チャートの更新を行うため必要
            notifyDataSetChanged()
            invalidate()
        }
    }
    
    fun fetchOHLCData( callback: (OhlcList) -> Unit){
        val client = OkHttpClient()
        val request = Request.Builder()
            .url("https://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc?periods=86400")
            .build()

        client.newCall(request).enqueue(object : okhttp3.Callback {

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {

                response.body()?.string()?.let { s ->
                    val adapter = Moshi.Builder().build().adapter(ResponseData::class.java)
                    adapter.fromJson(s)?.result?.let {
                        callback(it)
                    }
                }
            }
            override fun onFailure(call: Call, arg1: IOException) {

            }
        })
    }
    
    private fun generateCandleData( ohlcList: OhlcList): CandleData{
        val values = mutableListOf<CandleEntry>()
        ohlcList.list.forEachIndexed { index, list ->
            values.add(
                CandleEntry(index.toFloat(),list[2],list[3],list[1],list[4])
            )
        }
        val set1 = CandleDataSet(values, "SampleChandleData").apply {

            axisDependency = YAxis.AxisDependency.LEFT
            shadowColor = Color.DKGRAY
            shadowWidth = 0.7f
            //アイコン描画
            setDrawIcons(false)
            //color for open < close
            decreasingColor = Color.RED
            decreasingPaintStyle = Paint.Style.FILL
            //color for open > close
            increasingColor = Color.rgb(122, 242, 84)
            increasingPaintStyle = Paint.Style.STROKE
            neutralColor = Color.BLUE
        }
        return CandleData(set1)
    }
    
    fun movingAverageData(ohlcList: OhlcList): LineData{
        //幾つのデータの平均値を取得するか
        val movingDataCount = 5
        var movingLineChartData = mutableListOf<Entry>()
        ohlcList.list.forEachIndexed { index, list ->
            if (index >= movingDataCount -1) {
                //多元配列をreduceでうまく扱えませんでした。一時的にsumという変数を作成しています。
                var sum = 0.0f
                ohlcList.list.slice(index - (movingDataCount - 1)..index).forEach {
                    sum += it[4]
                }
                movingLineChartData.add(Entry(index.toFloat() + 0.5f, sum / movingDataCount))
            }
        }
        val yVals = LineDataSet(movingLineChartData, "MovingAverageData").apply {
            lineWidth = 2f
            highLightColor = Color.YELLOW
            setDrawValues(false)
            axisDependency =  YAxis.AxisDependency.LEFT
            color = Color.BLACK
            setDrawCircles(false)
            setDrawCircleHole(false)
        }
        val data = LineData(yVals)
        data.setValueTextColor(Color.BLACK)
        data.setValueTextSize(9f)
        return  data
    }

    override fun onValueSelected(e: Entry, h: Highlight) {
        Log.i("Entry selected", e.toString())
    }

    override fun onNothingSelected() {
        Log.i("Nothing selected", "Nothing selected.")
    }
}


data class ResponseData(
    val result: OhlcList
)

data class OhlcList (
    @field:Json(name = "86400")
    var list:List<List<Float>>
)

最後に

なんとか通信の内容をチャートに表示することができました。
kotlinは業務での使用があまりないので、今更な情報が多く思われたら申し訳ございません。
ライブラリ周りはそれぞれの開発環境の違いを多く感じますが、きちんと学んで行けたらと思います。

おすすめ書籍

suzuki

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

最近の投稿

Goの抽象構文木でコードを解析する

はじめに Goでアプリケーショ…

2時間 前

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

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

4週間 前

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

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

1か月 前

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

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

2か月 前