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.
287 lines
11 KiB
287 lines
11 KiB
{-# LANGUAGE MultiWayIf #-} |
|
{-# LANGUAGE OverloadedStrings #-} |
|
{-# LANGUAGE QuasiQuotes #-} |
|
{-# LANGUAGE RecordWildCards #-} |
|
|
|
module ATrade.Broker.Protocol ( |
|
BrokerServerRequest(..), |
|
BrokerServerResponse(..), |
|
Notification(..), |
|
NotificationSqnum(..), |
|
nextSqnum, |
|
getNotificationSqnum, |
|
notificationOrderId, |
|
TradeSinkMessage(..), |
|
mkTradeMessage, |
|
ClientIdentity(..), |
|
RequestId(..), |
|
getRequestId, |
|
getResponseRequestId |
|
) where |
|
|
|
import ATrade.Types |
|
import Control.Applicative ((<|>)) |
|
import Control.Error.Util |
|
import Data.Aeson |
|
import Data.Aeson.KeyMap as KM |
|
import Data.Aeson.Types hiding (parse) |
|
import Data.Int |
|
import qualified Data.Text as T |
|
import Data.Text.Encoding |
|
import Data.Time.Calendar |
|
import Data.Time.Clock |
|
import Language.Haskell.Printf |
|
import Text.Parsec hiding ((<|>)) |
|
|
|
data RequestId = RequestId Int64 |
|
deriving (Eq, Show, Ord) |
|
|
|
type ClientIdentity = T.Text |
|
|
|
newtype NotificationSqnum = NotificationSqnum { unNotificationSqnum :: Int64 } |
|
deriving (Eq, Show, Ord) |
|
|
|
nextSqnum :: NotificationSqnum -> NotificationSqnum |
|
nextSqnum (NotificationSqnum n) = NotificationSqnum (n + 1) |
|
|
|
data Notification = OrderNotification NotificationSqnum OrderId OrderState | TradeNotification NotificationSqnum Trade |
|
deriving (Eq, Show) |
|
|
|
getNotificationSqnum :: Notification -> NotificationSqnum |
|
getNotificationSqnum (OrderNotification sqnum _ _) = sqnum |
|
getNotificationSqnum (TradeNotification sqnum _) = sqnum |
|
|
|
notificationOrderId :: Notification -> OrderId |
|
notificationOrderId (OrderNotification _ oid _) = oid |
|
notificationOrderId (TradeNotification _ trade) = tradeOrderId trade |
|
|
|
instance FromJSON Notification where |
|
parseJSON = withObject "notification" $ \obj -> parseNotification obj |
|
where |
|
parseNotification obj = |
|
case KM.lookup "notification-sqnum" obj of |
|
Just (Number sqnum) -> parseTrade (NotificationSqnum $ truncate sqnum) obj <|> |
|
parseOrder (NotificationSqnum $ truncate sqnum) obj <|> |
|
fail "Can't parse notification" |
|
Just _ -> fail "Invalid sqnum" |
|
Nothing -> fail "Unable to lookup notification sqnum" |
|
parseTrade sqnum obj = case KM.lookup "trade" obj of |
|
Just val -> TradeNotification sqnum <$> (parseJSON val) |
|
Nothing -> fail "Can't parse trade" |
|
parseOrder sqnum obj = case KM.lookup "order-state" obj of |
|
Just v -> withObject "object" (\os -> do |
|
oid <- os .: "order-id" |
|
ns <- os .: "new-state" |
|
return $ OrderNotification sqnum oid ns) v |
|
Nothing -> fail "Should be order-state" |
|
|
|
instance ToJSON Notification where |
|
toJSON (OrderNotification sqnum oid newState) = object [ "notification-sqnum" .= toJSON (unNotificationSqnum sqnum), "order-state" .= object [ "order-id" .= oid, "new-state" .= newState ] ] |
|
toJSON (TradeNotification sqnum trade) = object [ "notification-sqnum" .= toJSON (unNotificationSqnum sqnum), "trade" .= toJSON trade ] |
|
|
|
|
|
data BrokerServerRequest = RequestSubmitOrder RequestId ClientIdentity Order |
|
| RequestCancelOrder RequestId ClientIdentity OrderId |
|
| RequestNotifications RequestId ClientIdentity NotificationSqnum |
|
| RequestCurrentSqnum RequestId ClientIdentity |
|
| RequestSetClientIdentity RequestId ClientIdentity |
|
deriving (Eq, Show) |
|
|
|
getRequestId :: BrokerServerRequest -> RequestId |
|
getRequestId (RequestSubmitOrder rid _ _) = rid |
|
getRequestId (RequestCancelOrder rid _ _) = rid |
|
getRequestId (RequestNotifications rid _ _) = rid |
|
getRequestId (RequestCurrentSqnum rid _) = rid |
|
getRequestId (RequestSetClientIdentity rid _) = rid |
|
|
|
instance FromJSON BrokerServerRequest where |
|
parseJSON = withObject "object" (\obj -> do |
|
clientIdentity <- obj .: "client-identity" |
|
requestId <- obj .: "request-id" |
|
parseRequest (RequestId requestId) clientIdentity obj) |
|
where |
|
parseRequest :: RequestId -> ClientIdentity -> Object -> Parser BrokerServerRequest |
|
parseRequest requestId clientIdentity obj |
|
| KM.member "order" obj = do |
|
order <- obj .: "order" |
|
RequestSubmitOrder requestId clientIdentity <$> parseJSON order |
|
| KM.member "cancel-order" obj = do |
|
orderId <- obj .: "cancel-order" |
|
RequestCancelOrder requestId clientIdentity <$> parseJSON orderId |
|
| KM.member "request-notifications" obj = do |
|
initialSqnum <- obj .: "initial-sqnum" |
|
return (RequestNotifications requestId clientIdentity (NotificationSqnum initialSqnum)) |
|
| KM.member "request-current-sqnum" obj = |
|
return (RequestCurrentSqnum requestId clientIdentity) |
|
| KM.member "set-client-identity" obj = |
|
return (RequestSetClientIdentity requestId clientIdentity) |
|
parseRequest _ _ _ = fail "Invalid request object" |
|
|
|
instance ToJSON BrokerServerRequest where |
|
toJSON (RequestSubmitOrder (RequestId rid) clientIdentity order) = object [ |
|
"request-id" .= rid, |
|
"client-identity" .= clientIdentity, |
|
"order" .= order ] |
|
toJSON (RequestCancelOrder (RequestId rid) clientIdentity oid) = object [ |
|
"request-id" .= rid, |
|
"client-identity" .= clientIdentity, |
|
"cancel-order" .= oid ] |
|
toJSON (RequestNotifications (RequestId rid) clientIdentity initialNotificationSqnum) = object [ |
|
"request-id" .= rid, |
|
"client-identity" .= clientIdentity, |
|
"request-notifications" .= ("" :: T.Text), |
|
"initial-sqnum" .= unNotificationSqnum initialNotificationSqnum] |
|
toJSON (RequestCurrentSqnum (RequestId rid) clientIdentity) = object |
|
["request-id" .= rid, |
|
"client-identity" .= clientIdentity, |
|
"request-current-sqnum" .= ("" :: T.Text) ] |
|
toJSON (RequestSetClientIdentity (RequestId rid) clientIdentity) = object |
|
["request-id" .= rid, |
|
"client-identity" .= clientIdentity, |
|
"set-client-identity" .= ("" :: T.Text) ] |
|
|
|
getResponseRequestId :: BrokerServerResponse -> RequestId |
|
getResponseRequestId (ResponseOk reqId) = reqId |
|
getResponseRequestId (ResponseNotifications reqId _) = reqId |
|
getResponseRequestId (ResponseCurrentSqnum reqId _) = reqId |
|
getResponseRequestId (ResponseError reqId _) = reqId |
|
|
|
data BrokerServerResponse = ResponseOk RequestId |
|
| ResponseNotifications RequestId [Notification] |
|
| ResponseCurrentSqnum RequestId NotificationSqnum |
|
| ResponseError RequestId T.Text |
|
deriving (Eq, Show) |
|
|
|
instance FromJSON BrokerServerResponse where |
|
parseJSON = withObject "object" (\obj -> do |
|
requestId <- obj .: "request-id" |
|
if | KM.member "result" obj -> do |
|
result <- obj .: "result" |
|
if (result :: T.Text) == "success" |
|
then return $ ResponseOk (RequestId requestId) |
|
else do |
|
msg <- obj .:? "message" .!= "" |
|
return $ (ResponseError (RequestId requestId) msg) |
|
| KM.member "notifications" obj -> do |
|
notifications <- obj .: "notifications" |
|
ResponseNotifications (RequestId requestId) <$> parseJSON notifications |
|
| KM.member "current-sqnum" obj -> do |
|
rawSqnum <- obj .: "current-sqnum" |
|
return $ ResponseCurrentSqnum (RequestId requestId) (NotificationSqnum rawSqnum) |
|
| otherwise -> fail "Unable to parse BrokerServerResponse") |
|
|
|
instance ToJSON BrokerServerResponse where |
|
toJSON (ResponseOk (RequestId rid)) = object [ "request-id" .= rid, "result" .= ("success" :: T.Text) ] |
|
toJSON (ResponseNotifications (RequestId rid) notifications) = object [ "request-id" .= rid, "notifications" .= notifications ] |
|
toJSON (ResponseCurrentSqnum (RequestId rid) sqnum) = object [ "request-id" .= rid, "current-sqnum" .= unNotificationSqnum sqnum ] |
|
toJSON (ResponseError (RequestId rid) errorMessage) = object [ "request-id" .= rid, "result" .= ("error" :: T.Text), "message" .= errorMessage ] |
|
|
|
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 = [t|%04d-%02d-%02d %02d:%02d:%02d.%03d|] y m d hour min sec 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 KM.lookup "command" obj of |
|
Nothing -> parseTrade obj |
|
Just cmd -> return TradeSinkHeartBeat) |
|
where |
|
parseTrade obj = case KM.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"
|
|
|