1 {-# LANGUAGE ViewPatterns, NoMonomorphismRestriction #-}
8 import Control
.Applicative
(pure
, (<*>), (<|
>))
9 import Control
.Monad
(Monad
, fail, return, (<=<), (>>=))
10 import Data
.Bool (Bool(False, True))
11 import Data
.Either (Either(Left
, Right
))
12 import Data
.Foldable
(foldMap
)
13 import Data
.Function
(on
)
14 import Data
.Functor
(fmap, (<$>))
15 import Data
.List
(minimum, maximum)
16 import Data
.Maybe (Maybe(Nothing
, Just
), catMaybes)
17 import Data
.Monoid
(Monoid
(..), Sum
(..), (<>))
18 import Data
.Ord
(Ord
(..), min, max)
19 import Data
.Ratio (Rational)
20 import Data
.Traversable
(mapM)
21 import Data
.Tuple
(curry, uncurry)
22 import Numeric
(readSigned, readFloat)
26 , print, putStr, putStrLn, read, show, take, undefined
31 import Data
.ByteString
(ByteString
)
32 import qualified Data
.ByteString
as BS
33 import qualified Data
.ByteString
.Lazy
as LBS
34 import qualified Data
.ByteString
.Char8
as CS
37 import Network
.URI
(URI
, parseAbsoluteURI
)
40 import Codec
.Utils
(Octet
)
41 import Data
.HMAC
(HashMethod
(..), hmac
)
42 import qualified Data
.Digest
.SHA512
as SHA512
(hash
)
45 import Network
.HTTP
.Base
46 ( Request
(..), Response
(..), RequestMethod
(..)
47 , urlEncodeVars
, httpPackageVersion
50 import Network
.HTTP
.Headers
(Header
(Header
), HeaderName
(HdrCustom
))
52 import qualified Network
.Browser
as Browser
(request
)
53 import Network
.Browser
54 ( Proxy
(NoProxy
), BrowserAction
55 , browse
, setAllowRedirects
, setMaxRedirects
, setAuthorities
56 , setAllowBasicAuth
, setMaxErrorRetries
, setMaxPoolSize
57 , setMaxAuthAttempts
, setProxy
, setUserAgent
63 , Value
(..), Result
(..)
64 , fromJSON
, json
, withArray
, withObject
67 import Data
.Aeson
.Types
(Parser
, parseEither
)
70 import Data
.Attoparsec
.ByteString
(parseOnly
)
71 import Data
.Attoparsec
.ByteString
.Lazy
(parse
, eitherResult
)
74 import Control
.Monad
.Trans
.Either (EitherT
, eitherT
, hoistEither
, left
, right
)
77 import Control
.Error
.Util
(note
)
80 import qualified Data
.Text
as T
81 import Data
.Text
(Text
)
84 import Data
.Time
(LocalTime
)
87 import Control
.Monad
.Trans
.Class
(lift
)
89 -- unordered-containers
90 import Data
.HashMap
.Strict
(HashMap
)
91 import qualified Data
.HashMap
.Strict
as HM
(lookup)
94 import Data
.Vector
(Vector
)
97 url
= "https://www.cryptsy.com/api"
100 mUri
= parseAbsoluteURI url
103 apiPubkey
= "5a8808b25e3f59d8818d3fbc0ce993fbb82dcf90"
108 get_nonce
:: IO Integer
115 sha512_hm
:: HashMethod
116 sha512_hm
= HashMethod
117 { digest
= SHA512
.hash
118 , input_blocksize
= 1024
121 hexdump
:: [Octet
] -> String
122 hexdump
= mconcat
. fmap show2Hex
126 readHex :: String -> Maybe [Octet
]
130 oldMain
= case (readHex apiPrivkey
, mUri
) of
133 putStrLn " could not be parsed as an absolute URI."
135 putStrLn "<apiPrivkey> could not be parsed as hex."
136 (Just pk
, Just uri
) -> do
139 postString
= urlEncodeVars
[ ("nonce", show nonce
) ]
140 postSig
= hexdump
. hmac sha512_hm pk
. BS
.unpack
$ CS
.pack postString
145 [ Header
(HdrCustom
"Key" ) apiPubkey
146 , Header
(HdrCustom
"Sign") postSig
148 , rqBody
= postString
152 (_
, response
) <- Browser
.request request
153 return $ rspBody response
156 initBrowser
:: BrowserAction t
()
158 setAllowRedirects
True
159 setMaxRedirects
$ Just
10
161 setAllowBasicAuth
False
162 setMaxErrorRetries
$ Just
0
163 setMaxPoolSize
$ Just
2
164 setMaxAuthAttempts
$ Just
0
166 setUserAgent
$ "Network.Browser/" <> httpPackageVersion
168 marketdatav2_url
:: String
169 marketdatav2_url
= "http://pubapi.cryptsy.com/api.php?method=marketdatav2"
171 parseResponseBody
:: LBS
.ByteString
-> Either String Value
172 parseResponseBody
= eitherResult
. parse json
175 dataKey
= T
.pack
"return"
177 extractData
:: Value
-> Maybe Value
178 extractData
(Object
(HM
.lookup dataKey
-> v
@(Just dat
))) = v
179 extractData _
= fail "Missing 'return' key."
181 newtype GGeneralMarketData p q dt t
=
182 GeneralMarketData
(HashMap Text
(GSingleGeneralMarketData p q dt t
))
184 type GeneralMarketData
= GGeneralMarketData
Rational Rational LocalTime
Rational
186 instance (FromJSON p
, FromJSON q
, FromJSON dt
, FromJSON t
) =>
187 FromJSON
(GGeneralMarketData p q dt t
)
189 parseJSON
= genGMD_parseJSON parseJSON parseJSON parseJSON parseJSON
191 genGMD_parseJSON
:: (Value
-> Parser p
) -> (Value
-> Parser q
) -> (Value
-> Parser dt
) -> (Value
-> Parser t
) -> Value
-> Parser
(GGeneralMarketData p q dt t
)
192 genGMD_parseJSON parsePrice parseQuantity parseDatetime parseTotal
=
193 withObject
"markets" (fmap GeneralMarketData
<$> mapM parseMarket
)
195 parseMarket
= genSGMD_parseJSON parsePrice parseQuantity parseDatetime parseTotal
197 data GSingleGeneralMarketData p q dt t
= SingleGeneralMarketData
200 , lasttradeprice
:: p
202 , lasttradetime
:: dt
203 , primaryname
:: Text
204 , primarycode
:: Text
205 , secondaryname
:: Text
206 , secondarycode
:: Text
207 , recenttrades
:: Vector
(GGeneralMarketDataTrade dt p q t
)
208 , sellorders
:: Vector
(GGeneralMarketDataOrder p q t
)
209 , buyorders
:: Vector
(GGeneralMarketDataOrder p q t
)
211 type SingleGeneralMarketData
= GSingleGeneralMarketData
Rational Rational LocalTime
Rational
213 instance (FromJSON p
, FromJSON q
, FromJSON dt
, FromJSON t
) =>
214 FromJSON
(GSingleGeneralMarketData p q dt t
)
216 parseJSON
= genSGMD_parseJSON parseJSON parseJSON parseJSON parseJSON
218 genSGMD_parseJSON
:: (Value
-> Parser p
) -> (Value
-> Parser q
) -> (Value
-> Parser dt
) -> (Value
-> Parser t
) -> Value
-> Parser
(GSingleGeneralMarketData p q dt t
)
219 genSGMD_parseJSON parsePrice parseQuantity parseDatetime parseTotal
(Object o
) =
220 SingleGeneralMarketData
<$>
221 o
.: T
.pack
"marketid" <*>
222 o
.: T
.pack
"label" <*>
223 (o
.: T
.pack
"lasttradeprice" >>= parsePrice
) <*>
224 (o
.: T
.pack
"volume" >>= parseQuantity
) <*>
225 (o
.: T
.pack
"lasttradetime" >>= parseDatetime
) <*>
226 o
.: T
.pack
"primaryname" <*>
227 o
.: T
.pack
"primarycode" <*>
228 o
.: T
.pack
"secondaryname" <*>
229 o
.: T
.pack
"secondarycode" <*>
230 (o
.: T
.pack
"recenttrades" >>= parseTrades
) <*>
231 (o
.: T
.pack
"sellorders" >>= parseOrders
) <*>
232 (o
.: T
.pack
"buyorders" >>= parseOrders
)
234 parseTrades
= withArray
"trades" . mapM $ genGMDT_parseJSON parseDatetime parsePrice parseQuantity parseTotal
235 parseOrders
= withArray
"orders" . mapM $ genGMDO_parseJSON parsePrice parseQuantity parseTotal
236 genSGMD_parseJSON _ _ _ _ _
=
237 fail "Data for a single market is a JSON object."
239 data GGeneralMarketDataTrade dt p q t
= GeneralMarketDataTrade
246 type GeneralMarketDataTrade
= GGeneralMarketDataTrade LocalTime
Rational Rational Rational
248 instance (FromJSON dt
, FromJSON p
, FromJSON q
, FromJSON t
) =>
249 FromJSON
(GGeneralMarketDataTrade dt p q t
)
251 parseJSON
= genGMDT_parseJSON parseJSON parseJSON parseJSON parseJSON
253 genGMDT_parseJSON
:: (Value
-> Parser dt
) -> (Value
-> Parser p
) -> (Value
-> Parser q
) -> (Value
-> Parser t
) -> Value
-> Parser
(GGeneralMarketDataTrade dt p q t
)
254 genGMDT_parseJSON parseDatetime parsePrice parseQuantity parseTotal
(Object o
) =
255 GeneralMarketDataTrade
<$>
257 (o
.: T
.pack
"time" >>= parseDatetime
) <*>
258 (o
.: T
.pack
"price" >>= parsePrice
) <*>
259 (o
.: T
.pack
"quantity" >>= parseQuantity
) <*>
260 (o
.: T
.pack
"total" >>= parseTotal
)
261 genGMDT_parseJSON _ _ _ _ _
=
262 fail "A trade is a JSON object."
264 data GGeneralMarketDataOrder p q t
= GeneralMarketDataOrder
269 type GeneralMarketDataOrder
= GGeneralMarketDataOrder
Rational Rational Rational
271 instance (FromJSON p
, FromJSON q
, FromJSON t
) =>
272 FromJSON
(GGeneralMarketDataOrder p q t
)
274 parseJSON
= genGMDO_parseJSON parseJSON parseJSON parseJSON
276 genGMDO_parseJSON
:: (Value
-> Parser p
) -> (Value
-> Parser q
) -> (Value
-> Parser t
) -> Value
-> Parser
(GGeneralMarketDataOrder p q t
)
277 genGMDO_parseJSON parsePrice parseQuantity parseTotal
(Object o
) =
278 GeneralMarketDataOrder
<$>
279 (o
.: T
.pack
"price" >>= parsePrice
) <*>
280 (o
.: T
.pack
"quantity" >>= parseQuantity
) <*>
281 (o
.: T
.pack
"total" >>= parseTotal
)
282 genGMDO_parseJSON _ _ _ _
=
283 fail "An order is a JSON object."
285 extractMarkets
:: Value
-> Either String Value
286 extractMarkets
(Object
(HM
.lookup (T
.pack
"markets") -> Just m
)) = Right m
287 extractMarkets _
= Left
"Missing 'markets' key."
289 hoistResult
:: Monad m
=> Result a
-> EitherT
String m a
290 hoistResult
(Error s
) = left s
291 hoistResult
(Success a
) = right a
293 parseRational
:: Value
-> Parser
Rational
294 parseRational v
= parseJSON v
<|
> parseRatString
298 case readSigned readFloat str
of
299 [] -> fail "No parse."
300 [(r
, "")] -> return r
301 _
-> fail "Ambiguous parse."
303 newtype Min n
= Min
{ getMin
:: Maybe n
} deriving Show
304 instance Ord n
=> Monoid
(Min n
) where
306 mappend
(Min Nothing
) m
= m
307 mappend m
(Min Nothing
) = m
308 mappend
(Min
(Just m
)) (Min
(Just n
)) = Min
. Just
$ min m n
309 mconcat
[] = Min Nothing
310 mconcat mins
= Min
. Just
. minimum . catMaybes $ fmap getMin mins
312 newtype Max n
= Max
{ getMax
:: Maybe n
} deriving Show
313 instance Ord n
=> Monoid
(Max n
) where
315 mappend
(Max Nothing
) m
= m
316 mappend m
(Max Nothing
) = m
317 mappend
(Max
(Just m
)) (Max
(Just n
)) = Max
. Just
$ max m n
318 mconcat
[] = Max Nothing
319 mconcat maxs
= Max
. Just
. maximum . catMaybes $ fmap getMax maxs
321 data MarketSummary p q t
= MarketSummary
324 , count
:: Sum
Integer
325 , primaryVolume
:: Sum q
326 , secondaryVolume
:: Sum t
328 instance (Ord p
, Num q
, Num t
) => Monoid
(MarketSummary p q t
) where
329 mempty
= MarketSummary
335 mappend
= curry $ MarketSummary
<$>
336 uncurry (mappend `on` minPrice
) <*>
337 uncurry (mappend `on` maxPrice
) <*>
338 uncurry (mappend `on` count
) <*>
339 uncurry (mappend `on` primaryVolume
) <*>
340 uncurry (mappend `on` secondaryVolume
)
341 mconcat
= MarketSummary
<$>
342 (mconcat
. fmap minPrice
) <*>
343 (mconcat
. fmap maxPrice
) <*>
344 (mconcat
. fmap count
) <*>
345 (mconcat
. fmap primaryVolume
) <*>
346 (mconcat
. fmap secondaryVolume
)
348 tradeSummary
:: GGeneralMarketDataTrade dt p q t
-> MarketSummary p q t
349 tradeSummary trade
= MarketSummary
350 { minPrice
= Min
. Just
$ tPrice trade
351 , maxPrice
= Max
. Just
$ tPrice trade
353 , primaryVolume
= Sum
$ tQuantity trade
354 , secondaryVolume
= Sum
$ tTotal trade
358 main
= case parseAbsoluteURI marketdatav2_url
of
360 putStr marketdatav2_url
361 putStrLn " could not be parsed as an absolute URI."
362 Just uri
-> eitherT
putStrLn print $ do
363 body
<- lift
$ browse
$ do
372 (_
, resp
) <- Browser
.request req
373 return $ rspBody resp
374 markets
<- hoistEither
$ do
375 val
<- parseResponseBody body
376 dat
<- note
"No 'return'" $ extractData val
377 jsonMkts
<- extractMarkets dat
378 GeneralMarketData mkts
<- parseEither
(genGMD_parseJSON parseRational parseRational pure parseRational
) jsonMkts
380 mkt
<- hoistEither
. note
"No 'LTC/BTC' market." $ HM
.lookup (T
.pack
"LTC/BTC") markets
381 return . foldMap tradeSummary
$ recenttrades mkt