msawady’s engineering-note

なにも分からないエンジニアです。

【Scala】Akka を利用して Alpaca API から米国株の日足データを取得する

Alpaca API から米国株の日足データを取得する

  • Alpaca API というものを見つけました

alpaca.markets

  • 無料で米国株のシミュレーショントレードを行えるAPIです
  • チャートデータなども取得出来ます
  • Scala で触りながら遊んでみたいと思います
    • Python の方が手っ取り早いんですが、久しぶりに Scala を書きたくなったので。

Outline

  • JSON ファイルの読み込み
  • Akka を利用した API リクエスト
  • JSON -> case class へのマッピング

JSON ファイルの読み込み

アカウントを作成して、API Key を取得します。リクエストヘッダーには API Key を書いてあげる必要があります。
今回は以下のようなJSON形式の設定ファイルに記述してsrc/main/resourece/ に置きます。 (API Key は無効化済みです。念の為。)

{
  "endpoint": "https://data.alpaca.markets/v1/",
  "keyId": "PK3J0PD7T13S25SE2EDI",
  "secretKey": "7PR6ABecF3MC3QXjH3l2fu5hPML84ZE9QkKxYMDD"
}

JSON を読み込むために、play-jsonを利用します。

build.sbt

build.sbtlibraryDependencies に以下を追加して

"com.typesafe.play" %% "play-json" % "2.7.2"

src/main/resourece/settings.json を読み込みます。

    val settings: JsValue = Json.parse(Source.fromResource("settings.json").getLines().mkString)
    val url: String = (settings \ "endpoint").as[String]
    val keyId: String = (settings \ "keyId").as[String]
    val secretKey: String = (settings \ "secretKey").as[String]
  • Source.fromResource()src/main/resources 配下のファイルにアクセス出来ます。
  • play-jsonの記法で (settings \ "endpoint").as[String]とすることで "endpoint" の value を String で取得できます。

Akka を利用した API リクエスト

設定ファイルから読み込んだ情報をもとにAPIリクエストを行います。 リクエストのライブラリにはAkka-HTTPを利用します。
fetchChart が main の API 処理です。

package apiclient

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model.{HttpMethods, HttpRequest, Uri}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import akka.util.Timeout
import domain.Bar
import play.api.libs.json.{JsValue, Json}

import scala.concurrent.Await
import scala.concurrent.duration._


class ApiClient(url: String, keyId: String, secretKey: String) {
  def apply(url: String, keyId: String, secretKey: String): ApiClient = new ApiClient(url, keyId, secretKey)

  implicit val timeout = Timeout(5 second)
  implicit val system = ActorSystem()
  implicit val materialize = ActorMaterializer()
  implicit val executionContext = system.dispatcher

  def fetchDailyChart(tickers: Seq[String]): Map[String, List[Bar]] = {

    val params = "symbols=" + tickers.mkString(",")
    val jsValue = fetchChart("/bars/1D", params)
    tickers.map(t => (
      t,
      (jsValue \ t).as[List[JsValue]].map(v => Bar.apply(t, v))
    )).toMap
  }

  private def fetchChart(path: String, params: String): JsValue = {

    val uri = Uri(this.url + path).withRawQueryString(params)
    val h1: RawHeader = RawHeader("APCA-API-KEY-ID", this.keyId)
    val h2: RawHeader = RawHeader("APCA-API-SECRET-KEY", this.secretKey)

    val req = HttpRequest(HttpMethods.GET, uri = uri).withHeaders(List(h1, h2))
    val res = Await.result(Http().singleRequest(req), timeout.duration)
    val body = Unmarshal(res.entity).to[String]
 
    Await.result(Http().shutdownAllConnectionPools(), timeout.duration)
    Await.result(system.terminate(), timeout.duration)

    Json.parse(Await.result(body, timeout.duration))
  }
}
    val params = "symbols=" + tickers.mkString(",")
    val jsValue = fetchChart("/bars/1D", params)
  • API ドキュメント にしたがって、対象銘柄の ticker をカンマ区切りの文字列にします
  • 日足のチャートなのでbars/1Dにアクセスします
  implicit val timeout = Timeout(5 second)
  implicit val system = ActorSystem()
  implicit val materialize = ActorMaterializer()
  implicit val executionContext = system.dispatcher

  private def fetchChart(path: String, params: String): JsValue = {

    val uri = Uri(this.url + path).withRawQueryString(params)
    val h1: RawHeader = RawHeader("APCA-API-KEY-ID", this.keyId)
    val h2: RawHeader = RawHeader("APCA-API-SECRET-KEY", this.secretKey)

    val req = HttpRequest(HttpMethods.GET, uri = uri).withHeaders(List(h1, h2))
    val res = Await.result(Http().singleRequest(req), timeout.duration)
    val body = Unmarshal(res.entity).to[String]

    Await.result(Http().shutdownAllConnectionPools(), timeout.duration)
    Await.result(system.terminate(), timeout.duration)

    Json.parse(Await.result(body, timeout.duration))
  }

JSON -> case class へのマッピング

レスポンスのJSONをBar(いわゆるローソク足ですね)オブジェクトにマッピングします。
以下のようなコンパニオンオブジェクトとして、JsValueからの apply メソッドを定義することで、レスポンスからのマッピングをスマートに出来るようにします。

package domain

import org.joda.time.DateTime
import play.api.libs.json.JsValue

case class Bar(
                ticker: String,
                close: BigDecimal,
                open: BigDecimal,
                high: BigDecimal,
                low: BigDecimal,
                volume: BigDecimal,
                time: DateTime
              )

object Bar {
  def apply(
             ticker: String,
             close: BigDecimal,
             open: BigDecimal,
             high: BigDecimal,
             low: BigDecimal,
             volume: BigDecimal,
             time: DateTime
           ): Bar = new Bar(ticker, close, open, high, low, volume, time)

  def apply(ticker: String, jsValue: JsValue): Bar = {
    val close = (jsValue \ "c").as[BigDecimal]
    val open = (jsValue \ "o").as[BigDecimal]
    val high = (jsValue \ "h").as[BigDecimal]
    val low = (jsValue \ "l").as[BigDecimal]
    val volume = (jsValue \ "v").as[BigDecimal]
    val time = new DateTime((jsValue \ "t").as[Long] * 1000L)
    new Bar(ticker, close, open, high, low, volume, time)
  }
}

レスポンスからのマッピングは以下のように行って、[tickercode, List[Bar]]の Map にします。

    val jsValue = fetchChart("/bars/1D", params)
    tickers.map(t => (
      t,
      (jsValue \ t).as[List[JsValue]].map(v => Bar.apply(t, v))
    )).toMap

実際にやってみる

Mainから、以下のように実行します。

    val cli = new ApiClient(url, keyId, secretKey)
    pprint.pprintln(cli.fetchDailyChart(Seq("AAPL")))
  "com.lihaoyi" %% "pprint" % "0.5.3"

以下のようにコンソール出力されます。 f:id:msawady:20190515063825p:plain

終わりに

  • コードは GitHub にupload しています。 github.com

  • Scala は書きやすいですね。型推論が賢いので冗長な型宣言が少なく、サクサク書けて快適です。

  • ちょっとコードというか、構造がイマイチなので要リファクタですね
  • テクニカル分析とかもやってみたいなと思います