packages feed

cointracking-imports (empty) → 0.1.0.0

raw patch · 8 files changed

+555/−0 lines, 8 filesdep +basedep +base-compat-batteriesdep +bytestringsetup-changed

Dependencies added: base, base-compat-batteries, bytestring, cassava, cointracking-imports, filepath, hedgehog, lens, scientific, tasty, tasty-hedgehog, tasty-hunit, text, time, xlsx

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# CHANGELOG++## v0.1.0.0++* Initial release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Pavan Rikhi (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Pavan Rikhi nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,40 @@+# cointracking-imports++[![cointracking-imports Build Status](https://github.com/prikhi/cointracking-imports/actions/workflows/main.yml/badge.svg)](https://github.com/prikhi/cointracking-imports/actions/workflows/main.yml)+++A Haskell library for generating CSV & XLSX files for importing into+[CoinTracking][cointracking]. Build a `[CTImportData]` & write to a file with+`writeImportDataToFile`.++Requires [`stack`][get-stack].++[cointracking]: https://cointracking.info/+[get-stack]: https://docs.haskellstack.org/en/stable/README/+++## Build++You can build the project with stack:++```sh+stack build+```++For development, you can enable fast builds with file-watching,+documentation-building, & test-running:++```sh+stack test --haddock --fast --file-watch --pedantic+```++To build & open the documentation, run:++```sh+stack haddock --open cointracking-imports+```+++## LICENSE++BSD-3
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cointracking-imports.cabal view
@@ -0,0 +1,106 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name:           cointracking-imports+version:        0.1.0.0+synopsis:       Generate CSV & XLSX files for importing into CoinTracking.+description:    @cointracking-imports@ is a library for generating CSV & XLSX files to use+                with <https://cointracking.info CoinTracking>'s Import feature. You+                construct a list of @CTImportData@, then feed those to our CSV/XLSX+                generating functions.+                .+                E.g., if you generate a list of your BNB staking rewards, you can use this+                library build a CSV file to import those rewards into CoinTracking.+category:       Web, Finance+homepage:       https://github.com/prikhi/cointracking-imports#readme+bug-reports:    https://github.com/prikhi/cointracking-imports/issues+author:         Pavan Rikhi+maintainer:     pavan.rikhi@gmail.com+copyright:      2021 Pavan Rikhi+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/prikhi/cointracking-imports++library+  exposed-modules:+      Web.CoinTracking.Imports+      Web.CoinTracking.Imports.Types+  other-modules:+      Paths_cointracking_imports+  hs-source-dirs:+      src+  default-extensions:+      DeriveGeneric+      LambdaCase+      NamedFieldPuns+      OverloadedStrings+      TupleSections+      TypeApplications+      TypeOperators+      ViewPatterns+  ghc-options: -Wall+  build-depends:+      base >=4.3 && <5+    , base-compat-batteries+    , bytestring <1+    , cassava <1+    , filepath ==1.*+    , lens >=3.0.4 && <6+    , scientific <1+    , text <2+    , time <2+    , xlsx <1+  if impl(ghc >= 8.0)+    ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints+  else+    ghc-options: -fwarn-incomplete-record-updates -fwarn-incomplete-uni-patterns+  default-language: Haskell2010++test-suite cointracking-imports-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_cointracking_imports+  hs-source-dirs:+      tests+  default-extensions:+      DeriveGeneric+      LambdaCase+      NamedFieldPuns+      OverloadedStrings+      TupleSections+      TypeApplications+      TypeOperators+      ViewPatterns+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts "-N -T"+  build-depends:+      base >=4.3 && <5+    , base-compat-batteries+    , bytestring <1+    , cassava <1+    , cointracking-imports+    , filepath ==1.*+    , hedgehog+    , lens >=3.0.4 && <6+    , scientific <1+    , tasty+    , tasty-hedgehog+    , tasty-hunit+    , text <2+    , time <2+    , xlsx <1+  if impl(ghc >= 8.0)+    ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints+  else+    ghc-options: -fwarn-incomplete-record-updates -fwarn-incomplete-uni-patterns+  default-language: Haskell2010
+ src/Web/CoinTracking/Imports.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE RecordWildCards #-}+{-| Generate CSV & XLSX files to use with CoinTracking's import feature.++-}+module Web.CoinTracking.Imports+    ( writeImportDataToFile+      -- * Types+    , module Web.CoinTracking.Imports.Types+      -- * CSV Import Files+    , coinTrackingCsvImport+    , headerRow+    , csvEncodingOptions+      -- * XLSX Import Files+    , coinTrackingXlsxImport+    , writeXlsxHeader+    , writeXlsxRow+    ) where++import           Codec.Xlsx                     ( CellValue(..)+                                                , DateBase(DateBase1900)+                                                , Worksheet+                                                , atSheet+                                                , cellValueAt+                                                , dateToNumber+                                                , def+                                                , fromXlsx+                                                )+import           Control.Lens                   ( (.~)+                                                , (?~)+                                                )+import           Data.Char                      ( toLower )+import           Data.Csv                       ( EncodeOptions(..)+                                                , defaultEncodeOptions+                                                , encodeWith+                                                )+import           Data.Foldable                  ( foldl' )+import           Data.Function                  ( (&) )+import           Data.Scientific                ( toRealFloat )+import           Data.Time                      ( zonedTimeToUTC )+import           Data.Time.Clock.POSIX          ( POSIXTime+                                                , getPOSIXTime+                                                )+import           System.FilePath                ( takeExtension )++import           Web.CoinTracking.Imports.Types++import qualified Data.ByteString.Lazy          as LBS+import qualified Data.ByteString.Lazy.Char8    as LBC+import qualified Data.Text                     as T+++-- | Write the given data to a file. If the file extension is @.xlsx@ or+-- @.xls@, we write a spreadsheet. Otherwise we write a CSV.+writeImportDataToFile :: FilePath -> [CTImportData] -> IO ()+writeImportDataToFile file xs = do+    currentTime <- getPOSIXTime+    let extension = takeExtension file+        output    = if map toLower extension `elem` [".xlsx", ".xls"]+            then coinTrackingXlsxImport currentTime xs+            else coinTrackingCsvImport xs+    LBC.writeFile file output+++-- CSVs++-- | Generate the CoinTracking CSV Import for the data, prepended by+-- a header row.+coinTrackingCsvImport :: [CTImportData] -> LBS.ByteString+coinTrackingCsvImport = (headerRow <>) . encodeWith csvEncodingOptions++-- | The CSV header row to prepend to the generated output.+headerRow :: LBS.ByteString+headerRow = encodeWith+    csvEncodingOptions+    [ [ "Type" :: T.Text+      , "Buy"+      , "Cur."+      , "Sell"+      , "Cur."+      , "Fee"+      , "Cur."+      , "Exchange"+      , "Trade-Group"+      , "Comment"+      , "Date"+      , "Tx-ID"+      , "Buy Value in your Account Currency"+      , "Sell Value in your Account Currency"+      ]+    ]++-- | 'defaultEncodeOptions', but with newline-only line endings.+csvEncodingOptions :: EncodeOptions+csvEncodingOptions = defaultEncodeOptions { encUseCrLf = False }+++-- XLSXs++-- | Generate an XLSX file containing the expected headers rows and the+-- import data.+coinTrackingXlsxImport+    :: POSIXTime+    -- ^ Creation time to embed in the spreadsheet.+    -> [CTImportData]+    -> LBS.ByteString+coinTrackingXlsxImport createdTime rows =+    let sheet = ixFoldl+            (\sheet_ rowNum row -> writeXlsxRow sheet_ (rowNum + 3) row)+            (writeXlsxHeader def)+            rows+        book = def & atSheet "Sheet1" ?~ sheet+    in  fromXlsx createdTime book+  where+    -- | Indexed fold from the left.+    ixFoldl :: (b -> Int -> a -> b) -> b -> [a] -> b+    ixFoldl f initial =+        fst . foldl' (\(b, i) a -> (f b i a, i + 1)) (initial, 0)++-- | Write the standard CoinTracking header to the first two rows of the+-- worksheet.+writeXlsxHeader :: Worksheet -> Worksheet+writeXlsxHeader sheet =+    sheet+        &  cellValueAt (1, 1)+        ?~ CellText+               "CoinTracking Excel Import data (see docs: https://cointracking.info/import/import_xls/)"+        &  writeColumn 1  "Type"+        &  writeColumn 2  "Buy Amount"+        &  writeColumn 3  "Buy Cur."+        &  writeColumn 4  "Sell Amount"+        &  writeColumn 5  "Sell Cur."+        &  writeColumn 6  "Feel Amount"+        &  writeColumn 7  "Fee Cur."+        &  writeColumn 8  "Exchange"+        &  writeColumn 9  "Trade Group"+        &  writeColumn 10 "Comment"+        &  writeColumn 11 "Date"+  where+    writeColumn :: Int -> T.Text -> Worksheet -> Worksheet+    writeColumn c t s = s & cellValueAt (2, c) ?~ CellText t+++-- | Write a 'CTImportData' to the given row(1-indexed) of the worksheet.+writeXlsxRow :: Worksheet -> Int -> CTImportData -> Worksheet+writeXlsxRow sheet row CTImportData {..} =+    sheet+        & (cellValueAt (row, 1) ?~ CellText (renderTransactionType ctidType))+        & (cellValueAt (row, 2) .~ renderAmount ctidBuy)+        & (cellValueAt (row, 3) .~ renderCurrency ctidBuy)+        & (cellValueAt (row, 4) .~ renderAmount ctidSell)+        & (cellValueAt (row, 5) .~ renderCurrency ctidSell)+        & (cellValueAt (row, 6) .~ renderAmount ctidFee)+        & (cellValueAt (row, 7) .~ renderCurrency ctidFee)+        & (cellValueAt (row, 8) ?~ CellText ctidExchange)+        & (cellValueAt (row, 9) ?~ CellText ctidGroup)+        & (cellValueAt (row, 10) ?~ CellText ctidComment)+        & (cellValueAt (row, 11) ?~ renderedDate)+  where+    renderAmount :: Maybe Amount -> Maybe CellValue+    renderAmount = fmap (CellDouble . toRealFloat . aAmount)+    renderCurrency :: Maybe Amount -> Maybe CellValue+    renderCurrency = fmap (CellText . cTicker . aCurrency)+    renderedDate :: CellValue+    renderedDate =+        CellDouble $ dateToNumber DateBase1900 $ zonedTimeToUTC ctidDate
+ src/Web/CoinTracking/Imports/Types.hs view
@@ -0,0 +1,173 @@+{-| Type definitions, instances, & utility functions for CoinTracking+imports.++-}+{-# LANGUAGE RecordWildCards #-}+module Web.CoinTracking.Imports.Types+    ( CTImportData(..)+    , CTTransactionType(..)+    , renderTransactionType+    , Amount(..)+    , Currency(..)+    ) where++import           Data.Csv                       ( Field+                                                , ToField(..)+                                                , ToRecord(..)+                                                , record+                                                )+import           Data.Scientific                ( FPFormat(Fixed)+                                                , Scientific+                                                , formatScientific+                                                )+import           Data.String                    ( IsString )+import           Data.Time                      ( ZonedTime+                                                , defaultTimeLocale+                                                , formatTime+                                                )+import           GHC.Generics                   ( Generic )++import qualified Data.Text                     as T++-- | Represents a single row in an export.+data CTImportData = CTImportData+    { ctidType      :: CTTransactionType+    , ctidBuy       :: Maybe Amount+    , ctidSell      :: Maybe Amount+    , ctidFee       :: Maybe Amount+    , ctidExchange  :: T.Text+    , ctidGroup     :: T.Text+    , ctidComment   :: T.Text+    , ctidDate      :: ZonedTime+    , ctidTradeId   :: T.Text+    , ctidBuyValue  :: Maybe Amount+    , ctidSellValue :: Maybe Amount+    }+    deriving (Show, Read, Generic)++instance ToRecord CTImportData where+    toRecord CTImportData {..} = record+        [ toField ctidType+        , orBlank renderAmount   ctidBuy+        , orBlank renderCurrency ctidBuy+        , orBlank renderAmount   ctidSell+        , orBlank renderCurrency ctidSell+        , orBlank renderAmount   ctidFee+        , orBlank renderCurrency ctidFee+        , toField ctidExchange+        , toField ctidGroup+        , toField ctidComment+        , toField $ formatTime defaultTimeLocale "%F %T" ctidDate+        , toField ctidTradeId+        , orBlank renderAmount ctidBuyValue+        , orBlank renderAmount ctidSellValue+        ]+      where+        orBlank :: (a -> Field) -> Maybe a -> Field+        orBlank = maybe ""+        renderAmount :: Amount -> Field+        renderAmount Amount { aCurrency = Currency {..}, ..} =+            toField $ formatScientific Fixed (Just cPrecision) aAmount+        renderCurrency :: Amount -> Field+        renderCurrency = toField . cTicker . aCurrency+++-- | An amount & currency specification.+data Amount = Amount+    { aAmount   :: Scientific+    -- ^ The total amount.+    , aCurrency :: Currency+    -- ^ The currency symbol & decimal-precision.+    }+    deriving (Show, Read, Eq)++-- | A pair containing a currency symbol & an amount.+data Currency = Currency+    { cPrecision :: Int+    -- ^ The number of decimals places of precision.+    , cTicker    :: T.Text+    -- ^ The ticker symbol+    }+    deriving (Show, Read, Eq, Generic)+++-- | Possible types for an imported transaction.+data CTTransactionType+    = Trade+    | Deposit+    | Withdrawal+    | Income+    | Mining+    | GiftTipIn+    | Spend+    | Donation+    | GiftOut+    | Stolen+    | Lost+    | Airdrop+    | Staking+    | Masternode+    | Minting+    | DividendsIncome+    | LendingIncome+    | InterestIncome+    | RewardBonus+    | MiningCommercial+    | MarginProfit+    | DerivativesFuturesProfit+    | OtherIncome+    | IncomeNonTaxable+    | OtherIncomeNonTaxable+    | MarginLoss+    | MarginFee+    | BorrowingFee+    | SettlementFee+    | DerivativesFuturesLoss+    | OtherFee+    | OtherExpense+    | ExpenseNonTaxable+    | MarginTrade+    | DerivativesFuturesTrade+    deriving (Show, Read, Eq, Ord, Bounded, Enum, Generic)++instance ToField CTTransactionType where+    toField = renderTransactionType++-- | Render the 'CTTransactionType' as CoinTracking displays/expects it.+renderTransactionType :: (IsString a) => CTTransactionType -> a+renderTransactionType = \case+    Trade                    -> "Trade"+    Deposit                  -> "Deposit"+    Withdrawal               -> "Withdrawal"+    Income                   -> "Income"+    Mining                   -> "Mining"+    GiftTipIn                -> "Gift/Tip(In)"+    Spend                    -> "Spend"+    Donation                 -> "Donation"+    GiftOut                  -> "Gift(Out)"+    Stolen                   -> "Stolen"+    Lost                     -> "Lost"+    Airdrop                  -> "Airdrop"+    Staking                  -> "Staking"+    Masternode               -> "Masternode"+    Minting                  -> "Minting"+    DividendsIncome          -> "Dividends Income"+    LendingIncome            -> "Lending Income"+    InterestIncome           -> "Interest Income"+    RewardBonus              -> "Reward / Bonus"+    MiningCommercial         -> "Mining (commercial)"+    MarginProfit             -> "Margin Profit"+    DerivativesFuturesProfit -> "Derivatives / Futures Profit"+    OtherIncome              -> "Other Income"+    IncomeNonTaxable         -> "Income (non taxable)"+    OtherIncomeNonTaxable    -> "Other Income (non taxable)"+    MarginLoss               -> "Margin Loss"+    MarginFee                -> "Margin Fee"+    BorrowingFee             -> "Borrowing Fee"+    SettlementFee            -> "Settlement Fee"+    DerivativesFuturesLoss   -> "Derivatives / Futures Loss"+    OtherFee                 -> "Other Fee"+    OtherExpense             -> "Other Expense"+    ExpenseNonTaxable        -> "Expense (non taxable)"+    MarginTrade              -> "Margin Trade"+    DerivativesFuturesTrade  -> "Derivatives / Futures Trade"
+ tests/Spec.hs view
@@ -0,0 +1,34 @@+import           Hedgehog+import           Test.Tasty+import           Test.Tasty.HUnit+import           Test.Tasty.Hedgehog++import qualified Hedgehog.Gen                  as Gen+import qualified Hedgehog.Range                as Range+++main :: IO ()+main = defaultMain tests+++tests :: TestTree+tests = testGroup "Tests" [unitTests, properties]+++unitTests :: TestTree+unitTests = testGroup "Unit Tests" [testCase "2+2 = 4" testAddition]+  where+    testAddition :: Assertion+    testAddition = (2 + 2) @?= (4 :: Integer)+++properties :: TestTree+properties = testGroup+    "Properties"+    [testProperty "Addition is Communative" testAdditionCommunative]+  where+    testAdditionCommunative :: Property+    testAdditionCommunative = property $ do+        let genInt = Gen.int $ Range.linear 0 9001+        (a, b) <- forAll $ (,) <$> genInt <*> genInt+        (a + b) === (b + a)