長いですがタイトルそのままの内容ですw
coincheckから約定履歴を取得する
coincheck は Bitcoin の取引所で、ありがたいことに多機能なAPIを公開されています。今回はその中でも認証なしで使える約定履歴のAPIにアクセスして直近の取引額を取得してみます。APIクライアントにはServantを使います。
data Trade = Trade
{ amount :: String
, rate :: Int
, order_type :: String
, created_at :: String
} deriving Generic
instance FromJSON Trade
Trade
は約定履歴のAPIで返却されるデータ構造です。これを使ってAPIを定義します。
type CoincheckAPI = "api" :> "trades" :> Get '[JSON] [Trade]
coincheckAPI :: Proxy CoincheckAPI
coincheckAPI = Proxy
今回は約定履歴しか使わないのでこれで十分です。他のAPIを使いたくなっても:<|>
で追加していけばいいだけです。APIを定義したのでクライアントを作りましょう。
getTrades :: EitherT ServantError IO [Trade]
getTrades = client coincheckAPI $ BaseUrl Https "coincheck.jp" 443
冗談のように短いですがこれで終わりです。
Webサーバーで画像を返却する
servant-JuicyPixelsを使います。
type API = "trades.png" :> Get '[PNG] DynamicImage
api :: Proxy API
api = Proxy
これで GET /trades.png
にアクセスすればpng画像が返ってくるはずです。
グラフ画像を生成する
いよいよ一番実装しなければいけないところです。グラフの描画にはChartを使います。
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
を利用して変換しています。あとはtoFile
でグラフを画像ファイルに書き出してreadImage
で書きだしたファイルを読み込んでDynamicImage
にしています。実はここはファイルにせずに直接DynamicImage
に変換できればよかったのですがChart
を使ってどのようにすればわからなかったので知っている人がいればコメント等でアドバイスいただければ幸いです。
あとは実行あるのみです。
main :: IO ()
main = do
putStrLn "Listening on 8080"
Warp.run 8080 $ serve api server
これで http://localhost:8080/trades.png にアクセスすると
このようにグラフ画像が表示されるはずです
最後に 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
{-# 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