coincheckの約定履歴をグラフにしてWebから画像で取得するサーバーを書いてみた

長いですがタイトルそのままの内容ですｗ

coincheckから約定履歴を取得する
=============================

coincheck は Bitcoin の取引所で、ありがたいことに[多機能なAPI](https://coincheck.jp/documents/exchange/api)を公開されています。今回はその中でも認証なしで使える[約定履歴のAPI](https://coincheck.jp/documents/exchange/api#public-trades)にアクセスして直近の取引額を取得してみます。APIクライアントにはServantを使います。

```hs
data Trade = Trade
  { amount     :: String
  , rate       :: Int
  , order_type :: String
  , created_at :: String
  } deriving Generic

instance FromJSON Trade
```

`Trade`は約定履歴のAPIで返却されるデータ構造です。これを使ってAPIを定義します。

```hs
type CoincheckAPI = "api" :> "trades" :> Get '[JSON] [Trade]

coincheckAPI :: Proxy CoincheckAPI
coincheckAPI = Proxy
```

今回は約定履歴しか使わないのでこれで十分です。他のAPIを使いたくなっても`:<|>`で追加していけばいいだけです。APIを定義したのでクライアントを作りましょう。

```hs
getTrades :: EitherT ServantError IO [Trade]
getTrades = client coincheckAPI $ BaseUrl Https "coincheck.jp" 443
```

冗談のように短いですがこれで終わりです。

Webサーバーで画像を返却する
=========================

[servant-JuicyPixels](https://hackage.haskell.org/package/servant-JuicyPixels)を使います。

```hs
type API = "trades.png" :> Get '[PNG] DynamicImage

api :: Proxy API
api = Proxy
```

これで `GET /trades.png` にアクセスすればpng画像が返ってくるはずです。

グラフ画像を生成する
==================

いよいよ一番実装しなければいけないところです。グラフの描画には[Chart](http://hackage.haskell.org/package/Chart)を使います。

```hs
server :: Server API
server = do
  trades <- bimapEitherT (const err500) id getTrades
  image <- io $ do
    let imagePath = "trades.png"
    toFile def imagePath $ plot (line "price" $ [[(parseCreatedAt t, rate t) | t <- trades]])
    readImage imagePath
  either (const $ left err500) pure image
    where
      parseCreatedAt :: Trade -> LocalTime
      parseCreatedAt trade = parseTimeOrError True defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S.000Z")) (created_at trade)
```

ポイントは Servant Client から Servant Server への変換だと思います。型的には `EitherT ServantError IO a -> EitherT ServantErr IO a`という変換になっていて、ここでは[`bimapEitherT :: Functor m => (e -> f) -> (a -> b) -> EitherT e m a -> EitherT f m b`](http://hackage.haskell.org/package/either/docs/Control-Monad-Trans-Either.html#v:bimapEitherT)を利用して変換しています。あとは`toFile`でグラフを画像ファイルに書き出して`readImage`で書きだしたファイルを読み込んで`DynamicImage`にしています。実はここはファイルにせずに直接`DynamicImage`に変換できればよかったのですが`Chart`を使ってどのようにすればわからなかったので知っている人がいればコメント等でアドバイスいただければ幸いです。

あとは実行あるのみです。

```hs
main :: IO ()
main = do
  putStrLn "Listening on 8080"
  Warp.run 8080 $ serve api server
```

これで <http://localhost:8080/trades.png> にアクセスすると

![](https://i.gyazo.com/c7a106cd2042a86483b936e72eee4920.png)

このようにグラフ画像が表示されるはずです :exclamation: 

最後に `cabal` ファイルと全てのコードを載せておきます。

```hs:coincheck-trades-image-server.cabal
name:                coincheck-trades-image-server
version:             0.1.0.0
synopsis:            Initial project template from stack
license:             BSD3
build-type:          Simple
cabal-version:       >=1.10

executable coincheck-trades-image-server-exe
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , time
                     , transformers
                     , either
                     , aeson
                     , servant-server
                     , servant-client
                     , Chart
                     , Chart-cairo
                     , JuicyPixels
                     , servant-JuicyPixels
                     , warp
  default-language:    Haskell2010
```

```hs:app/Main.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}

module Main where

import Codec.Picture
import Control.Monad.Trans.Either
import Control.Monad.IO.Class
import Data.Aeson
import Data.Time
import Graphics.Rendering.Chart.Easy
import Graphics.Rendering.Chart.Backend.Cairo
import GHC.Generics
import qualified Network.Wai.Handler.Warp as Warp
import Servant
import Servant.Client
import Servant.JuicyPixels

data Trade = Trade
  { amount     :: String
  , rate       :: Int
  , order_type :: String
  , created_at :: String
  } deriving Generic

instance FromJSON Trade

type CoincheckAPI = "api" :> "trades" :> Get '[JSON] [Trade]

coincheckAPI :: Proxy CoincheckAPI
coincheckAPI = Proxy

getTrades :: EitherT ServantError IO [Trade]
getTrades = client coincheckAPI $ BaseUrl Https "coincheck.jp" 443

type API = "trades.png" :> Get '[PNG] DynamicImage

api :: Proxy API
api = Proxy

server :: Server API
server = do
  trades <- bimapEitherT (const err500) id getTrades
  image <- liftIO $ do
    let imagePath = "trades.png"
    toFile def imagePath $ plot (line "price" $ [[(parseCreatedAt t, rate t) | t <- trades]])
    readImage imagePath
  either (const $ left err500) pure image
    where
      parseCreatedAt :: Trade -> LocalTime
      parseCreatedAt trade = parseTimeOrError True defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S.000Z")) (created_at trade)

main :: IO ()
main = do
  putStrLn "Listening on 8080"
  Warp.run 8080 $ serve api server
```