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 +5/−0
- LICENSE +30/−0
- README.md +40/−0
- Setup.hs +2/−0
- cointracking-imports.cabal +106/−0
- src/Web/CoinTracking/Imports.hs +165/−0
- src/Web/CoinTracking/Imports/Types.hs +173/−0
- tests/Spec.hs +34/−0
+ 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++[](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)