packages feed

tinkoff-invest-sdk-0.1.0.0: src/Invest/Service/MarketDataStream.hs

{-# OPTIONS_GHC -Wno-incomplete-patterns #-}
module Invest.Service.MarketDataStream(
    marketDataStream,
    subscribeOrderBook,
    unsubscribeOrderBook,
    close,
    wait
) where

import           Control.Concurrent                       (forkIO, newChan,
                                                           newEmptyMVar,
                                                           putMVar, readChan,
                                                           takeMVar, writeChan)
import           Control.Concurrent.Async                 (async, link)
import           Control.Concurrent.Chan                  (newChan, readChan,
                                                           writeChan)
import           Control.Error
import           Control.Exception                        ()
import           Control.Exception.Base                   (IOException)
import           Control.Lens                             ((&), (.~), (^.))
import           Control.Monad
import           Control.Monad.Cont                       (MonadIO (..))
import           Control.Monad.Except                     (MonadIO (..))
import           Control.Monad.IO.Class                   (MonadIO (..))
import           Control.Monad.Trans                      (MonadIO (..))
import           Data.Int                                 (Int32)
import           Data.ProtoLens.Message                   (defMessage)
import           Data.ProtoLens.Service.Types             ()
import           Data.Text                                as T (pack)
import           Invest.Client.Helpers                    (ChanFlow (Next),
                                                           GrpcClient)
import           Invest.Service.Internal.MarketDataStream (MDStream,
                                                           MDStreamMonad,
                                                           STRequest (..),
                                                           STResponse (..),
                                                           producedBy, (<@),
                                                           (@>))
import           Network.GRPC.Client                      (CompressMode (Compressed),
                                                           IncomingEvent (Headers, Invalid, RecvMessage, Trailers),
                                                           OutgoingEvent (Finalize, SendMessage))
import           Network.GRPC.Client.Helpers              (GrpcClient,
                                                           rawGeneralStream)
import           Network.GRPC.HTTP2.ProtoLens             (RPC (..))
import           Network.HTTP2.Client                     (runClientIO)
import           Proto.Invest.Marketdata
import qualified Proto.Invest.Marketdata_Fields           as MD

printIO :: (MonadIO m, Show a) => a -> m ()
printIO = liftIO . print

runAsync :: IO a -> IO ()
runAsync f = async f >>= link

marketDataStream :: GrpcClient -> MDStreamMonad
marketDataStream gc = liftIO $ do
    closed <- newEmptyMVar
    let close = liftIO . putMVar closed $ ()
    let genLoopInput chan = \case
            Headers hdrs -> printIO hdrs >> pure chan
            Trailers trls -> (liftIO . writeChan chan $ StreamStopped) >> close >> pure chan
            Invalid err -> (liftIO . writeChan chan $ StreamError err) >> close >> pure chan
            RecvMessage msg -> (liftIO . runAsync . writeChan chan $ Message msg) >> pure chan
    let genLoopOutput chan = (liftIO . readChan $ chan) >>= \case
            PostRequest msg -> pure (chan, SendMessage Compressed msg)
            Shutdown        -> pure (chan, Finalize)

    inputChannel <- newChan
    outputChannel <- newChan

    void . forkIO . void . runClientIO $
        rawGeneralStream (RPC :: RPC MarketDataStreamService "marketDataStream") gc inputChannel genLoopInput outputChannel genLoopOutput
    return (closed, outputChannel, inputChannel)

close :: MDStream -> IO ()
close stream = stream <@ Shutdown

wait :: MDStream -> IO ()
wait (closed, _, _) = takeMVar closed

subscribeMarketData :: MDStream -> MarketDataRequest -> (MarketDataResponse -> IO ChanFlow) -> IO ()
subscribeMarketData stream request callback = do
    stream @> \response -> if response `producedBy` request
        then callback response
        else pure Next
    stream <@ PostRequest request

-- OrderBook --
subscribeOrderBook :: MDStream -> String -> Int32 -> (MarketDataResponse -> IO ChanFlow) -> IO ()
subscribeOrderBook stream figi depth =
    subscribeMarketData stream (orderBookRequest [(figi, depth)] SUBSCRIPTION_ACTION_SUBSCRIBE)

unsubscribeOrderBook :: MDStream -> String -> Int32 -> IO ()
unsubscribeOrderBook stream figi depth =
    stream <@ PostRequest (orderBookRequest [(figi, depth)] SUBSCRIPTION_ACTION_UNSUBSCRIBE)

orderBookRequest :: [(String, Int32)] -> SubscriptionAction -> MarketDataRequest
orderBookRequest insts action = defMessage &
    MD.subscribeOrderBookRequest .~ (defMessage &
        MD.subscriptionAction .~ action &
        MD.instruments .~ map (\(figi, depth) -> defMessage & MD.figi .~ T.pack figi & MD.depth .~ depth) insts
    )

-- Candles --
subscribeCandles :: MDStream -> String -> SubscriptionInterval -> (MarketDataResponse -> IO ChanFlow) -> IO ()
subscribeCandles stream figi interval =
    subscribeMarketData stream (candlesRequest [(figi, interval)] SUBSCRIPTION_ACTION_SUBSCRIBE)

unsubscribeCandles :: MDStream -> String -> SubscriptionInterval -> IO ()
unsubscribeCandles stream figi interval =
    stream <@ PostRequest (candlesRequest [(figi, interval)] SUBSCRIPTION_ACTION_UNSUBSCRIBE)

candlesRequest :: [(String, SubscriptionInterval)] -> SubscriptionAction -> MarketDataRequest
candlesRequest insts action = defMessage &
    MD.subscribeCandlesRequest .~ (defMessage &
        MD.subscriptionAction .~ action &
        MD.instruments .~ map (\(figi, interval) -> defMessage & MD.figi .~ T.pack figi & MD.interval .~ interval) insts
    )

-- Trades --
subscribeTrades :: MDStream -> String -> (MarketDataResponse -> IO ChanFlow) -> IO ()
subscribeTrades stream figi =
    subscribeMarketData stream (tradesRequest [figi] SUBSCRIPTION_ACTION_SUBSCRIBE)

unsubscribeTrades :: MDStream -> String -> IO ()
unsubscribeTrades stream figi = stream <@ PostRequest (tradesRequest [figi] SUBSCRIPTION_ACTION_UNSUBSCRIBE)

tradesRequest :: [String] -> SubscriptionAction -> MarketDataRequest
tradesRequest figis action = defMessage &
    MD.subscribeTradesRequest .~ (defMessage &
        MD.subscriptionAction .~ action &
        MD.instruments .~ map (\figi -> defMessage & MD.figi .~ T.pack figi) figis
    )

-- Info --
subscribeInfo :: MDStream -> String -> (MarketDataResponse -> IO ChanFlow) -> IO ()
subscribeInfo stream figi = subscribeMarketData stream (infoRequest [figi] SUBSCRIPTION_ACTION_SUBSCRIBE)

unsubscribeInfo :: MDStream -> String -> IO ()
unsubscribeInfo stream figi = stream <@ PostRequest (infoRequest [figi] SUBSCRIPTION_ACTION_UNSUBSCRIBE)

infoRequest :: [String] -> SubscriptionAction -> MarketDataRequest
infoRequest figis action = defMessage &
    MD.subscribeInfoRequest .~ (defMessage &
        MD.subscriptionAction .~ action &
        MD.instruments .~ map (\figi -> defMessage & MD.figi .~ T.pack figi) figis
    )

-- Last Price --
subscribeLastPrice :: MDStream -> String -> (MarketDataResponse -> IO ChanFlow) -> IO ()
subscribeLastPrice stream figi = subscribeMarketData stream (lastPriceRequest [figi] SUBSCRIPTION_ACTION_SUBSCRIBE)

unsubscribeLastPrice :: MDStream -> String -> IO ()
unsubscribeLastPrice stream figi = stream <@ PostRequest (lastPriceRequest [figi] SUBSCRIPTION_ACTION_UNSUBSCRIBE)

lastPriceRequest :: [String] -> SubscriptionAction -> MarketDataRequest
lastPriceRequest figis action = defMessage &
    MD.subscribeLastPriceRequest .~ (defMessage &
        MD.subscriptionAction .~ action &
        MD.instruments .~ map (\figi -> defMessage & MD.figi .~ T.pack figi) figis
    )