-- | A wrapper around Yahoo API for downloading market data.
-- It sends a request to Yahoo's servers and returns results as they are, without any postprocessing.
-- The simples way to use it is to send an empty request. The last price is returned.
--
-- >>> fetch $ request "RSX"
-- Right [Price {date = <last_date>, ...}]
--
-- or, more explicitly
-- 
-- >>> fetchLatest "RSX"
-- Right (Price {date = <latest_date>, ...})
module Web.Data.Yahoo.API where
    
import Control.Lens ((^.))
import Data.Time.Calendar (Day, fromGregorian)
import Network.Wreq (get, responseBody)

import Web.Data.Yahoo.Utils (right)
import Web.Data.Yahoo.Request
    ( YahooRequest(..),
      TimeRange(Before, After, Range),
      Interval(Daily, Weekly),
      Ticker(..),
      requestUrl)

import Web.Data.Yahoo.Response (PriceResponse, tryParseAsPrice)

-- | An alias for a type representing the request record.
type Request = YahooRequest

-- | Sends a request and returns a list of prices or an error message.
fetch :: YahooRequest -> IO (Either String [PriceResponse])
fetch :: YahooRequest -> IO (Either String [PriceResponse])
fetch YahooRequest
request = do
    Response ByteString
response <- String -> IO (Response ByteString)
get (String -> IO (Response ByteString))
-> String -> IO (Response ByteString)
forall a b. (a -> b) -> a -> b
$ YahooRequest -> String
requestUrl YahooRequest
request
    Either String [PriceResponse] -> IO (Either String [PriceResponse])
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String [PriceResponse]
 -> IO (Either String [PriceResponse]))
-> Either String [PriceResponse]
-> IO (Either String [PriceResponse])
forall a b. (a -> b) -> a -> b
$ ByteString -> Either String [PriceResponse]
tryParseAsPrice (ByteString -> Either String [PriceResponse])
-> ByteString -> Either String [PriceResponse]
forall a b. (a -> b) -> a -> b
$ Response ByteString
response Response ByteString
-> Getting ByteString (Response ByteString) ByteString
-> ByteString
forall s a. s -> Getting a s a -> a
^. Getting ByteString (Response ByteString) ByteString
forall body0 body1.
Lens (Response body0) (Response body1) body0 body1
responseBody

-- | Sends a request for the specified ticker and returns its latest prices or an error code.
-- Throws an exception if the returned list of prices is empty.
fetchLatest :: String -> IO (Either String PriceResponse)
fetchLatest :: String -> IO (Either String PriceResponse)
fetchLatest String
ticker = do
    Response ByteString
response <- String -> IO (Response ByteString)
get (String -> IO (Response ByteString))
-> (String -> String) -> String -> IO (Response ByteString)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. YahooRequest -> String
requestUrl (YahooRequest -> String)
-> (String -> YahooRequest) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> YahooRequest
request (String -> IO (Response ByteString))
-> String -> IO (Response ByteString)
forall a b. (a -> b) -> a -> b
$ String
ticker
    Either String PriceResponse -> IO (Either String PriceResponse)
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String PriceResponse -> IO (Either String PriceResponse))
-> Either String PriceResponse -> IO (Either String PriceResponse)
forall a b. (a -> b) -> a -> b
$ ([PriceResponse] -> PriceResponse)
-> Either String [PriceResponse] -> Either String PriceResponse
forall t b a. (t -> b) -> Either a t -> Either a b
right [PriceResponse] -> PriceResponse
forall a. [a] -> a
head (Either String [PriceResponse] -> Either String PriceResponse)
-> (ByteString -> Either String [PriceResponse])
-> ByteString
-> Either String PriceResponse
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Either String [PriceResponse]
tryParseAsPrice (ByteString -> Either String PriceResponse)
-> ByteString -> Either String PriceResponse
forall a b. (a -> b) -> a -> b
$ Response ByteString
response Response ByteString
-> Getting ByteString (Response ByteString) ByteString
-> ByteString
forall s a. s -> Getting a s a -> a
^. Getting ByteString (Response ByteString) ByteString
forall body0 body1.
Lens (Response body0) (Response body1) body0 body1
responseBody

-- | Creates an unparameterized request for the ticker provided. 
-- If the request gets send without specifying any additional parameters, it'll return the latest price(s).
request :: String -> YahooRequest
request :: String -> YahooRequest
request String
t = YahooRequest :: Ticker -> Maybe Interval -> Maybe TimeRange -> YahooRequest
YahooRequest {
    ticker :: Ticker
ticker   = String -> Ticker
Ticker String
t,
    interval :: Maybe Interval
interval = Maybe Interval
forall a. Maybe a
Nothing,
    period :: Maybe TimeRange
period   = Maybe TimeRange
forall a. Maybe a
Nothing
}

-- | Specifies the request to query for daily data.
--
-- >>> fetch $ after (day 2021 01 07) . withDaily . request $ "RSX"
-- Right [Price {date = 2021-01-07, ...}, {date = 2021-01-08, ...} ...]
withDaily :: YahooRequest -> YahooRequest
withDaily :: YahooRequest -> YahooRequest
withDaily (YahooRequest {ticker :: YahooRequest -> Ticker
ticker = Ticker
t, period :: YahooRequest -> Maybe TimeRange
period = Maybe TimeRange
p}) = YahooRequest :: Ticker -> Maybe Interval -> Maybe TimeRange -> YahooRequest
YahooRequest {
    ticker :: Ticker
ticker   = Ticker
t,
    interval :: Maybe Interval
interval = Interval -> Maybe Interval
forall a. a -> Maybe a
Just Interval
Daily,
    period :: Maybe TimeRange
period   = Maybe TimeRange
p
}

-- | Specifies the request to query for weekly data. 
-- Note that Yahoo returns data for a beginning of every week specified, including the ones that don't fully fit within the specified range.
-- This might mean that the first record returned might refer to a day that comes before the time range specified in the request.
--
-- >>> fetch $ after (day 2021 01 08) . withWeekly . request $ "RSX"
-- Right [Price {date = 2021-01-04, ...},Price {date = 2021-01-11, ...}, ...]
withWeekly :: YahooRequest -> YahooRequest
withWeekly :: YahooRequest -> YahooRequest
withWeekly (YahooRequest {ticker :: YahooRequest -> Ticker
ticker = Ticker
t, period :: YahooRequest -> Maybe TimeRange
period = Maybe TimeRange
p}) = YahooRequest :: Ticker -> Maybe Interval -> Maybe TimeRange -> YahooRequest
YahooRequest {
    ticker :: Ticker
ticker   = Ticker
t,
    interval :: Maybe Interval
interval = Interval -> Maybe Interval
forall a. a -> Maybe a
Just Interval
Weekly,
    period :: Maybe TimeRange
period   = Maybe TimeRange
p
}

-- | Specifies the request to query for all the data available after the specified day, including this day.
--
-- >>> fetch $ after (day 2021 01 08) . request $ "RSX"
-- Right [Price {date = 2021-01-08, ...},Price {date = 2021-01-09, ...}, ...]
after :: Day -> YahooRequest -> YahooRequest
after :: Day -> YahooRequest -> YahooRequest
after Day
day (YahooRequest {ticker :: YahooRequest -> Ticker
ticker = Ticker
t, interval :: YahooRequest -> Maybe Interval
interval = Maybe Interval
i}) = YahooRequest :: Ticker -> Maybe Interval -> Maybe TimeRange -> YahooRequest
YahooRequest {
    ticker :: Ticker
ticker   = Ticker
t,
    interval :: Maybe Interval
interval = Maybe Interval
i,
    period :: Maybe TimeRange
period   = TimeRange -> Maybe TimeRange
forall a. a -> Maybe a
Just (TimeRange -> Maybe TimeRange) -> TimeRange -> Maybe TimeRange
forall a b. (a -> b) -> a -> b
$ Day -> TimeRange
After Day
day
}

-- | Specifies the request to query for all the data available before the specified day, excluding this day.
--
-- >>> fetch $ before (day 2021 01 08) . request $ "RSX"
-- Right [Price {date = 2007-04-30, ...}, ..., Price {date = 2021-01-07, ...}]
before :: Day -> YahooRequest -> YahooRequest
before :: Day -> YahooRequest -> YahooRequest
before Day
day (YahooRequest {ticker :: YahooRequest -> Ticker
ticker = Ticker
t, interval :: YahooRequest -> Maybe Interval
interval = Maybe Interval
i}) = YahooRequest :: Ticker -> Maybe Interval -> Maybe TimeRange -> YahooRequest
YahooRequest {
    ticker :: Ticker
ticker   = Ticker
t,
    interval :: Maybe Interval
interval = Maybe Interval
i,
    period :: Maybe TimeRange
period   = TimeRange -> Maybe TimeRange
forall a. a -> Maybe a
Just (TimeRange -> Maybe TimeRange) -> TimeRange -> Maybe TimeRange
forall a b. (a -> b) -> a -> b
$ Day -> TimeRange
Before Day
day
}

-- | Specifies the request to query for all the data between the range specified, including the opening day and excluding the end day.
--
-- >>> fetch $ between (day 2021 01 08, day 2021 01 12) . request $ "RSX"
-- Right [Price {date = 2021-01-08, ...}, Price {date = 2021-01-11, ...}]
between :: (Day, Day) -> YahooRequest -> YahooRequest
between :: (Day, Day) -> YahooRequest -> YahooRequest
between (Day
from, Day
to) (YahooRequest {ticker :: YahooRequest -> Ticker
ticker = Ticker
t, interval :: YahooRequest -> Maybe Interval
interval = Maybe Interval
i}) = YahooRequest :: Ticker -> Maybe Interval -> Maybe TimeRange -> YahooRequest
YahooRequest {
    ticker :: Ticker
ticker   = Ticker
t,
    interval :: Maybe Interval
interval = Maybe Interval
i,
    period :: Maybe TimeRange
period   = TimeRange -> Maybe TimeRange
forall a. a -> Maybe a
Just (TimeRange -> Maybe TimeRange) -> TimeRange -> Maybe TimeRange
forall a b. (a -> b) -> a -> b
$ Day -> Day -> TimeRange
Range Day
from Day
to
}

-- | An alias for "Data.Time.Calendar.fromGregorian". Included for convenience only. 
-- Feel free to use "fromGregorian" if it's more conveninent.
day :: Integer -> Int -> Int -> Day
day :: Integer -> Int -> Int -> Day
day = Integer -> Int -> Int -> Day
fromGregorian