ATrade core infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

231 lines
8.2 KiB

{-# LANGUAGE OverloadedStrings, MultiWayIf, RecordWildCards #-}
module ATrade.Broker.Protocol (
BrokerServerRequest(..),
BrokerServerResponse(..),
Notification(..),
notificationOrderId,
RequestSqnum(..),
requestSqnum,
TradeSinkMessage(..),
mkTradeMessage,
ClientIdentity(..)
) where
import Control.Error.Util
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import Data.Text.Format
import Data.Text.Encoding
import Data.Aeson
import Data.Aeson.Types hiding (parse)
import Data.Int
import Data.Time.Clock
import Data.Time.Calendar
import ATrade.Types
import Text.Parsec
type ClientIdentity = T.Text
type RequestSqnum = Int64
data BrokerServerRequest = RequestSubmitOrder RequestSqnum ClientIdentity Order
| RequestCancelOrder RequestSqnum ClientIdentity OrderId
| RequestNotifications RequestSqnum ClientIdentity
deriving (Eq, Show)
requestSqnum :: BrokerServerRequest -> RequestSqnum
requestSqnum (RequestSubmitOrder sqnum _ _) = sqnum
requestSqnum (RequestCancelOrder sqnum _ _) = sqnum
requestSqnum (RequestNotifications sqnum _) = sqnum
instance FromJSON BrokerServerRequest where
parseJSON = withObject "object" (\obj -> do
sqnum <- obj .: "request-sqnum"
clientIdentity <- obj .: "client-identity"
parseRequest sqnum clientIdentity obj)
where
parseRequest :: RequestSqnum -> ClientIdentity -> Object -> Parser BrokerServerRequest
parseRequest sqnum clientIdentity obj
| HM.member "order" obj = do
order <- obj .: "order"
RequestSubmitOrder sqnum clientIdentity <$> parseJSON order
| HM.member "cancel-order" obj = do
orderId <- obj .: "cancel-order"
RequestCancelOrder sqnum clientIdentity <$> parseJSON orderId
| HM.member "request-notifications" obj = return (RequestNotifications sqnum clientIdentity)
parseRequest _ _ _ = fail "Invalid request object"
instance ToJSON BrokerServerRequest where
toJSON (RequestSubmitOrder sqnum clientIdentity order) = object ["request-sqnum" .= sqnum,
"client-identity" .= clientIdentity,
"order" .= order ]
toJSON (RequestCancelOrder sqnum clientIdentity oid) = object ["request-sqnum" .= sqnum,
"client-identity" .= clientIdentity,
"cancel-order" .= oid ]
toJSON (RequestNotifications sqnum clientIdentity) = object ["request-sqnum" .= sqnum,
"client-identity" .= clientIdentity,
"request-notifications" .= ("" :: T.Text) ]
data BrokerServerResponse = ResponseOrderSubmitted OrderId
| ResponseOrderCancelled OrderId
| ResponseNotifications [Notification]
| ResponseError T.Text
deriving (Eq, Show)
instance FromJSON BrokerServerResponse where
parseJSON = withObject "object" (\obj ->
if | HM.member "order-id" obj -> do
oid <- obj .: "order-id"
return $ ResponseOrderSubmitted oid
| HM.member "order-cancelled" obj -> do
oid <- obj .: "order-cancelled"
return $ ResponseOrderCancelled oid
| HM.member "notifications" obj -> do
notifications <- obj .: "notifications"
ResponseNotifications <$> parseJSON notifications
| HM.member "error" obj -> do
error <- obj .: "error"
ResponseError <$> parseJSON error)
instance ToJSON BrokerServerResponse where
toJSON (ResponseOrderSubmitted oid) = object [ "order-id" .= oid ]
toJSON (ResponseOrderCancelled oid) = object [ "order-cancelled" .= oid ]
toJSON (ResponseNotifications notifications) = object [ "notifications" .= notifications ]
toJSON (ResponseError errorMessage) = object [ "error" .= errorMessage ]
data Notification = OrderNotification OrderId OrderState | TradeNotification Trade
deriving (Eq, Show)
notificationOrderId :: Notification -> OrderId
notificationOrderId (OrderNotification oid _) = oid
notificationOrderId (TradeNotification trade) = tradeOrderId trade
instance FromJSON Notification where
parseJSON n = withObject "notification" (\obj ->
case HM.lookup "trade" obj of
Just v -> parseTrade v
Nothing -> parseOrder n) n
where
parseTrade v = TradeNotification <$> parseJSON v
parseOrder (Object o) = case HM.lookup "order-state" o of
Just v -> withObject "object" (\os -> do
oid <- os .: "order-id"
ns <- os .: "new-state"
return $ OrderNotification oid ns) v
Nothing -> fail "Should be order-state"
parseOrder _ = fail "Unable to parse order state"
instance ToJSON Notification where
toJSON (OrderNotification oid newState) = object ["order-state" .= object [ "order-id" .= oid, "new-state" .= newState] ]
toJSON (TradeNotification trade) = object ["trade" .= toJSON trade]
data TradeSinkMessage = TradeSinkHeartBeat | TradeSinkTrade {
tsAccountId :: T.Text,
tsSecurity :: T.Text,
tsPrice :: Double,
tsQuantity :: Int,
tsVolume :: Double,
tsCurrency :: T.Text,
tsOperation :: Operation,
tsExecutionTime :: UTCTime,
tsCommission :: Double,
tsSignalId :: SignalId
} deriving (Show, Eq)
mkTradeMessage trade = TradeSinkTrade {
tsAccountId = tradeAccount trade,
tsSecurity = tradeSecurity trade,
tsPrice = (toDouble . tradePrice) trade,
tsQuantity = (fromInteger . tradeQuantity) trade,
tsVolume = (toDouble . tradeVolume) trade,
tsCurrency = tradeVolumeCurrency trade,
tsOperation = tradeOperation trade,
tsExecutionTime = tradeTimestamp trade,
tsCommission = toDouble $ tradeCommission trade,
tsSignalId = tradeSignalId trade
}
where
toDouble = fromRational . toRational
getHMS :: UTCTime -> (Int, Int, Int, Int)
getHMS (UTCTime _ diff) = (intsec `div` 3600, (intsec `mod` 3600) `div` 60, intsec `mod` 60, msec)
where
intsec = floor diff
msec = floor $ (diff - fromIntegral intsec) * 1000
formatTimestamp dt = format "{}-{}-{} {}:{}:{}.{}" (left 4 '0' y, left 2 '0' m, left 2 '0' d, left 2 '0' hour, left 2 '0' min, left 2 '0' sec, left 3 '0' msec)
where
(y, m, d) = toGregorian $ utctDay dt
(hour, min, sec, msec) = getHMS dt
parseTimestamp (String t) = case hush $ parse p "" t of
Just ts -> return ts
Nothing -> fail "Unable to parse timestamp"
where
p = do
year <- read <$> many1 digit
char '-'
mon <- read <$> many1 digit
char '-'
day <- read <$> many1 digit
char ' '
hour <- read <$> many1 digit
char ':'
min <- read <$> many1 digit
char ':'
sec <- read <$> many1 digit
char '.'
msec <- many1 digit -- TODO use msec
return $ UTCTime (fromGregorian year mon day) (secondsToDiffTime $ hour * 3600 + min * 60 + sec)
parseTimestamp _ = fail "Unable to parse timestamp: invalid type"
instance ToJSON TradeSinkMessage where
toJSON TradeSinkHeartBeat = object ["command" .= T.pack "heartbeat" ]
toJSON TradeSinkTrade { .. } = object ["trade" .= object ["account" .= tsAccountId,
"security" .= tsSecurity,
"price" .= tsPrice,
"quantity" .= tsQuantity,
"volume" .= tsVolume,
"volume-currency" .= tsCurrency,
"operation" .= tsOperation,
"execution-time" .= formatTimestamp tsExecutionTime,
"commission" .= tsCommission,
"strategy" .= strategyId tsSignalId,
"signal-id" .= signalName tsSignalId,
"comment" .= comment tsSignalId]]
instance FromJSON TradeSinkMessage where
parseJSON = withObject "object" (\obj ->
case HM.lookup "command" obj of
Nothing -> parseTrade obj
Just cmd -> return TradeSinkHeartBeat)
where
parseTrade obj = case HM.lookup "trade" obj of
Just (Object v) -> do
acc <- v .: "account"
sec <- v .: "security"
pr <- v .: "price"
q <- v .: "quantity"
vol <- v .: "volume"
cur <- v .: "volume-currency"
op <- v .: "operation"
commission <- v .:? "commission" .!= 0
extime <- v .: "execution-time" >>= parseTimestamp
strategy <- v .: "strategy"
sid <- v .: "signal-id"
com <- v .: "comment"
return TradeSinkTrade {
tsAccountId = acc,
tsSecurity = sec,
tsPrice = pr,
tsQuantity = q,
tsVolume = vol,
tsCurrency = cur,
tsOperation = op,
tsExecutionTime = extime,
tsCommission = commission,
tsSignalId = SignalId strategy sid com }
_ -> fail "Should've been trade object"