packages feed

trade-journal-0.0.1: src/Journal/ThinkOrSwim.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Journal.ThinkOrSwim where

import Control.Lens
import Control.Monad.State
import qualified Data.ByteString as B
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as Csv
import Data.Map (Map)
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
-- import Data.Time
import Data.Vector (toList)
-- import Journal.Amount
import System.IO

data ThinkOrSwim = ThinkOrSwim
  { _account :: Text,
    _xacts :: [Csv.NamedRecord],
    _futures :: [Csv.NamedRecord],
    _forex :: [Csv.NamedRecord],
    _orders :: [Csv.NamedRecord],
    _trades :: [Csv.NamedRecord],
    _equities :: [Csv.NamedRecord],
    _options :: [Csv.NamedRecord],
    _pandl :: [Csv.NamedRecord],
    _byOrderId ::
      Map
        B.ByteString
        ([Csv.NamedRecord], [Csv.NamedRecord], [Csv.NamedRecord])
  }

makeLenses ''ThinkOrSwim

readSection :: Handle -> IO [Csv.NamedRecord]
readSection h =
  fmap (toList . snd)
    . check
    . Csv.decodeByNameWithP pure Csv.defaultDecodeOptions
    =<< readUntilBlank
  where
    readUntilBlank = do
      line <- BL.fromStrict <$> B.hGetLine h
      if BL.null line
        then pure line
        else do
          rest <- readUntilBlank
          pure $ line <> "\n" <> rest
    check :: MonadFail m => Either String a -> m a
    check (Left err) = fail $ "Error: " ++ err
    check (Right res) = pure res

readCsv :: FilePath -> IO ThinkOrSwim
readCsv path = withFile path ReadMode $ \h -> do
  "\65279Account" : "Statement" : "for" : _account : _ <-
    TL.words . TL.decodeUtf8 <$> readLine h
  "" <- readLine h
  "Cash Balance" <- readLine h
  _xacts <- readSection h
  "Futures Statements" <- readLine h
  _futures <- readSection h
  "Forex Statements" <- readLine h
  _forex <- readSection h
  " " <- readLine h
  "\"Total" : "Cash" : _ <-
    TL.words . TL.decodeUtf8 <$> readLine h
  "" <- readLine h
  "" <- readLine h
  "Account Order History" <- readLine h
  _orders <- readSection h
  "Account Trade History" <- readLine h
  _trades <- readSection h
  "Equities" <- readLine h
  _equities <- readSection h
  "Options" <- readLine h
  _options <- readSection h
  "Profits and Losses" <- readLine h
  _pandl <- readSection h
  let _byOrderId = flip execState mempty $ do
        forM_ _xacts $ \xact -> do
          entry (xact ^?! ix "REF #") . _1 <>= [xact]
        scanOrders _2 _orders
        scanOrders _3 _trades
  pure ThinkOrSwim {..}
  where
    entry oid = at oid . non ([], [], [])
    scanOrders l = flip foldM_ Nothing $ \lx x -> do
      let oid = x ^?! ix "Order ID"
      entry
        ( case lx of
            Nothing -> oid
            Just o
              | B.null oid -> o
              | otherwise -> oid
        )
        . l
        <>= [x]
      pure $
        if null x
          then lx
          else Just oid
    readLine :: Handle -> IO ByteString
    readLine h = do
      line <- BL.fromStrict <$> B.hGetLine h
      -- print line
      pure line