{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module ModelTests (testModel) where
import Control.Exception
import Control.Lens
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Data.Int
import Data.Ratio
import Data.Time
import Data.Typeable
import Hedgehog hiding (Action)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Journal.Amount
import Journal.Model
import Journal.Types
import Test.HUnit.Lang (FailureReason (..))
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.Hedgehog
import Text.Show.Pretty
data MockFailure = MockFailure FailureReason
deriving (Eq, Typeable)
instance Show MockFailure where
show (MockFailure (Reason msg)) = msg
show (MockFailure (ExpectedButGot preface expected actual)) = msg
where
msg =
(case preface of Just str -> str ++ "\n"; Nothing -> "")
++ "expected:\n"
++ expected
++ "\n but got:\n"
++ actual
instance Exception MockFailure
assertEqual' ::
(Eq a, PrettyVal a, HasCallStack) =>
-- | The message prefix
String ->
-- | The expected value
a ->
-- | The actual value
a ->
Assertion
assertEqual' preface expected actual =
unless (actual == expected) $ do
throwIO
( MockFailure
( ExpectedButGot
( if Prelude.null preface
then Nothing
else Just preface
)
(dumpStr expected)
(dumpStr actual)
)
)
infix 1 @?==
(@?==) :: (MonadIO m, Eq a, PrettyVal a, HasCallStack) => a -> a -> m ()
actual @?== expected = liftIO $ assertEqual' "" expected actual
testModel :: TestTree
testModel =
testGroup
"model"
[ baseline
]
genUTCTime :: MonadGen m => m UTCTime
genUTCTime = do
y <- toInteger <$> Gen.int (Range.constant 2000 2019)
m <- Gen.int (Range.constant 1 12)
d <- Gen.int (Range.constant 1 28)
let day = fromGregorian y m d
secs <- toInteger <$> Gen.int (Range.constant 0 86401)
pure $ UTCTime day (secondsToDiffTime secs)
genAmount :: MonadGen m => Range Int64 -> m (Amount n)
genAmount range = do
d <- Gen.integral range
n <- Gen.integral range
pure $ Amount (d % n)
genTimed :: MonadGen m => m a -> m (Timed a)
genTimed gen = do
_time <- genUTCTime
_item <- gen
pure Timed {..}
genLot :: MonadGen m => m Lot
genLot = do
q <- genAmount (Range.linear 1 1000)
p <- genAmount (Range.linear 1 2000)
pure $ Lot q "FOO" p [] []
baseline :: TestTree
baseline =
testGroup
"baseline"
[ testProperty "simple-buy" simpleBuy,
testProperty "buy-buy" buyBuy,
testProperty "buy-sell-breakeven" buySellBreakeven,
testProperty "buy-sell-profit" buySellProfit,
testProperty "buy-sell-part-profit" buySellPartProfit,
testProperty "buy-buy-sell-loss" buyBuySellLoss,
testProperty "buy-sell-loss-buy" buySellLossBuy,
testProperty "buy-sell-loss" buySellLoss,
testProperty "simple-sell" simpleSell,
testProperty "sell-buy-profit" sellBuyProfit
]
simpleBuy :: Property
simpleBuy = property $ do
b <- forAll $ genTimed genLot
let c = (b ^. item . amount) * (b ^. item . price)
-- A simple buy
lift $
processActionsWithChanges
[Buy <$> b]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open]))
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
)
]
)
buyBuy :: Property
buyBuy = property $ do
b <- forAll $ genTimed genLot
let c = (b ^. item . amount) * (b ^. item . price)
lift $
processActionsWithChanges
[ Buy <$> b,
Buy <$> b
]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open]))
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
),
Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (2 * (c ^. coerced . to negate)),
Net (c ^. coerced . to negate)
]
)
]
)
buySellBreakeven :: Property
buySellBreakeven = property $ do
b <- forAll $ genTimed genLot
let c = (b ^. item . amount) * (b ^. item . price)
-- A buy and sell at break-even
lift $
processActionsWithChanges
[ Buy <$> b,
Sell <$> b
]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Sell <$> b),
Result
( Sell
<$> (b & item . details <>~ [Position Close, Gain 0])
),
RemoveEvent 1
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
),
Sell
<$> ( b & item . details
<>~ [ Position Close,
Gain 0,
Balance 0,
Net (c ^. coerced)
]
)
]
)
buySellProfit :: Property
buySellProfit = property $ do
b <- forAll $ genTimed genLot
let s = b
sp = s & item . price +~ 10
c = (b ^. item . amount) * (b ^. item . price)
sc = (sp ^. item . amount) * (sp ^. item . price)
-- A buy and sell at a profit
lift $
processActionsWithChanges
[ Buy <$> b,
Sell <$> sp
]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Sell <$> sp),
Result
( Sell
<$> (sp & item . details <>~ [Position Close, Gain 10])
),
RemoveEvent 1
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
),
Sell
<$> ( sp & item . details
<>~ [ Position Close,
Gain 10,
Balance (c ^. coerced . to negate + sc ^. coerced),
Net (sc ^. coerced)
]
)
]
)
buySellPartProfit :: Property
buySellPartProfit = property $ do
b <- forAll $ genTimed genLot
let b2 = b & item . amount *~ 2
s = b
sp = s & item . price +~ 10
c2 = (b2 ^. item . amount) * (b2 ^. item . price)
sc = (sp ^. item . amount) * (sp ^. item . price)
-- A buy and sell at a partial profit
lift $
processActionsWithChanges
[ Buy <$> b2,
Sell <$> sp
]
@?== Right
( [ SawAction (Buy <$> b2),
AddEvent (Opened True <$> b2),
Result (Buy <$> (b2 & item . details <>~ [Position Open])),
SawAction (Sell <$> sp),
Result
( Sell
<$> (sp & item . details <>~ [Position Close, Gain 10])
),
ReplaceEvent 1 (Opened True <$> b)
],
[ Buy
<$> ( b2 & item . details
<>~ [ Position Open,
Balance (c2 ^. coerced . to negate),
Net (c2 ^. coerced . to negate)
]
),
Sell
<$> ( sp & item . details
<>~ [ Position Close,
Gain 10,
Balance (c2 ^. coerced . to negate + sc ^. coerced),
Net (sc ^. coerced)
]
)
]
)
buySellLoss :: Property
buySellLoss = property $ do
b <- forAll $ genTimed genLot
let s = b
sl = s & item . price -~ 1
c = (b ^. item . amount) * (b ^. item . price)
sc = (sl ^. item . amount) * (sl ^. item . price)
-- A buy and sell at a loss
lift $
processActionsWithChanges
[ Buy <$> b,
Sell <$> sl
]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Sell <$> sl),
Result
( Sell
<$> ( sl & item . details
<>~ [Position Close, Loss 1]
)
),
RemoveEvent 1,
Submit (sl & item . price .~ 1),
SubmitEnd
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
),
Sell
<$> ( sl & item . details
<>~ [ Position Close,
Loss 1,
Balance
( c ^. coerced . to negate
+ sc ^. coerced
),
Net (sc ^. coerced)
]
)
]
)
buySellLossBuy :: Property
buySellLossBuy = property $ do
b <- forAll $ genTimed genLot
let s = b
sl =
s & item . price -~ 1
& item . details <>~ [WashTo "A" Nothing]
b2 =
b & item . details <>~ [WashApply "A" (b ^. item . amount)]
c = (b ^. item . amount) * (b ^. item . price)
c2 = (b2 ^. item . amount) * (b2 ^. item . price)
sc = (sl ^. item . amount) * (sl ^. item . price)
-- A buy and sell at a loss, followed by a buy
lift $
processActionsWithChanges
[ Buy <$> b,
Sell <$> sl,
Buy <$> b2
]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Sell <$> sl),
Result
( Sell
<$> ( sl & item . details
<>~ [Position Close, Loss 1]
)
),
RemoveEvent 1,
Submit (sl & item . price .~ 1),
SaveWash "A" (sl & item . price .~ 1),
Result (Wash <$> (sl & item . price .~ 1)),
SubmitEnd,
SawAction (Buy <$> b2),
AddEvent
( Opened True
<$> ( b2 & item . details
<>~ [Washed 1]
)
),
Result
( Buy
<$> ( b2 & item . details
<>~ [ Position Open,
Washed 1
]
)
)
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
),
Sell
<$> ( sl & item . details
<>~ [ Position Close,
Loss 1,
Balance
( c ^. coerced . to negate
+ sc ^. coerced
),
Net (sc ^. coerced)
]
),
Wash
<$> ( sl & item . price .~ 1
& item . details
<>~ [ Balance
( c ^. coerced . to negate
+ sc ^. coerced
),
Net 0
]
),
Buy
<$> ( b2 & item . details
<>~ [ Position Open,
Washed 1,
Balance
( c ^. coerced . to negate
+ sc ^. coerced
+ c2 ^. coerced . to negate
),
Net (c2 ^. coerced . to negate)
]
)
]
)
buyBuySellLoss :: Property
buyBuySellLoss = property $ do
b <- forAll $ genTimed genLot
let s = b
sl = s & item . price -~ 1
c = (b ^. item . amount) * (b ^. item . price)
sc = (sl ^. item . amount) * (sl ^. item . price)
-- A buy, a buy and then a sell at a loss
lift $
processActionsWithChanges
[ Buy <$> b,
Buy <$> b,
Sell <$> sl
]
@?== Right
( [ SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Buy <$> b),
AddEvent (Opened True <$> b),
Result (Buy <$> (b & item . details <>~ [Position Open])),
SawAction (Sell <$> sl),
Result
( Sell
<$> ( sl & item . details
<>~ [Position Close, Loss 1]
)
),
RemoveEvent 1,
Submit (sl & item . price .~ 1),
RemoveEvent 2,
AddEvent
( Opened True
<$> ( b & item . details
<>~ [Washed 1]
)
),
Result
( Wash
<$> ( b & item . price .~ 1
& item . details <>~ [Washed (b ^. item . price)]
)
),
SubmitEnd
],
[ Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate),
Net (c ^. coerced . to negate)
]
),
Buy
<$> ( b & item . details
<>~ [ Position Open,
Balance (c ^. coerced . to negate * 2),
Net (c ^. coerced . to negate)
]
),
Sell
<$> ( sl & item . details
<>~ [ Position Close,
Loss 1,
Balance
( c ^. coerced . to negate * 2
+ sc ^. coerced
),
Net (sc ^. coerced)
]
),
Wash
<$> ( b & item . price .~ 1
& item . details
<>~ [ Washed (b ^. item . price),
Balance
( c ^. coerced . to negate * 2
+ sc ^. coerced
),
Net 0
]
)
]
)
simpleSell :: Property
simpleSell = property $ do
s <- forAll $ genTimed genLot
let c = (s ^. item . amount) * (s ^. item . price)
-- A simple sell
lift $
processActionsWithChanges
[Sell <$> s]
@?== Right
( [ SawAction (Sell <$> s),
AddEvent (Opened False <$> s),
Result (Sell <$> (s & item . details <>~ [Position Open]))
],
[ Sell
<$> ( s & item . details
<>~ [ Position Open,
Balance (c ^. coerced),
Net (c ^. coerced)
]
)
]
)
sellBuyProfit :: Property
sellBuyProfit = property $ do
b <- forAll $ genTimed genLot
let s = b
c = (s ^. item . amount) * (s ^. item . price)
-- A sell and buy at a profit
lift $
processActionsWithChanges
[ Sell <$> s,
Buy <$> b
]
@?== Right
( [ SawAction (Sell <$> s),
AddEvent (Opened False <$> s),
Result (Sell <$> (s & item . details <>~ [Position Open])),
SawAction (Buy <$> b),
Result
( Buy
<$> (b & item . details <>~ [Position Close, Gain 0])
),
RemoveEvent 1
],
[ Sell
<$> ( s & item . details
<>~ [ Position Open,
Balance (c ^. coerced),
Net (c ^. coerced)
]
),
Buy
<$> ( b & item . details
<>~ [ Position Close,
Gain 0,
Balance 0,
Net (c ^. coerced . to negate)
]
)
]
)