packages feed

gemini-exports-0.1.0.0: src/Console/Gemini/Exports/Csv.hs

{-# LANGUAGE RecordWildCards #-}
{-| Types & functions for converting Gemini API responses into CSV exports.
-}
module Console.Gemini.Exports.Csv
    ( ExportData(..)
    , makeExportData
    , makeExportCsv
    , ExportLine(..)
    , getExportLineTimestamp
    ) where
import           Control.Applicative            ( (<|>) )
import           Control.Monad.IO.Class         ( MonadIO(..) )
import           Data.Csv                       ( (.=)
                                                , DefaultOrdered(..)
                                                , ToNamedRecord(..)
                                                , defaultEncodeOptions
                                                , encUseCrLf
                                                , encodeDefaultOrderedByNameWith
                                                , header
                                                , namedRecord
                                                )
import           Data.Maybe                     ( fromMaybe )
import           Data.Scientific                ( FPFormat(Fixed)
                                                , Scientific
                                                , formatScientific
                                                )
import           Data.Text                      ( Text
                                                , empty
                                                , pack
                                                )
import           Data.Time                      ( TimeZone
                                                , defaultTimeLocale
                                                , formatTime
                                                , getTimeZone
                                                , utcToZonedTime
                                                )
import           Data.Time.Clock.POSIX          ( POSIXTime
                                                , posixSecondsToUTCTime
                                                )

import           Web.Gemini

import qualified Data.ByteString.Lazy.Char8    as LBS


-- | The data required for rendering a single CSV row.
data ExportData = ExportData
    { edTZ   :: TimeZone
    , edLine :: ExportLine
    }
    deriving (Show, Read, Eq, Ord)

instance DefaultOrdered ExportData where
    headerOrder _ = header
        [ "time"
        , "base-asset"
        , "quote-asset"
        , "type"
        , "description"
        , "price"
        , "quantity"
        , "total"
        , "fee"
        , "fee-currency"
        , "trade-id"
        ]

instance ToNamedRecord ExportData where
    toNamedRecord (ExportData tz lineData) = namedRecord $ case lineData of
        TradeExport Trade {..} SymbolDetails {..} ->
            [ "time" .= formatTimestamp tTimestamp
            , "base-asset" .= sdBaseCurrency
            , "quote-asset" .= sdQuoteCurrency
            , "type" .= if tIsBuy then "Buy" else ("Sell" :: Text)
            , "description" .= empty
            , "price" .= formatDecimal tPrice
            , "quantity" .= formatDecimal tAmount
            , "total" .= formatDecimal (tPrice * tAmount)
            , "fee" .= formatDecimal tFeeAmount
            , "fee-currency" .= tFeeCurrency
            , "trade-id" .= tId
            ]
        TransferExport Transfer {..} ->
            [ "time" .= formatTimestamp trTimestamp
            , "base-asset" .= trCurrency
            , "quote-asset" .= empty
            , "type" .= trType
            , "description" .= toDescr (trMethod, trPurpose)
            , "price" .= empty
            , "quantity" .= formatDecimal trAmount
            , "total" .= formatDecimal trAmount
            , "fee" .= empty
            , "fee-currency" .= empty
            , "trade-id" .= trId
            ]
        EarnExport EarnTransaction {..} ->
            [ "time" .= formatTimestamp etTimestamp
            , "base-asset" .= etAmountCurrency
            , "quote-asset" .= fromMaybe empty etPriceCurrency
            , "type" .= ("Earn " <> etType)
            , "description" .= empty
            , "price" .= maybe empty formatDecimal etPrice
            , "quantity" .= formatDecimal etAmount
            , "total" .= maybe empty (formatDecimal . (* etAmount)) etPrice
            , "fee" .= empty
            , "fee-currency" .= empty
            , "trade-id" .= etId
            ]
      where
        -- Render a transfer description by combining the optional method
        -- & purpose fields.
        toDescr :: (Maybe Text, Maybe Text) -> Text
        toDescr = \case
            (Just m, Just p) -> m <> " " <> p
            (m     , p     ) -> fromMaybe "" $ m <|> p
        -- Convert a timestamp into a localtime with the line's timezone
        -- & render it in `YYYY-MM-DD HH:MM:SS.nnnnnnnnn` format.`
        formatTimestamp :: POSIXTime -> String
        formatTimestamp =
            formatTime defaultTimeLocale "%F %T%Q"
                . utcToZonedTime tz
                . posixSecondsToUTCTime
        -- Render a decimal number with the minimum precision neccesary..
        formatDecimal :: Scientific -> Text
        formatDecimal = pack . formatScientific Fixed Nothing

-- | Determine the 'TimeZone' for the 'ExportLine' & return both as an
-- 'ExportData'.
makeExportData :: MonadIO m => ExportLine -> m ExportData
makeExportData lineData = do
    tz <- liftIO . getTimeZone . posixSecondsToUTCTime $ getExportLineTimestamp
        lineData
    return $ ExportData tz lineData

-- | Render the export data as a CSV with a header row.
makeExportCsv :: [ExportData] -> LBS.ByteString
makeExportCsv =
    encodeDefaultOrderedByNameWith (defaultEncodeOptions { encUseCrLf = False })


-- | Split out the data required for different export line types.
data ExportLine
    = TradeExport Trade SymbolDetails
    | TransferExport Transfer
    | EarnExport EarnTransaction
    deriving (Show, Read, Eq, Ord)

-- | Get the timestamp field of an 'ExportLine'.
getExportLineTimestamp :: ExportLine -> POSIXTime
getExportLineTimestamp = \case
    TradeExport t _  -> tTimestamp t
    TransferExport t -> trTimestamp t
    EarnExport     t -> etTimestamp t