Take the Monoid and Run(State-WriterT)!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
module Analytics.Trading.Advice.Backtesting where

import Control.Monad.State
import Control.Monad.Writer

import Data.Time

import Analytics.Trading.Data.Row          -- http://lpaste.net/109658
import Analytics.Trading.Data.Advice       -- http://lpaste.net/109712
import Control.Comonad                     -- http://lpaste.net/107661
import Control.List                        -- http://lpaste.net/107211
import Data.Monetary.USD                   -- http://lpaste.net/109597
import Control.DList                       -- http://lpaste.net/107607

import Analytics.Trading.Indicators.MovingAverages.Simple
 -- http://lpaste.net/109670

{--

The 'interesting' thing about recommend is that by looking into the 'future'
of the list looks you further into the past: head == today, head . tail ==
yesterday, head . tail . tail == day before, ... etc. So we can do predictive
recommendations on the future (tomorrow) by looking forward into the past.

 --}

recommend :: [Row] -> [USD] -> Advice
recommend history smas = 

-- let's define our strategy thusly:
-- BUY: last 4 days SMA < close && SMA(0) + $1 < close(0)
-- SELL: same, but opposite
-- HOLD: otherwise

   let verg = zipWith (\row d -> (close row, d)) history smas
   in  either (buyOrSell 0.1) (const HOLD) $ mbtake verg 4

{--

But ... should I buy, sell, or hold today?

*Main> let rows = readRows "screens/GMCR.csv" 
*Main> let smas = rows >>= \r -> return $ r =>> sma 15
*Main Control.Comonad> let s2 = smas >>= return . map USD
*Main Control.Comonad Control.Monad> liftM2 recommend rows s2
SELL

... so we want to sell. Why?

*Main Control.Comonad Control.Monad> rows >>= return . head
Row {date = 2014-08-15, open = $115.06, high = $115.17, low = $113.38,
     close = $115.00, volume = 1521800, adjClose = $115.00}
*Main Control.Comonad Control.Monad> s2 >>= return . head
$117.16

... because SMA is at least $1 above close and the last for SMA has been
    above close. That's why. Cool!

Now let's operationalize this over the course of the year:

 --}


-- backTest :: USD -> [Row] -> ([Row] -> USD) -> USD -> USD

-- ... No. Wait. Is the above type Peirce's Law? p -> q -> p -> p? ... anyway.

backTest :: USD -> [Row] -> ([Row] -> USD)
                -> Writer (DList (Day, USD, Advice)) ()
backTest costPerTrade screen indicator =
   let inds = screen =>> indicator
   in  b' screen inds
      where b' [] _ = return ()
            b' rows@(r:rs) inds =
               tell (dl' (date r, close r, recommend rows inds)) >>
               b' rs (tail inds)

{--

*Main> readRows "screens/GMCR-1mos.csv" >>= \rows ->
       return $ runWriter (backTest (USD 7) rows (USD . sma 15))

gets a whole lotta results -- one per row -- so let's do a few things:

1. filter out the HOLD recommendations.
2. reverse the recommendations, so they now go in chronological order, then:

play the game: buy when BUY rec, sell when SELL rec and you have shares to
sell (otherwise ignore sell recs with no holdings), and record the costs as
we go.

 --}

type Shares = Integer
playTheGame :: [(Day, USD, Advice)] -> USD 
               -> WriterT (DList String) (State (Shares, USD)) ()

playTheGame [] _ = return ()

playTheGame ((dt, closing, BUY):rest) cost = get >>= \(sh, m) -> 
   let askPrice = closing * USD 10.0 + cost
   in  (if askPrice > m then tell (dl' (show dt ++ ": unable to buy shares."))
        else tell (dl' (show dt ++ ": bought 10 shares for " ++ show askPrice)) >>
             put (sh + 10, m - askPrice)) >> playTheGame rest cost

playTheGame ((dt, closing, SELL):rest) cost = get >>= \(sh, m) ->
   let sharesSold = min 10 sh
       sellPrice = closing * USD (fromInteger sharesSold) - cost
   in  (if sh == 0 then tell (dl' (show dt ++ ": no shares to sell."))
        else tell (dl' (show dt ++ ": sold " ++ show sharesSold ++ " shares.")) >>
             put (sh - sharesSold, m + sellPrice)) >> playTheGame rest cost

playTheGame ((_, _, HOLD):rest) cost = playTheGame rest cost
 
{--

So, everything:

*Main> readRows "screens/GMCR.csv" >>= \rows -> 
       return $ snd $ runWriter (backTest (USD 7) rows (USD . sma 15))

... but actually difference lists are returned, so you have to 'dlToList'
the result to see it.

Just sayin'.

*Main> let res = dlToList it
*Main> *Main> let list = reverse res
list

[(2014-08-15,$115.00,HOLD),(2014-08-14,$114.30,HOLD),...(long list)

*Main> runState (runWriterT (playTheGame list (USD 7))) (0, 1000000)

The resulting log is also a difference list, so same procedure.

*Main> let ans = it
*Main> let log = dlToList (snd (fst ans))
*Main> (log, snd ans)

(["2013-10-14: no shares to sell.",
"2013-10-15: no shares to sell.",
"2013-10-18: no shares to sell.",
"2013-10-22: no shares to sell.",
"2013-10-24: no shares to sell.",
"2013-11-26: bought 10 shares for $648.59",
"2014-02-11: bought 10 shares for $1201.00",
"2014-02-12: bought 10 shares for $1206.59",
"2014-02-13: bought 10 shares for $1214.70",
"2014-02-14: bought 10 shares for $1167.30",
"2014-02-18: bought 10 shares for $1210.59",
"2014-02-19: bought 10 shares for $1186.40",
"2014-02-20: bought 10 shares for $1244.39",
"2014-02-21: bought 10 shares for $1237.40",
"2014-02-24: bought 10 shares for $1221.89",
"2014-02-25: bought 10 shares for $1182.00",
"2014-02-26: bought 10 shares for $1168.00",
"2014-05-14: bought 10 shares for $1167.00",
"2014-05-15: bought 10 shares for $1151.09",
"2014-05-16: bought 10 shares for $1151.80",
"2014-05-19: bought 10 shares for $1142.30",
"2014-05-20: bought 10 shares for $1145.09",
"2014-05-21: bought 10 shares for $1145.00"]),
(180,$979208.80))

So, original asset value was $1,000,000.00 and no shares == $1,000,000.00
End: $979,208.80 and 180 shares

valuation on 2014-08-15, closing price $115 == $20,700 + 979,208.80 ==

$999,908.80 ... so I lost about $100. WAAA! ;)

... but, ooh! I'm already thinking on how to tune my investment strategy! FUN!

 --}