A Haskell client library for the Binance cryptocurrency exchange API. The Binance API is documented here: Binance API documentation. The OpenAPI (Swagger) here: binance.github.io/binance-api-swagger. The library can be found here: maridonkers/binance-api.

Generated code

This code is auto-generated by Haskell-OpenAPI-Client-Code-Generator available on Hackage.

The generator was run as follows:

openapi3-code-generator-exe --use-numbered-variant-constructors --convert-to-camel-case --package-name binance-api --module-name Binance --generate-nix-files spot_api.yaml

Where spot_apy.yml is the file containing the downloaded Binance OpenAPI specification (link is at the top of the binance.github.io/binance-api-swagger page).

Manual changes were required

The generated Haskell code needed some minor manual changing to make it compile.

In the generated Haskell source code I have replaced every first aggTradeM (the ones for capital ‘M’) with aggTradeMM in the src/Binance/Types/AggTrade.hs file, so that it now looks like this:

{-# LANGUAGE MultiWayIf #-}
-- CHANGE WITH CAUTION: This is a generated code file generated by https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator.
{-# LANGUAGE OverloadedStrings #-}

-- | Contains the types generated from the schema AggTrade
module Binance.Types.AggTrade where

import qualified Binance.Common

import Binance.TypeAlias
import qualified Control.Monad.Fail
import qualified Data.Aeson
import qualified Data.Aeson as Data.Aeson.Encoding.Internal
import qualified Data.Aeson as Data.Aeson.Types
import qualified Data.Aeson as Data.Aeson.Types.FromJSON
import qualified Data.Aeson as Data.Aeson.Types.Internal
import qualified Data.Aeson as Data.Aeson.Types.ToJSON
import qualified Data.ByteString.Char8
import qualified Data.ByteString.Char8 as Data.ByteString.Internal
import qualified Data.Foldable
import qualified Data.Functor
import qualified Data.Maybe
import qualified Data.Scientific
import qualified Data.Text
import qualified Data.Text.Internal
import qualified Data.Time.Calendar as Data.Time.Calendar.Days
import qualified Data.Time.LocalTime as Data.Time.LocalTime.Internal.ZonedTime
import qualified GHC.Base
import qualified GHC.Classes
import qualified GHC.Int
import qualified GHC.Show
import qualified GHC.Types
import qualified Prelude as GHC.Integer.Type
import qualified Prelude as GHC.Maybe

-- | Defines the object schema located at @components.schemas.aggTrade@ in the specification.
data AggTrade = AggTrade
  { -- | M: Was the trade the best price match?
    aggTradeMM :: GHC.Types.Bool,
    -- | T: Timestamp
    aggTradeT :: GHC.Types.Bool,
    -- | a: Aggregate tradeId
    aggTradeA :: GHC.Int.Int64,
    -- | f: First tradeId
    aggTradeF :: GHC.Int.Int64,
    -- | l: Last tradeId
    aggTradeL :: GHC.Int.Int64,
    -- | m: Was the buyer the maker?
    aggTradeM :: GHC.Types.Bool,
    -- | p: Price
    aggTradeP :: Data.Text.Internal.Text,
    -- | q: Quantity
    aggTradeQ :: Data.Text.Internal.Text
  }
  deriving
    ( GHC.Show.Show,
      GHC.Classes.Eq
    )

instance Data.Aeson.Types.ToJSON.ToJSON AggTrade where
  toJSON obj = Data.Aeson.Types.Internal.object (Data.Foldable.concat (["M" Data.Aeson.Types.ToJSON..= aggTradeMM obj] : ["T" Data.Aeson.Types.ToJSON..= aggTradeT obj] : ["a" Data.Aeson.Types.ToJSON..= aggTradeA obj] : ["f" Data.Aeson.Types.ToJSON..= aggTradeF obj] : ["l" Data.Aeson.Types.ToJSON..= aggTradeL obj] : ["m" Data.Aeson.Types.ToJSON..= aggTradeM obj] : ["p" Data.Aeson.Types.ToJSON..= aggTradeP obj] : ["q" Data.Aeson.Types.ToJSON..= aggTradeQ obj] : GHC.Base.mempty))
  toEncoding obj = Data.Aeson.Encoding.Internal.pairs (GHC.Base.mconcat (Data.Foldable.concat (["M" Data.Aeson.Types.ToJSON..= aggTradeMM obj] : ["T" Data.Aeson.Types.ToJSON..= aggTradeT obj] : ["a" Data.Aeson.Types.ToJSON..= aggTradeA obj] : ["f" Data.Aeson.Types.ToJSON..= aggTradeF obj] : ["l" Data.Aeson.Types.ToJSON..= aggTradeL obj] : ["m" Data.Aeson.Types.ToJSON..= aggTradeM obj] : ["p" Data.Aeson.Types.ToJSON..= aggTradeP obj] : ["q" Data.Aeson.Types.ToJSON..= aggTradeQ obj] : GHC.Base.mempty)))

instance Data.Aeson.Types.FromJSON.FromJSON AggTrade where
  parseJSON = Data.Aeson.Types.FromJSON.withObject "AggTrade" (\obj -> (((((((GHC.Base.pure AggTrade GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "M")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "T")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "a")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "f")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "l")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "m")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "p")) GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "q"))

-- | Create a new 'AggTrade' with all required fields.
mkAggTrade ::
  -- | 'aggTradeMM'
  GHC.Types.Bool ->
  -- | 'aggTradeT'
  GHC.Types.Bool ->
  -- | 'aggTradeA'
  GHC.Int.Int64 ->
  -- | 'aggTradeF'
  GHC.Int.Int64 ->
  -- | 'aggTradeL'
  GHC.Int.Int64 ->
  -- | 'aggTradeM'
  GHC.Types.Bool ->
  -- | 'aggTradeP'
  Data.Text.Internal.Text ->
  -- | 'aggTradeQ'
  Data.Text.Internal.Text ->
  AggTrade
mkAggTrade aggTradeMM aggTradeT aggTradeA aggTradeF aggTradeL aggTradeM aggTradeP aggTradeQ =
  AggTrade
    { aggTradeMM = aggTradeMM,
      aggTradeT = aggTradeT,
      aggTradeA = aggTradeA,
      aggTradeF = aggTradeF,
      aggTradeL = aggTradeL,
      aggTradeM = aggTradeM,
      aggTradeP = aggTradeP,
      aggTradeQ = aggTradeQ
    }

Usage examples

getSapiV1SystemStatus

{-# LANGUAGE OverloadedStrings #-}

module Example1 where

import qualified Binance
import Control.Monad (when)
import qualified Data.Text as T
import qualified Network.HTTP.Simple as HS

trade :: Bool -> IO ()
trade verbose = do
  ss <- getSystemStatus
  putStrLn $ "Binance API system status: " <> ss
  where
    getSystemStatus :: IO String
    getSystemStatus = do
      resp <- Binance.runWithConfiguration Binance.defaultConfiguration Binance.getSapiV1SystemStatus
      when verbose (print resp)
      pure $
        case HS.getResponseBody resp of
          Binance.GetSapiV1SystemStatusResponse200 status ->
            T.unpack (Binance.getSapiV1SystemStatusResponseBody200Msg status)
              ++ " ("
              ++ show (Binance.getSapiV1SystemStatusResponseBody200Status status)
              ++ ")"
          _ -> "wrong type of response"

Running this example code results in the following output:

Response {responseStatus = Status {statusCode = 200, statusMessage = ""}, responseVersion = HTTP/1.1, responseHeaders = [("Content-Type","application/json;charset=UTF-8"),("Content-Length","27"),("Connection","keep-alive"),("Date","Wed, 16 Nov 2022 20:18:25 GMT"),("Server","nginx"),("X-SAPI-USED-IP-WEIGHT-1M","1"),("Strict-Transport-Security","max-age=31536000; includeSubdomains"),("X-Frame-Options","SAMEORIGIN"),("X-Xss-Protection","1; mode=block"),("X-Content-Type-Options","nosniff"),("Content-Security-Policy","default-src 'self'"),("X-Content-Security-Policy","default-src 'self'"),("X-WebKit-CSP","default-src 'self'"),("Cache-Control","no-cache, no-store, must-revalidate"),("Pragma","no-cache"),("Expires","0"),("Access-Control-Allow-Origin","*"),("Access-Control-Allow-Methods","GET, HEAD, OPTIONS"),("X-Cache","Miss from cloudfront"),("Via","1.1 4ce5e5162c2d4fc9022ceb290f794ffe.cloudfront.net (CloudFront)"),("X-Amz-Cf-Pop","AMS1-C1"),("X-Amz-Cf-Id","V8g__abrKZMYn3os-dRPOuJK3964z28Gk9VrOnh-l6rK4cHK3uRZ8g==")], responseBody = GetSapiV1SystemStatusResponse200 (GetSapiV1SystemStatusResponseBody200 {getSapiV1SystemStatusResponseBody200Msg = "normal", getSapiV1SystemStatusResponseBody200Status = 0}), responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose, responseOriginalRequest = Request {
  host                 = "api.binance.com"
  port                 = 443
  secure               = True
  requestHeaders       = [("User-Agent","Binance Public Spot API openapi3-code-generator/0.1.0.7 (https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator)")]
  path                 = "/sapi/v1/system/status"
  queryString          = ""
  method               = "GET"
  proxy                = Nothing
  rawBody              = False
  redirectCount        = 10
  responseTimeout      = ResponseTimeoutDefault
  requestVersion       = HTTP/1.1
  proxySecureMode      = ProxySecureWithConnect
}
}
Binance API system status: normal (0)

getApiV3Klines

{-# LANGUAGE OverloadedStrings #-}

module Example2 where

import qualified Binance
import Control.Monad (when)
import Data.Either ()
import qualified Data.Text as T
import qualified Network.HTTP.Simple as HS

defaultConf = Binance.defaultConfiguration

config = Binance.apiKeyInHeaderAuthenticationSecurityScheme "your-api-key-goes-here"

conf =
  defaultConf
    { Binance.configSecurityScheme = config
    }

trade :: Bool -> IO ()
trade verbose = do
  kl <- getKlines
  putStr "Binance API klines "
  case kl of
    Left e -> putStrLn $ "*** " <> e
    Right kls -> do
      let l = length kls
      putStrLn $ "(" <> show l <> ")"
      putStrLn "[ Kline open time, Open price, High price, Low price, Close price, Volume, Kline Close time, Quote asset volume, Number of trades, Taker buy base asset volume, Taker buy quote asset volume, Unused field (ignore) ]"
      mapM_ (\(i, ks) -> putStrLn $ show i <> ": [ " <> printKline ks <> " ]") $ zip [1 ..] kls
  where
    printKline = foldl (\acc c -> acc <> (if null acc then "" else ", ") <> kLineToString c) ""
      where
        kLineToString k = case k of
          Binance.GetApiV3KlinesResponseBody200Variant1 s -> show s
          Binance.GetApiV3KlinesResponseBody200Variant2 s -> T.unpack s

    getKlines = do
      resp <-
        Binance.runWithConfiguration conf $
          Binance.getApiV3Klines $
            Binance.mkGetApiV3KlinesParameters Binance.GetApiV3KlinesParametersQueryIntervalEnum1m "BTCBUSD"
      pure $
        case HS.getResponseBody resp of
          Binance.GetApiV3KlinesResponse200 klines -> Right klines
          _ -> Left "wrong type of response"

Running this example code results in the following output (excerpt):

Binance API klines (500)
[ Kline open time, Open price, High price, Low price, Close price, Volume, Kline Close time, Quote asset volume, Number of trades, Taker buy base asset volume, Taker buy quote asset volume, Unused field (ignore) ]
1: [ 1668674700000, 16579.28000000, 16580.95000000, 16572.00000000, 16576.36000000, 55.90257000, 1668674759999, 926698.28532810, 1124, 25.76195000, 427075.54336190, 0 ]
2: [ 1668674760000, 16576.36000000, 16580.35000000, 16567.78000000, 16570.00000000, 47.20438000, 1668674819999, 782390.60662060, 780, 19.95414000, 330747.43392430, 0 ]
3: [ 1668674820000, 16570.00000000, 16574.23000000, 16565.37000000, 16568.04000000, 44.22206000, 1668674879999, 732752.61122150, 906, 26.58506000, 440518.72028710, 0 ]
...
497: [ 1668704460000, 16643.01000000, 16670.00000000, 16639.54000000, 16660.34000000, 260.66434000, 1668704519999, 4341628.84450980, 3393, 140.34795000, 2337813.95373040, 0 ]
498: [ 1668704520000, 16658.53000000, 16677.40000000, 16658.52000000, 16673.91000000, 189.25594000, 1668704579999, 3154901.49785200, 3246, 107.74395000, 1796194.88950920, 0 ]
499: [ 1668704580000, 16673.86000000, 16686.16000000, 16662.65000000, 16670.89000000, 97.68432000, 1668704639999, 1628707.62788410, 2400, 46.47844000, 775010.42178890, 0 ]
500: [ 1668704640000, 16669.80000000, 16678.15000000, 16668.27000000, 16668.27000000, 27.51324000, 1668704699999, 458747.76417530, 778, 12.29112000, 204949.28881110, 0 ]

Other examples

Another example of using generated code — not Binance API related — can be found here: Haskell-OpenAPI-Code-Generator/Stripe-Haskell-Library/example.

Disclaimer

I have not yet tested this client library myself — aside from the small examples given above — so no guarantees are given at all! Use this software at your own risk — caveat emptor!